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
@@ -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);