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:
+98
@@ -0,0 +1,98 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateMessageFolderInput } from 'src/engine/metadata-modules/message-folder/dtos/create-message-folder.input';
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
import { UpdateMessageFolderInput } from 'src/engine/metadata-modules/message-folder/dtos/update-message-folder.input';
|
||||
import { MessageFolderGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor';
|
||||
import { MessageFolderMetadataService } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseInterceptors(MessageFolderGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageFolderDTO)
|
||||
export class MessageFolderResolver {
|
||||
constructor(
|
||||
private readonly messageFolderMetadataService: MessageFolderMetadataService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageFolderDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageFolders(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Args('messageChannelId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
})
|
||||
messageChannelId?: string,
|
||||
): Promise<MessageFolderDTO[]> {
|
||||
if (messageChannelId) {
|
||||
return this.messageFolderMetadataService.findByMessageChannelId(
|
||||
messageChannelId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
return this.messageFolderMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => MessageFolderDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageFolder(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO | null> {
|
||||
return this.messageFolderMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createMessageFolder(
|
||||
@Args('input') input: CreateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.create({
|
||||
...input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolder(
|
||||
@Args('input') input: UpdateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteMessageFolder(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.delete(id, workspace.id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user