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:
@@ -3,6 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
@@ -12,6 +15,9 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity]),
|
||||
ConnectedAccountModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageChannelDataAccessModule,
|
||||
MessageFolderDataAccessModule,
|
||||
MetricsModule,
|
||||
],
|
||||
providers: [MessageChannelSyncStatusService],
|
||||
|
||||
+16
-14
@@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
@@ -34,22 +35,19 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConnectedAccountRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
findOneByOrFail: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockGlobalWorkspaceOrmManager = {
|
||||
getRepository: jest.fn().mockImplementation((workspaceId, name) => {
|
||||
if (name === 'messageChannelMessageAssociation') {
|
||||
return mockMessageChannelMessageAssociationRepository;
|
||||
}
|
||||
if (name === 'connectedAccount') {
|
||||
return mockConnectedAccountRepository;
|
||||
}
|
||||
if (name === 'workspaceMember') {
|
||||
return mockWorkspaceMemberRepository;
|
||||
}
|
||||
@@ -67,6 +65,10 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: mockGlobalWorkspaceOrmManager,
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -105,7 +107,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
item.subject === 'Test Subject' && item.text === 'Test Message',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
|
||||
expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return message without obfuscated subject and with obfuscated text if the visibility is SUBJECT', async () => {
|
||||
@@ -123,7 +125,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
|
||||
|
||||
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
@@ -158,7 +160,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
|
||||
|
||||
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
@@ -198,7 +200,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-account-owner-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
messages,
|
||||
@@ -233,7 +235,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-not-account-owner-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
messages,
|
||||
@@ -279,7 +281,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find
|
||||
mockConnectedAccountDataAccessService.find
|
||||
.mockResolvedValueOnce([]) // request for message 3
|
||||
.mockResolvedValueOnce([]); // request for message 2
|
||||
|
||||
@@ -338,7 +340,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find
|
||||
mockConnectedAccountDataAccessService.find
|
||||
.mockResolvedValueOnce([]) // request for message 3
|
||||
.mockResolvedValueOnce([]); // request for message 2
|
||||
|
||||
|
||||
+5
-12
@@ -6,9 +6,9 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
@@ -18,6 +18,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
export class ApplyMessagesVisibilityRestrictionsService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
public async applyMessagesVisibilityRestrictions(
|
||||
@@ -43,12 +44,6 @@ export class ApplyMessagesVisibilityRestrictionsService {
|
||||
relations: ['messageChannel'],
|
||||
});
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
@@ -91,15 +86,13 @@ export class ApplyMessagesVisibilityRestrictionsService {
|
||||
userId,
|
||||
});
|
||||
|
||||
const connectedAccounts = await connectedAccountRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
const connectedAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId, {
|
||||
messageChannels: {
|
||||
id: In(messageChannels.map((channel) => channel.id)),
|
||||
},
|
||||
accountOwnerId: workspaceMember.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (connectedAccounts.length > 0) {
|
||||
continue;
|
||||
|
||||
+25
-30
@@ -14,6 +14,8 @@ import {
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import {
|
||||
@@ -21,10 +23,7 @@ import {
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessageFolderPendingSyncAction,
|
||||
type MessageFolderWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
|
||||
|
||||
const ONGOING_SYNC_STAGES = [
|
||||
@@ -41,6 +40,8 @@ export class MessageChannelUpdateOnePreQueryHook
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
|
||||
) {}
|
||||
|
||||
@@ -57,15 +58,10 @@ export class MessageChannelUpdateOnePreQueryHook
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const messageChannel = await messageChannelRepository.findOne({
|
||||
where: { id: payload.id },
|
||||
});
|
||||
const messageChannel =
|
||||
await this.messageChannelDataAccessService.findOne(workspace.id, {
|
||||
where: { id: payload.id },
|
||||
});
|
||||
|
||||
if (!isDefined(messageChannel)) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
@@ -77,29 +73,27 @@ export class MessageChannelUpdateOnePreQueryHook
|
||||
);
|
||||
}
|
||||
|
||||
const messageChannelWorkspace =
|
||||
messageChannel as unknown as MessageChannelWorkspaceEntity;
|
||||
|
||||
const isSyncOngoing = ONGOING_SYNC_STAGES.includes(
|
||||
messageChannel.syncStage,
|
||||
messageChannelWorkspace.syncStage,
|
||||
);
|
||||
|
||||
const messageFolderRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'messageFolder',
|
||||
);
|
||||
const messageFoldersWithPendingAction =
|
||||
await this.messageFolderDataAccessService.find(workspace.id, {
|
||||
messageChannelId: messageChannel.id,
|
||||
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
|
||||
});
|
||||
|
||||
const messageFoldersWithPendingActionCount =
|
||||
await messageFolderRepository.count({
|
||||
where: {
|
||||
messageChannelId: messageChannel.id,
|
||||
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
|
||||
},
|
||||
});
|
||||
messageFoldersWithPendingAction.length;
|
||||
|
||||
const hasPendingFolderActions =
|
||||
messageFoldersWithPendingActionCount > 0;
|
||||
|
||||
const hasPendingGroupEmailsAction =
|
||||
messageChannel.pendingGroupEmailsAction !==
|
||||
messageChannelWorkspace.pendingGroupEmailsAction !==
|
||||
MessageChannelPendingGroupEmailsAction.NONE;
|
||||
|
||||
if (
|
||||
@@ -116,12 +110,12 @@ export class MessageChannelUpdateOnePreQueryHook
|
||||
}
|
||||
|
||||
const hasCompletedConfiguration =
|
||||
messageChannel.syncStage !==
|
||||
messageChannelWorkspace.syncStage !==
|
||||
MessageChannelSyncStage.PENDING_CONFIGURATION;
|
||||
|
||||
if (!hasCompletedConfiguration) {
|
||||
this.logger.log(
|
||||
`MessageChannelId: ${messageChannel.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`,
|
||||
`MessageChannelId: ${messageChannelWorkspace.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`,
|
||||
);
|
||||
|
||||
return payload;
|
||||
@@ -129,11 +123,12 @@ export class MessageChannelUpdateOnePreQueryHook
|
||||
|
||||
const excludeGroupEmailsChanged =
|
||||
isDefined(payload.data.excludeGroupEmails) &&
|
||||
payload.data.excludeGroupEmails !== messageChannel.excludeGroupEmails;
|
||||
payload.data.excludeGroupEmails !==
|
||||
messageChannelWorkspace.excludeGroupEmails;
|
||||
|
||||
if (excludeGroupEmailsChanged) {
|
||||
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
|
||||
messageChannel,
|
||||
messageChannelWorkspace,
|
||||
workspace.id,
|
||||
payload.data.excludeGroupEmails
|
||||
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
|
||||
|
||||
+9
-1
@@ -1,5 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
|
||||
import { MessageChannelUpdateOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook';
|
||||
import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook';
|
||||
@@ -7,7 +10,12 @@ import { MessageFindOnePostQueryHook } from 'src/modules/messaging/common/query-
|
||||
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [MessagingImportManagerModule],
|
||||
imports: [
|
||||
MessagingImportManagerModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageChannelDataAccessModule,
|
||||
MessageFolderDataAccessModule,
|
||||
],
|
||||
providers: [
|
||||
ApplyMessagesVisibilityRestrictionsService,
|
||||
MessageFindOnePostQueryHook,
|
||||
|
||||
+132
-148
@@ -7,10 +7,12 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
@@ -18,10 +20,8 @@ import {
|
||||
MessageChannelSyncStatus,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessageFolderPendingSyncAction,
|
||||
MessageFolderWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelSyncStatusService {
|
||||
@@ -29,6 +29,9 @@ export class MessageChannelSyncStatusService {
|
||||
@InjectCacheStorage(CacheStorageNamespace.ModuleMessaging)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||
private readonly metricsService: MetricsService,
|
||||
) {}
|
||||
@@ -45,16 +48,14 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -70,16 +71,14 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -100,27 +99,20 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncCursor: '',
|
||||
syncStageStartedAt: null,
|
||||
throttleFailureCount: 0,
|
||||
throttleRetryAfter: null,
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
|
||||
},
|
||||
);
|
||||
|
||||
const messageFolderRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageFolder',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncCursor: '',
|
||||
syncStageStartedAt: null,
|
||||
throttleFailureCount: 0,
|
||||
throttleRetryAfter: null,
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
|
||||
});
|
||||
|
||||
await messageFolderRepository.update(
|
||||
await this.messageFolderDataAccessService.update(
|
||||
workspaceId,
|
||||
{ messageChannelId: In(messageChannelIds) },
|
||||
{
|
||||
syncCursor: '',
|
||||
@@ -143,15 +135,11 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStageStartedAt: null,
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{ syncStageStartedAt: null },
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -166,17 +154,15 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
|
||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
|
||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -191,17 +177,15 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
|
||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
|
||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -216,20 +200,18 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStatus: MessageChannelSyncStatus.ACTIVE,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
throttleFailureCount: 0,
|
||||
throttleRetryAfter: null,
|
||||
syncStageStartedAt: null,
|
||||
syncedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStatus: MessageChannelSyncStatus.ACTIVE,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
throttleFailureCount: 0,
|
||||
throttleRetryAfter: null,
|
||||
syncStageStartedAt: null,
|
||||
syncedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
await this.metricsService.batchIncrementCounter({
|
||||
@@ -249,15 +231,13 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED,
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -272,17 +252,15 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
|
||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
|
||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -300,17 +278,15 @@ export class MessageChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
await messageChannelRepository.update(messageChannelIds, {
|
||||
syncStage: MessageChannelSyncStage.FAILED,
|
||||
syncStatus: syncStatus,
|
||||
throttleRetryAfter: null,
|
||||
});
|
||||
await this.messageChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
{
|
||||
syncStage: MessageChannelSyncStage.FAILED,
|
||||
syncStatus: syncStatus,
|
||||
throttleRetryAfter: null,
|
||||
},
|
||||
);
|
||||
|
||||
const metricsKey =
|
||||
syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS
|
||||
@@ -325,22 +301,17 @@ export class MessageChannelSyncStatusService {
|
||||
if (
|
||||
syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS
|
||||
) {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
select: ['id', 'connectedAccountId'],
|
||||
where: { id: Any(messageChannelIds) },
|
||||
});
|
||||
const messageChannels = await this.messageChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{ id: In(messageChannelIds) },
|
||||
);
|
||||
|
||||
const connectedAccountIds = messageChannels.map(
|
||||
(messageChannel) => messageChannel.connectedAccountId,
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
await this.connectedAccountDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: Any(connectedAccountIds) },
|
||||
{
|
||||
authFailedAt: new Date(),
|
||||
@@ -363,30 +334,43 @@ export class MessageChannelSyncStatusService {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
const messageChannels = await this.messageChannelDataAccessService.findMany(
|
||||
workspaceId,
|
||||
{
|
||||
where: { id: In(messageChannelIds) },
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
where: { id: Any(messageChannelIds) },
|
||||
relations: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
const userId = messageChannel.connectedAccount.accountOwner.userId;
|
||||
const connectedAccountId = messageChannel.connectedAccount.id;
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: messageChannel.connectedAccountId },
|
||||
});
|
||||
|
||||
if (!connectedAccount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: connectedAccount.accountOwnerId },
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.accountsToReconnectService.addAccountToReconnectByKey(
|
||||
AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
|
||||
userId,
|
||||
workspaceMember.userId,
|
||||
workspaceId,
|
||||
connectedAccountId,
|
||||
connectedAccount.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-47
@@ -1,6 +1,15 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderImportPolicy,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
@@ -9,52 +18,15 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco
|
||||
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
|
||||
export enum MessageChannelSyncStatus {
|
||||
NOT_SYNCED = 'NOT_SYNCED',
|
||||
ONGOING = 'ONGOING',
|
||||
ACTIVE = 'ACTIVE',
|
||||
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
|
||||
}
|
||||
|
||||
export enum MessageChannelSyncStage {
|
||||
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION',
|
||||
MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING',
|
||||
MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED',
|
||||
MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING',
|
||||
MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING',
|
||||
MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED',
|
||||
MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
|
||||
export enum MessageChannelVisibility {
|
||||
METADATA = 'METADATA',
|
||||
SUBJECT = 'SUBJECT',
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
|
||||
}
|
||||
|
||||
export enum MessageChannelType {
|
||||
EMAIL = 'EMAIL',
|
||||
SMS = 'SMS',
|
||||
}
|
||||
|
||||
export enum MessageChannelContactAutoCreationPolicy {
|
||||
SENT_AND_RECEIVED = 'SENT_AND_RECEIVED',
|
||||
SENT = 'SENT',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
|
||||
export enum MessageFolderImportPolicy {
|
||||
ALL_FOLDERS = 'ALL_FOLDERS',
|
||||
SELECTED_FOLDERS = 'SELECTED_FOLDERS',
|
||||
}
|
||||
|
||||
export enum MessageChannelPendingGroupEmailsAction {
|
||||
GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION',
|
||||
GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
export {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderImportPolicy,
|
||||
};
|
||||
|
||||
registerEnumType(MessageChannelVisibility, {
|
||||
name: 'MessageChannelVisibility',
|
||||
|
||||
+5
-5
@@ -1,6 +1,9 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
MessageFolderPendingSyncAction,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
@@ -8,10 +11,7 @@ import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migr
|
||||
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
export enum MessageFolderPendingSyncAction {
|
||||
FOLDER_DELETION = 'FOLDER_DELETION',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
export { MessageFolderPendingSyncAction };
|
||||
|
||||
registerEnumType(MessageFolderPendingSyncAction, {
|
||||
name: 'MessageFolderPendingSyncAction',
|
||||
|
||||
Reference in New Issue
Block a user