Files
twenty/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts
T
Charles Bochet cee4cf6452 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
2026-03-20 00:34:58 +01:00

135 lines
3.4 KiB
TypeScript

import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
type Relation,
UpdateDateColumn,
} from 'typeorm';
import {
MessageChannelContactAutoCreationPolicy,
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
MessageChannelVisibility,
MessageFolderImportPolicy,
} from 'twenty-shared/types';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
@Entity({ name: 'messageChannel', schema: 'core' })
export class MessageChannelEntity extends WorkspaceRelatedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: MessageChannelVisibility,
nullable: false,
})
visibility: MessageChannelVisibility;
@Column({ type: 'varchar', nullable: false })
handle: string;
@Column({
type: 'enum',
enum: MessageChannelType,
nullable: false,
})
type: MessageChannelType;
@Column({ type: 'boolean', nullable: false })
isContactAutoCreationEnabled: boolean;
@Column({
type: 'enum',
enum: MessageChannelContactAutoCreationPolicy,
nullable: false,
})
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
@Column({
type: 'enum',
enum: MessageFolderImportPolicy,
nullable: false,
})
messageFolderImportPolicy: MessageFolderImportPolicy;
@Column({ type: 'boolean', nullable: false })
excludeNonProfessionalEmails: boolean;
@Column({ type: 'boolean', nullable: false })
excludeGroupEmails: boolean;
@Column({
type: 'enum',
enum: MessageChannelPendingGroupEmailsAction,
nullable: false,
})
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
@Column({ type: 'boolean', nullable: false })
isSyncEnabled: boolean;
@Column({ type: 'varchar', nullable: true })
syncCursor: string | null;
@Column({ type: 'timestamptz', nullable: true })
syncedAt: Date | null;
@Column({
type: 'enum',
enum: MessageChannelSyncStatus,
nullable: false,
default: MessageChannelSyncStatus.NOT_SYNCED,
})
syncStatus: MessageChannelSyncStatus;
@Column({
type: 'enum',
enum: MessageChannelSyncStage,
nullable: false,
})
syncStage: MessageChannelSyncStage;
@Column({ type: 'timestamptz', nullable: true })
syncStageStartedAt: Date | null;
@Column({ type: 'integer', nullable: false, default: 0 })
throttleFailureCount: number;
@Column({ type: 'timestamptz', nullable: true })
throttleRetryAfter: Date | null;
@Column({ type: 'uuid', nullable: false })
connectedAccountId: string;
@ManyToOne(
() => ConnectedAccountEntity,
(connectedAccount) => connectedAccount.messageChannels,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'connectedAccountId' })
connectedAccount: Relation<ConnectedAccountEntity>;
@OneToMany(
'MessageFolderEntity',
(messageFolder: MessageFolderEntity) => messageFolder.messageChannel,
)
messageFolders: Relation<MessageFolderEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}