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:
@@ -1549,6 +1549,7 @@ enum FeatureFlagKey {
|
||||
IS_DRAFT_EMAIL_ENABLED
|
||||
IS_RICH_TEXT_V1_MIGRATED
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED
|
||||
}
|
||||
|
||||
type SSOIdentityProvider {
|
||||
@@ -2204,6 +2205,163 @@ type AgentTurn {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type CalendarChannel {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
syncStatus: CalendarChannelSyncStatus!
|
||||
syncStage: CalendarChannelSyncStage!
|
||||
visibility: CalendarChannelVisibility!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
|
||||
isSyncEnabled: Boolean!
|
||||
syncCursor: String
|
||||
syncedAt: DateTime
|
||||
syncStageStartedAt: DateTime
|
||||
throttleFailureCount: Float!
|
||||
connectedAccountId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum CalendarChannelSyncStatus {
|
||||
NOT_SYNCED
|
||||
ONGOING
|
||||
ACTIVE
|
||||
FAILED_INSUFFICIENT_PERMISSIONS
|
||||
FAILED_UNKNOWN
|
||||
}
|
||||
|
||||
enum CalendarChannelSyncStage {
|
||||
PENDING_CONFIGURATION
|
||||
CALENDAR_EVENT_LIST_FETCH_PENDING
|
||||
CALENDAR_EVENT_LIST_FETCH_SCHEDULED
|
||||
CALENDAR_EVENT_LIST_FETCH_ONGOING
|
||||
CALENDAR_EVENTS_IMPORT_PENDING
|
||||
CALENDAR_EVENTS_IMPORT_SCHEDULED
|
||||
CALENDAR_EVENTS_IMPORT_ONGOING
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum CalendarChannelVisibility {
|
||||
METADATA
|
||||
SHARE_EVERYTHING
|
||||
}
|
||||
|
||||
enum CalendarChannelContactAutoCreationPolicy {
|
||||
AS_PARTICIPANT_AND_ORGANIZER
|
||||
AS_PARTICIPANT
|
||||
AS_ORGANIZER
|
||||
NONE
|
||||
}
|
||||
|
||||
type ConnectedAccountDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
connectionParameters: JSON
|
||||
lastSignedInAt: DateTime
|
||||
oidcTokenClaims: JSON
|
||||
userWorkspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type MessageChannel {
|
||||
id: UUID!
|
||||
visibility: MessageChannelVisibility!
|
||||
handle: String!
|
||||
type: MessageChannelType!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy!
|
||||
excludeNonProfessionalEmails: Boolean!
|
||||
excludeGroupEmails: Boolean!
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
|
||||
isSyncEnabled: Boolean!
|
||||
syncCursor: String
|
||||
syncedAt: DateTime
|
||||
syncStatus: MessageChannelSyncStatus!
|
||||
syncStage: MessageChannelSyncStage!
|
||||
syncStageStartedAt: DateTime
|
||||
throttleFailureCount: Float!
|
||||
throttleRetryAfter: DateTime
|
||||
connectedAccountId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum MessageChannelVisibility {
|
||||
METADATA
|
||||
SUBJECT
|
||||
SHARE_EVERYTHING
|
||||
}
|
||||
|
||||
enum MessageChannelType {
|
||||
EMAIL
|
||||
SMS
|
||||
}
|
||||
|
||||
enum MessageChannelContactAutoCreationPolicy {
|
||||
SENT_AND_RECEIVED
|
||||
SENT
|
||||
NONE
|
||||
}
|
||||
|
||||
enum MessageFolderImportPolicy {
|
||||
ALL_FOLDERS
|
||||
SELECTED_FOLDERS
|
||||
}
|
||||
|
||||
enum MessageChannelPendingGroupEmailsAction {
|
||||
GROUP_EMAILS_DELETION
|
||||
GROUP_EMAILS_IMPORT
|
||||
NONE
|
||||
}
|
||||
|
||||
enum MessageChannelSyncStatus {
|
||||
NOT_SYNCED
|
||||
ONGOING
|
||||
ACTIVE
|
||||
FAILED_INSUFFICIENT_PERMISSIONS
|
||||
FAILED_UNKNOWN
|
||||
}
|
||||
|
||||
enum MessageChannelSyncStage {
|
||||
PENDING_CONFIGURATION
|
||||
MESSAGE_LIST_FETCH_PENDING
|
||||
MESSAGE_LIST_FETCH_SCHEDULED
|
||||
MESSAGE_LIST_FETCH_ONGOING
|
||||
MESSAGES_IMPORT_PENDING
|
||||
MESSAGES_IMPORT_SCHEDULED
|
||||
MESSAGES_IMPORT_ONGOING
|
||||
FAILED
|
||||
}
|
||||
|
||||
type MessageFolder {
|
||||
id: UUID!
|
||||
name: String
|
||||
syncCursor: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
parentFolderId: UUID
|
||||
externalId: String
|
||||
pendingSyncAction: MessageFolderPendingSyncAction!
|
||||
messageChannelId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum MessageFolderPendingSyncAction {
|
||||
FOLDER_DELETION
|
||||
NONE
|
||||
}
|
||||
|
||||
type CollectionHash {
|
||||
collectionName: AllMetadataName!
|
||||
hash: String!
|
||||
@@ -3010,6 +3168,14 @@ type Query {
|
||||
findWorkspaceFromInviteHash(inviteHash: String!): Workspace!
|
||||
validatePasswordResetToken(passwordResetToken: String!): ValidatePasswordResetToken!
|
||||
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
|
||||
messageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
messageFolder(id: UUID!): MessageFolder
|
||||
calendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
calendarChannel(id: UUID!): CalendarChannel
|
||||
messageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
messageChannel(id: UUID!): MessageChannel
|
||||
connectedAccounts: [ConnectedAccountDTO!]!
|
||||
connectedAccount(id: UUID!): ConnectedAccountDTO
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
minimalMetadata: MinimalMetadata!
|
||||
@@ -3307,6 +3473,18 @@ type Mutation {
|
||||
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
|
||||
createMessageFolder(input: CreateMessageFolderInput!): MessageFolder!
|
||||
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
|
||||
deleteMessageFolder(id: UUID!): MessageFolder!
|
||||
createCalendarChannel(input: CreateCalendarChannelInput!): CalendarChannel!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
deleteCalendarChannel(id: UUID!): CalendarChannel!
|
||||
createMessageChannel(input: CreateMessageChannelInput!): MessageChannel!
|
||||
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
|
||||
deleteMessageChannel(id: UUID!): MessageChannel!
|
||||
createConnectedAccount(input: CreateConnectedAccountInput!): ConnectedAccountDTO!
|
||||
updateConnectedAccount(input: UpdateConnectedAccountInput!): ConnectedAccountDTO!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountDTO!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
@@ -4203,6 +4381,105 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
input CreateMessageFolderInput {
|
||||
id: UUID
|
||||
name: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
externalId: String
|
||||
pendingSyncAction: MessageFolderPendingSyncAction!
|
||||
messageChannelId: UUID!
|
||||
parentFolderId: UUID
|
||||
}
|
||||
|
||||
input UpdateMessageFolderInput {
|
||||
id: UUID!
|
||||
update: UpdateMessageFolderInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateMessageFolderInputUpdates {
|
||||
name: String
|
||||
syncCursor: String
|
||||
isSynced: Boolean
|
||||
pendingSyncAction: MessageFolderPendingSyncAction
|
||||
}
|
||||
|
||||
input CreateCalendarChannelInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
visibility: CalendarChannelVisibility!
|
||||
syncStage: CalendarChannelSyncStage!
|
||||
connectedAccountId: UUID!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
|
||||
isSyncEnabled: Boolean!
|
||||
}
|
||||
|
||||
input UpdateCalendarChannelInput {
|
||||
id: UUID!
|
||||
update: UpdateCalendarChannelInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateCalendarChannelInputUpdates {
|
||||
visibility: CalendarChannelVisibility
|
||||
isContactAutoCreationEnabled: Boolean
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
|
||||
isSyncEnabled: Boolean
|
||||
}
|
||||
|
||||
input CreateMessageChannelInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
visibility: MessageChannelVisibility!
|
||||
type: MessageChannelType!
|
||||
syncStage: MessageChannelSyncStage!
|
||||
connectedAccountId: UUID!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy!
|
||||
excludeNonProfessionalEmails: Boolean!
|
||||
excludeGroupEmails: Boolean!
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
|
||||
isSyncEnabled: Boolean!
|
||||
}
|
||||
|
||||
input UpdateMessageChannelInput {
|
||||
id: UUID!
|
||||
update: UpdateMessageChannelInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateMessageChannelInputUpdates {
|
||||
visibility: MessageChannelVisibility
|
||||
isContactAutoCreationEnabled: Boolean
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy
|
||||
isSyncEnabled: Boolean
|
||||
excludeNonProfessionalEmails: Boolean
|
||||
excludeGroupEmails: Boolean
|
||||
}
|
||||
|
||||
input CreateConnectedAccountInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
provider: String!
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
scopes: [String!]
|
||||
userWorkspaceId: UUID!
|
||||
}
|
||||
|
||||
input UpdateConnectedAccountInput {
|
||||
id: UUID!
|
||||
update: UpdateConnectedAccountInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateConnectedAccountInputUpdates {
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
id: UUID
|
||||
targetUrl: String!
|
||||
|
||||
Reference in New Issue
Block a user