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:
+38
-27
@@ -1,19 +1,30 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecordCreateEvent } from 'twenty-shared/database-events';
|
||||
import { MessageParticipantRole } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { And, Any, ILike, In, IsNull, Not, Or } from 'typeorm';
|
||||
import { type ObjectRecordCreateEvent } from 'twenty-shared/database-events';
|
||||
import {
|
||||
And,
|
||||
Any,
|
||||
type FindManyOptions,
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
Not,
|
||||
Or,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
|
||||
|
||||
export type BlocklistItemDeleteMessagesJobData = WorkspaceEventBatch<
|
||||
@@ -28,6 +39,7 @@ export class BlocklistItemDeleteMessagesJob {
|
||||
constructor(
|
||||
private readonly threadCleanerService: MessagingMessageCleanerService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
) {}
|
||||
|
||||
@Process(BlocklistItemDeleteMessagesJob.name)
|
||||
@@ -72,12 +84,6 @@ export class BlocklistItemDeleteMessagesJob {
|
||||
new Map<string, string[]>(),
|
||||
);
|
||||
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
|
||||
workspaceId,
|
||||
@@ -97,30 +103,35 @@ export class BlocklistItemDeleteMessagesJob {
|
||||
MessageParticipantRole.TO,
|
||||
] as const;
|
||||
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
handle: true,
|
||||
connectedAccount: {
|
||||
handleAliases: true,
|
||||
const messageChannels =
|
||||
await this.messageChannelDataAccessService.findMany(workspaceId, {
|
||||
select: {
|
||||
id: true,
|
||||
handle: true,
|
||||
connectedAccount: {
|
||||
handleAliases: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
deletedAt: IsNull(),
|
||||
where: {
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
relations: ['connectedAccount'],
|
||||
} as FindManyOptions<MessageChannelWorkspaceEntity>);
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
const messageChannelHandles = [messageChannel.handle];
|
||||
|
||||
if (isDefined(messageChannel.connectedAccount.handleAliases)) {
|
||||
messageChannelHandles.push(
|
||||
...messageChannel.connectedAccount.handleAliases.split(','),
|
||||
);
|
||||
const handleAliases = messageChannel.connectedAccount?.handleAliases;
|
||||
|
||||
if (isDefined(handleAliases)) {
|
||||
const aliasList: string[] = Array.isArray(handleAliases)
|
||||
? handleAliases
|
||||
: (handleAliases as string).split(',');
|
||||
|
||||
messageChannelHandles.push(...aliasList);
|
||||
}
|
||||
|
||||
const handleConditions = handles.map((handle) => {
|
||||
|
||||
+7
-14
@@ -6,15 +6,13 @@ import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
export type BlocklistReimportMessagesJobData = WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<BlocklistWorkspaceEntity>
|
||||
@@ -27,6 +25,7 @@ export type BlocklistReimportMessagesJobData = WorkspaceEventBatch<
|
||||
export class BlocklistReimportMessagesJob {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService,
|
||||
) {}
|
||||
|
||||
@@ -37,25 +36,19 @@ export class BlocklistReimportMessagesJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
for (const eventPayload of data.events) {
|
||||
const workspaceMemberId =
|
||||
eventPayload.properties.before.workspaceMemberId;
|
||||
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
const messageChannels = await this.messageChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
},
|
||||
syncStage: Not(MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING),
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
|
||||
messageChannels.map((messageChannel) => messageChannel.id),
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { BlocklistItemDeleteMessagesJob } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job';
|
||||
import { BlocklistReimportMessagesJob } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job';
|
||||
import { MessagingBlocklistListener } from 'src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener';
|
||||
@@ -7,7 +8,11 @@ import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-co
|
||||
import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module';
|
||||
|
||||
@Module({
|
||||
imports: [MessagingCommonModule, MessagingMessageCleanerModule],
|
||||
imports: [
|
||||
MessagingCommonModule,
|
||||
MessagingMessageCleanerModule,
|
||||
MessageChannelDataAccessModule,
|
||||
],
|
||||
providers: [
|
||||
MessagingBlocklistListener,
|
||||
BlocklistItemDeleteMessagesJob,
|
||||
|
||||
Reference in New Issue
Block a user