diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 946d57e0536..6285888cfc1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -761,6 +761,56 @@ export type BooleanFieldComparison = { isNot?: InputMaybe; }; +export type CalendarChannel = { + __typename?: 'CalendarChannel'; + connectedAccountId: Scalars['UUID']; + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy; + createdAt: Scalars['DateTime']; + handle: Scalars['String']; + id: Scalars['UUID']; + isContactAutoCreationEnabled: Scalars['Boolean']; + isSyncEnabled: Scalars['Boolean']; + syncCursor?: Maybe; + syncStage: CalendarChannelSyncStage; + syncStageStartedAt?: Maybe; + syncStatus: CalendarChannelSyncStatus; + syncedAt?: Maybe; + throttleFailureCount: Scalars['Float']; + updatedAt: Scalars['DateTime']; + visibility: CalendarChannelVisibility; +}; + +export enum CalendarChannelContactAutoCreationPolicy { + AS_ORGANIZER = 'AS_ORGANIZER', + AS_PARTICIPANT = 'AS_PARTICIPANT', + AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER', + NONE = 'NONE' +} + +export enum CalendarChannelSyncStage { + CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING', + CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING', + CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED', + CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING', + CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING', + CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED', + FAILED = 'FAILED', + PENDING_CONFIGURATION = 'PENDING_CONFIGURATION' +} + +export enum CalendarChannelSyncStatus { + ACTIVE = 'ACTIVE', + FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', + FAILED_UNKNOWN = 'FAILED_UNKNOWN', + NOT_SYNCED = 'NOT_SYNCED', + ONGOING = 'ONGOING' +} + +export enum CalendarChannelVisibility { + METADATA = 'METADATA', + SHARE_EVERYTHING = 'SHARE_EVERYTHING' +} + export type CalendarConfiguration = { __typename?: 'CalendarConfiguration'; configurationType: WidgetConfigurationType; @@ -927,6 +977,25 @@ export type ConfigVariablesGroupData = { variables: Array; }; +export type ConnectedAccountDto = { + __typename?: 'ConnectedAccountDTO'; + accessToken?: Maybe; + authFailedAt?: Maybe; + connectionParameters?: Maybe; + createdAt: Scalars['DateTime']; + handle: Scalars['String']; + handleAliases?: Maybe>; + id: Scalars['UUID']; + lastCredentialsRefreshedAt?: Maybe; + lastSignedInAt?: Maybe; + oidcTokenClaims?: Maybe; + provider: Scalars['String']; + refreshToken?: Maybe; + scopes?: Maybe>; + updatedAt: Scalars['DateTime']; + userWorkspaceId: Scalars['UUID']; +}; + export type ConnectedImapSmtpCaldavAccount = { __typename?: 'ConnectedImapSmtpCaldavAccount'; accountOwnerId: Scalars['UUID']; @@ -1008,6 +1077,17 @@ export type CreateApprovedAccessDomainInput = { email: Scalars['String']; }; +export type CreateCalendarChannelInput = { + connectedAccountId: Scalars['UUID']; + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy; + handle: Scalars['String']; + id?: InputMaybe; + isContactAutoCreationEnabled: Scalars['Boolean']; + isSyncEnabled: Scalars['Boolean']; + syncStage: CalendarChannelSyncStage; + visibility: CalendarChannelVisibility; +}; + export type CreateCommandMenuItemInput = { availabilityObjectMetadataId?: InputMaybe; availabilityType?: InputMaybe; @@ -1023,6 +1103,16 @@ export type CreateCommandMenuItemInput = { workflowVersionId?: InputMaybe; }; +export type CreateConnectedAccountInput = { + accessToken?: InputMaybe; + handle: Scalars['String']; + id?: InputMaybe; + provider: Scalars['String']; + refreshToken?: InputMaybe; + scopes?: InputMaybe>; + userWorkspaceId: Scalars['UUID']; +}; + export type CreateFieldInput = { defaultValue?: InputMaybe; description?: InputMaybe; @@ -1069,6 +1159,33 @@ export type CreateLogicFunctionFromSourceInput = { universalIdentifier?: InputMaybe; }; +export type CreateMessageChannelInput = { + connectedAccountId: Scalars['UUID']; + contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy; + excludeGroupEmails: Scalars['Boolean']; + excludeNonProfessionalEmails: Scalars['Boolean']; + handle: Scalars['String']; + id?: InputMaybe; + isContactAutoCreationEnabled: Scalars['Boolean']; + isSyncEnabled: Scalars['Boolean']; + messageFolderImportPolicy: MessageFolderImportPolicy; + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction; + syncStage: MessageChannelSyncStage; + type: MessageChannelType; + visibility: MessageChannelVisibility; +}; + +export type CreateMessageFolderInput = { + externalId?: InputMaybe; + id?: InputMaybe; + isSentFolder: Scalars['Boolean']; + isSynced: Scalars['Boolean']; + messageChannelId: Scalars['UUID']; + name?: InputMaybe; + parentFolderId?: InputMaybe; + pendingSyncAction: MessageFolderPendingSyncAction; +}; + export type CreateNavigationMenuItemInput = { color?: InputMaybe; folderId?: InputMaybe; @@ -1618,6 +1735,7 @@ export enum FeatureFlagKey { IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', + IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED', IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED', @@ -2289,6 +2407,98 @@ export type MarketplaceAppRoleObjectPermission = { objectUniversalIdentifier: Scalars['String']; }; +export type MessageChannel = { + __typename?: 'MessageChannel'; + connectedAccountId: Scalars['UUID']; + contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy; + createdAt: Scalars['DateTime']; + excludeGroupEmails: Scalars['Boolean']; + excludeNonProfessionalEmails: Scalars['Boolean']; + handle: Scalars['String']; + id: Scalars['UUID']; + isContactAutoCreationEnabled: Scalars['Boolean']; + isSyncEnabled: Scalars['Boolean']; + messageFolderImportPolicy: MessageFolderImportPolicy; + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction; + syncCursor?: Maybe; + syncStage: MessageChannelSyncStage; + syncStageStartedAt?: Maybe; + syncStatus: MessageChannelSyncStatus; + syncedAt?: Maybe; + throttleFailureCount: Scalars['Float']; + throttleRetryAfter?: Maybe; + type: MessageChannelType; + updatedAt: Scalars['DateTime']; + visibility: MessageChannelVisibility; +}; + +export enum MessageChannelContactAutoCreationPolicy { + NONE = 'NONE', + SENT = 'SENT', + SENT_AND_RECEIVED = 'SENT_AND_RECEIVED' +} + +export enum MessageChannelPendingGroupEmailsAction { + GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION', + GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT', + NONE = 'NONE' +} + +export enum MessageChannelSyncStage { + FAILED = 'FAILED', + MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING', + MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING', + MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED', + MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING', + MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING', + MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED', + PENDING_CONFIGURATION = 'PENDING_CONFIGURATION' +} + +export enum MessageChannelSyncStatus { + ACTIVE = 'ACTIVE', + FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', + FAILED_UNKNOWN = 'FAILED_UNKNOWN', + NOT_SYNCED = 'NOT_SYNCED', + ONGOING = 'ONGOING' +} + +export enum MessageChannelType { + EMAIL = 'EMAIL', + SMS = 'SMS' +} + +export enum MessageChannelVisibility { + METADATA = 'METADATA', + SHARE_EVERYTHING = 'SHARE_EVERYTHING', + SUBJECT = 'SUBJECT' +} + +export type MessageFolder = { + __typename?: 'MessageFolder'; + createdAt: Scalars['DateTime']; + externalId?: Maybe; + id: Scalars['UUID']; + isSentFolder: Scalars['Boolean']; + isSynced: Scalars['Boolean']; + messageChannelId: Scalars['UUID']; + name?: Maybe; + parentFolderId?: Maybe; + pendingSyncAction: MessageFolderPendingSyncAction; + syncCursor?: Maybe; + updatedAt: Scalars['DateTime']; +}; + +export enum MessageFolderImportPolicy { + ALL_FOLDERS = 'ALL_FOLDERS', + SELECTED_FOLDERS = 'SELECTED_FOLDERS' +} + +export enum MessageFolderPendingSyncAction { + FOLDER_DELETION = 'FOLDER_DELETION', + NONE = 'NONE' +} + export type MetadataEvent = { __typename?: 'MetadataEvent'; metadataName: Scalars['String']; @@ -2361,8 +2571,10 @@ export type Mutation = { createApplicationRegistration: CreateApplicationRegistration; createApplicationRegistrationVariable: ApplicationRegistrationVariable; createApprovedAccessDomain: ApprovedAccessDomain; + createCalendarChannel: CalendarChannel; createChatThread: AgentChatThread; createCommandMenuItem: CommandMenuItem; + createConnectedAccount: ConnectedAccountDto; createDatabaseConfigVariable: Scalars['Boolean']; createDevelopmentApplication: DevelopmentApplication; createEmailingDomain: EmailingDomain; @@ -2370,6 +2582,8 @@ export type Mutation = { createManyViewFieldGroups: Array; createManyViewFields: Array; createManyViewGroups: Array; + createMessageChannel: MessageChannel; + createMessageFolder: MessageFolder; createNavigationMenuItem: NavigationMenuItem; createOIDCIdentityProvider: SetupSso; createObjectEvent: Analytics; @@ -2397,12 +2611,16 @@ export type Mutation = { deleteApplicationRegistration: Scalars['Boolean']; deleteApplicationRegistrationVariable: Scalars['Boolean']; deleteApprovedAccessDomain: Scalars['Boolean']; + deleteCalendarChannel: CalendarChannel; deleteCommandMenuItem: CommandMenuItem; + deleteConnectedAccount: ConnectedAccountDto; deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; deleteEmailingDomain: Scalars['Boolean']; deleteFrontComponent: FrontComponent; deleteJobs: DeleteJobsResponse; + deleteMessageChannel: MessageChannel; + deleteMessageFolder: MessageFolder; deleteNavigationMenuItem: NavigationMenuItem; deleteOneAgent: Agent; deleteOneField: Field; @@ -2487,10 +2705,14 @@ export type Mutation = { updateApiKey?: Maybe; updateApplicationRegistration: ApplicationRegistration; updateApplicationRegistrationVariable: ApplicationRegistrationVariable; + updateCalendarChannel: CalendarChannel; updateCommandMenuItem: CommandMenuItem; + updateConnectedAccount: ConnectedAccountDto; updateDatabaseConfigVariable: Scalars['Boolean']; updateFrontComponent: FrontComponent; updateLabPublicFeatureFlag: FeatureFlag; + updateMessageChannel: MessageChannel; + updateMessageFolder: MessageFolder; updateNavigationMenuItem: NavigationMenuItem; updateOneAgent: Agent; updateOneApplicationVariable: Scalars['Boolean']; @@ -2608,11 +2830,21 @@ export type MutationCreateApprovedAccessDomainArgs = { }; +export type MutationCreateCalendarChannelArgs = { + input: CreateCalendarChannelInput; +}; + + export type MutationCreateCommandMenuItemArgs = { input: CreateCommandMenuItemInput; }; +export type MutationCreateConnectedAccountArgs = { + input: CreateConnectedAccountInput; +}; + + export type MutationCreateDatabaseConfigVariableArgs = { key: Scalars['String']; value: Scalars['JSON']; @@ -2651,6 +2883,16 @@ export type MutationCreateManyViewGroupsArgs = { }; +export type MutationCreateMessageChannelArgs = { + input: CreateMessageChannelInput; +}; + + +export type MutationCreateMessageFolderArgs = { + input: CreateMessageFolderInput; +}; + + export type MutationCreateNavigationMenuItemArgs = { input: CreateNavigationMenuItemInput; }; @@ -2789,11 +3031,21 @@ export type MutationDeleteApprovedAccessDomainArgs = { }; +export type MutationDeleteCalendarChannelArgs = { + id: Scalars['UUID']; +}; + + export type MutationDeleteCommandMenuItemArgs = { id: Scalars['UUID']; }; +export type MutationDeleteConnectedAccountArgs = { + id: Scalars['UUID']; +}; + + export type MutationDeleteDatabaseConfigVariableArgs = { key: Scalars['String']; }; @@ -2815,6 +3067,16 @@ export type MutationDeleteJobsArgs = { }; +export type MutationDeleteMessageChannelArgs = { + id: Scalars['UUID']; +}; + + +export type MutationDeleteMessageFolderArgs = { + id: Scalars['UUID']; +}; + + export type MutationDeleteNavigationMenuItemArgs = { id: Scalars['UUID']; }; @@ -3217,11 +3479,21 @@ export type MutationUpdateApplicationRegistrationVariableArgs = { }; +export type MutationUpdateCalendarChannelArgs = { + input: UpdateCalendarChannelInput; +}; + + export type MutationUpdateCommandMenuItemArgs = { input: UpdateCommandMenuItemInput; }; +export type MutationUpdateConnectedAccountArgs = { + input: UpdateConnectedAccountInput; +}; + + export type MutationUpdateDatabaseConfigVariableArgs = { key: Scalars['String']; value: Scalars['JSON']; @@ -3238,6 +3510,16 @@ export type MutationUpdateLabPublicFeatureFlagArgs = { }; +export type MutationUpdateMessageChannelArgs = { + input: UpdateMessageChannelInput; +}; + + +export type MutationUpdateMessageFolderArgs = { + input: UpdateMessageFolderInput; +}; + + export type MutationUpdateNavigationMenuItemArgs = { input: UpdateOneNavigationMenuItemInput; }; @@ -3939,6 +4221,8 @@ export type Query = { applicationRegistrationTarballUrl?: Maybe; barChartData: BarChartData; billingPortalSession: BillingSession; + calendarChannel?: Maybe; + calendarChannels: Array; chatMessages: Array; chatThread: AgentChatThread; chatThreads: AgentChatThreadConnection; @@ -3946,6 +4230,8 @@ export type Query = { checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; commandMenuItem?: Maybe; commandMenuItems: Array; + connectedAccount?: Maybe; + connectedAccounts: Array; currentUser: User; currentWorkspace: Workspace; enterpriseCheckoutSession?: Maybe; @@ -4020,6 +4306,10 @@ export type Query = { indexMetadatas: IndexConnection; lineChartData: LineChartData; listPlans: Array; + messageChannel?: Maybe; + messageChannels: Array; + messageFolder?: Maybe; + messageFolders: Array; minimalMetadata: MinimalMetadata; navigationMenuItem?: Maybe; navigationMenuItems: Array; @@ -4061,6 +4351,16 @@ export type QueryBillingPortalSessionArgs = { }; +export type QueryCalendarChannelArgs = { + id: Scalars['UUID']; +}; + + +export type QueryCalendarChannelsArgs = { + connectedAccountId?: InputMaybe; +}; + + export type QueryChatMessagesArgs = { threadId: Scalars['UUID']; }; @@ -4094,6 +4394,11 @@ export type QueryCommandMenuItemArgs = { }; +export type QueryConnectedAccountArgs = { + id: Scalars['UUID']; +}; + + export type QueryEnterpriseCheckoutSessionArgs = { billingInterval?: InputMaybe; }; @@ -4357,6 +4662,26 @@ export type QueryLineChartDataArgs = { }; +export type QueryMessageChannelArgs = { + id: Scalars['UUID']; +}; + + +export type QueryMessageChannelsArgs = { + connectedAccountId?: InputMaybe; +}; + + +export type QueryMessageFolderArgs = { + id: Scalars['UUID']; +}; + + +export type QueryMessageFoldersArgs = { + messageChannelId?: InputMaybe; +}; + + export type QueryNavigationMenuItemArgs = { id: Scalars['UUID']; }; @@ -4871,6 +5196,18 @@ export type UpdateApplicationRegistrationVariablePayload = { value?: InputMaybe; }; +export type UpdateCalendarChannelInput = { + id: Scalars['UUID']; + update: UpdateCalendarChannelInputUpdates; +}; + +export type UpdateCalendarChannelInputUpdates = { + contactAutoCreationPolicy?: InputMaybe; + isContactAutoCreationEnabled?: InputMaybe; + isSyncEnabled?: InputMaybe; + visibility?: InputMaybe; +}; + export type UpdateCommandMenuItemInput = { availabilityObjectMetadataId?: InputMaybe; availabilityType?: InputMaybe; @@ -4884,6 +5221,18 @@ export type UpdateCommandMenuItemInput = { shortLabel?: InputMaybe; }; +export type UpdateConnectedAccountInput = { + id: Scalars['UUID']; + update: UpdateConnectedAccountInputUpdates; +}; + +export type UpdateConnectedAccountInputUpdates = { + accessToken?: InputMaybe; + handleAliases?: InputMaybe>; + refreshToken?: InputMaybe; + scopes?: InputMaybe>; +}; + export type UpdateFieldInput = { defaultValue?: InputMaybe; description?: InputMaybe; @@ -4940,6 +5289,33 @@ export type UpdateLogicFunctionFromSourceInputUpdates = { toolInputSchema?: InputMaybe; }; +export type UpdateMessageChannelInput = { + id: Scalars['UUID']; + update: UpdateMessageChannelInputUpdates; +}; + +export type UpdateMessageChannelInputUpdates = { + contactAutoCreationPolicy?: InputMaybe; + excludeGroupEmails?: InputMaybe; + excludeNonProfessionalEmails?: InputMaybe; + isContactAutoCreationEnabled?: InputMaybe; + isSyncEnabled?: InputMaybe; + messageFolderImportPolicy?: InputMaybe; + visibility?: InputMaybe; +}; + +export type UpdateMessageFolderInput = { + id: Scalars['UUID']; + update: UpdateMessageFolderInputUpdates; +}; + +export type UpdateMessageFolderInputUpdates = { + isSynced?: InputMaybe; + name?: InputMaybe; + pendingSyncAction?: InputMaybe; + syncCursor?: InputMaybe; +}; + export type UpdateNavigationMenuItemInput = { color?: InputMaybe; folderId?: InputMaybe; diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql index e8168ef17ae..b53643be750 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql @@ -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! diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts index e1e4c6b7f30..a4807811117 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts @@ -1252,7 +1252,7 @@ export interface FeatureFlag { __typename: 'FeatureFlag' } -export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' +export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' export interface SSOIdentityProvider { id: Scalars['UUID'] @@ -1877,6 +1877,108 @@ export interface AgentTurn { __typename: 'AgentTurn' } +export interface CalendarChannel { + id: Scalars['UUID'] + handle: Scalars['String'] + syncStatus: CalendarChannelSyncStatus + syncStage: CalendarChannelSyncStage + visibility: CalendarChannelVisibility + isContactAutoCreationEnabled: Scalars['Boolean'] + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy + isSyncEnabled: Scalars['Boolean'] + syncCursor?: Scalars['String'] + syncedAt?: Scalars['DateTime'] + syncStageStartedAt?: Scalars['DateTime'] + throttleFailureCount: Scalars['Float'] + connectedAccountId: Scalars['UUID'] + createdAt: Scalars['DateTime'] + updatedAt: Scalars['DateTime'] + __typename: 'CalendarChannel' +} + +export type CalendarChannelSyncStatus = 'NOT_SYNCED' | 'ONGOING' | 'ACTIVE' | 'FAILED_INSUFFICIENT_PERMISSIONS' | 'FAILED_UNKNOWN' + +export type 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' + +export type CalendarChannelVisibility = 'METADATA' | 'SHARE_EVERYTHING' + +export type CalendarChannelContactAutoCreationPolicy = 'AS_PARTICIPANT_AND_ORGANIZER' | 'AS_PARTICIPANT' | 'AS_ORGANIZER' | 'NONE' + +export interface ConnectedAccountDTO { + id: Scalars['UUID'] + handle: Scalars['String'] + provider: Scalars['String'] + accessToken?: Scalars['String'] + refreshToken?: Scalars['String'] + lastCredentialsRefreshedAt?: Scalars['DateTime'] + authFailedAt?: Scalars['DateTime'] + handleAliases?: Scalars['String'][] + scopes?: Scalars['String'][] + connectionParameters?: Scalars['JSON'] + lastSignedInAt?: Scalars['DateTime'] + oidcTokenClaims?: Scalars['JSON'] + userWorkspaceId: Scalars['UUID'] + createdAt: Scalars['DateTime'] + updatedAt: Scalars['DateTime'] + __typename: 'ConnectedAccountDTO' +} + +export interface MessageChannel { + id: Scalars['UUID'] + visibility: MessageChannelVisibility + handle: Scalars['String'] + type: MessageChannelType + isContactAutoCreationEnabled: Scalars['Boolean'] + contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy + messageFolderImportPolicy: MessageFolderImportPolicy + excludeNonProfessionalEmails: Scalars['Boolean'] + excludeGroupEmails: Scalars['Boolean'] + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction + isSyncEnabled: Scalars['Boolean'] + syncCursor?: Scalars['String'] + syncedAt?: Scalars['DateTime'] + syncStatus: MessageChannelSyncStatus + syncStage: MessageChannelSyncStage + syncStageStartedAt?: Scalars['DateTime'] + throttleFailureCount: Scalars['Float'] + throttleRetryAfter?: Scalars['DateTime'] + connectedAccountId: Scalars['UUID'] + createdAt: Scalars['DateTime'] + updatedAt: Scalars['DateTime'] + __typename: 'MessageChannel' +} + +export type MessageChannelVisibility = 'METADATA' | 'SUBJECT' | 'SHARE_EVERYTHING' + +export type MessageChannelType = 'EMAIL' | 'SMS' + +export type MessageChannelContactAutoCreationPolicy = 'SENT_AND_RECEIVED' | 'SENT' | 'NONE' + +export type MessageFolderImportPolicy = 'ALL_FOLDERS' | 'SELECTED_FOLDERS' + +export type MessageChannelPendingGroupEmailsAction = 'GROUP_EMAILS_DELETION' | 'GROUP_EMAILS_IMPORT' | 'NONE' + +export type MessageChannelSyncStatus = 'NOT_SYNCED' | 'ONGOING' | 'ACTIVE' | 'FAILED_INSUFFICIENT_PERMISSIONS' | 'FAILED_UNKNOWN' + +export type 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' + +export interface MessageFolder { + id: Scalars['UUID'] + name?: Scalars['String'] + syncCursor?: Scalars['String'] + isSentFolder: Scalars['Boolean'] + isSynced: Scalars['Boolean'] + parentFolderId?: Scalars['UUID'] + externalId?: Scalars['String'] + pendingSyncAction: MessageFolderPendingSyncAction + messageChannelId: Scalars['UUID'] + createdAt: Scalars['DateTime'] + updatedAt: Scalars['DateTime'] + __typename: 'MessageFolder' +} + +export type MessageFolderPendingSyncAction = 'FOLDER_DELETION' | 'NONE' + export interface CollectionHash { collectionName: AllMetadataName hash: Scalars['String'] @@ -2622,6 +2724,14 @@ export interface Query { findWorkspaceFromInviteHash: Workspace validatePasswordResetToken: ValidatePasswordResetToken getSSOIdentityProviders: FindAvailableSSOIDP[] + messageFolders: MessageFolder[] + messageFolder?: MessageFolder + calendarChannels: CalendarChannel[] + calendarChannel?: CalendarChannel + messageChannels: MessageChannel[] + messageChannel?: MessageChannel + connectedAccounts: ConnectedAccountDTO[] + connectedAccount?: ConnectedAccountDTO webhooks: Webhook[] webhook?: Webhook minimalMetadata: MinimalMetadata @@ -2818,6 +2928,18 @@ export interface Mutation { createSAMLIdentityProvider: SetupSso deleteSSOIdentityProvider: DeleteSso editSSOIdentityProvider: EditSso + createMessageFolder: MessageFolder + updateMessageFolder: MessageFolder + deleteMessageFolder: MessageFolder + createCalendarChannel: CalendarChannel + updateCalendarChannel: CalendarChannel + deleteCalendarChannel: CalendarChannel + createMessageChannel: MessageChannel + updateMessageChannel: MessageChannel + deleteMessageChannel: MessageChannel + createConnectedAccount: ConnectedAccountDTO + updateConnectedAccount: ConnectedAccountDTO + deleteConnectedAccount: ConnectedAccountDTO createWebhook: Webhook updateWebhook: Webhook deleteWebhook: Webhook @@ -4863,6 +4985,88 @@ export interface AgentTurnGenqlSelection{ __scalar?: boolean | number } +export interface CalendarChannelGenqlSelection{ + id?: boolean | number + handle?: boolean | number + syncStatus?: boolean | number + syncStage?: boolean | number + visibility?: boolean | number + isContactAutoCreationEnabled?: boolean | number + contactAutoCreationPolicy?: boolean | number + isSyncEnabled?: boolean | number + syncCursor?: boolean | number + syncedAt?: boolean | number + syncStageStartedAt?: boolean | number + throttleFailureCount?: boolean | number + connectedAccountId?: boolean | number + createdAt?: boolean | number + updatedAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface ConnectedAccountDTOGenqlSelection{ + id?: boolean | number + handle?: boolean | number + provider?: boolean | number + accessToken?: boolean | number + refreshToken?: boolean | number + lastCredentialsRefreshedAt?: boolean | number + authFailedAt?: boolean | number + handleAliases?: boolean | number + scopes?: boolean | number + connectionParameters?: boolean | number + lastSignedInAt?: boolean | number + oidcTokenClaims?: boolean | number + userWorkspaceId?: boolean | number + createdAt?: boolean | number + updatedAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface MessageChannelGenqlSelection{ + id?: boolean | number + visibility?: boolean | number + handle?: boolean | number + type?: boolean | number + isContactAutoCreationEnabled?: boolean | number + contactAutoCreationPolicy?: boolean | number + messageFolderImportPolicy?: boolean | number + excludeNonProfessionalEmails?: boolean | number + excludeGroupEmails?: boolean | number + pendingGroupEmailsAction?: boolean | number + isSyncEnabled?: boolean | number + syncCursor?: boolean | number + syncedAt?: boolean | number + syncStatus?: boolean | number + syncStage?: boolean | number + syncStageStartedAt?: boolean | number + throttleFailureCount?: boolean | number + throttleRetryAfter?: boolean | number + connectedAccountId?: boolean | number + createdAt?: boolean | number + updatedAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface MessageFolderGenqlSelection{ + id?: boolean | number + name?: boolean | number + syncCursor?: boolean | number + isSentFolder?: boolean | number + isSynced?: boolean | number + parentFolderId?: boolean | number + externalId?: boolean | number + pendingSyncAction?: boolean | number + messageChannelId?: boolean | number + createdAt?: boolean | number + updatedAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + export interface CollectionHashGenqlSelection{ collectionName?: boolean | number hash?: boolean | number @@ -5670,6 +5874,14 @@ export interface QueryGenqlSelection{ findWorkspaceFromInviteHash?: (WorkspaceGenqlSelection & { __args: {inviteHash: Scalars['String']} }) validatePasswordResetToken?: (ValidatePasswordResetTokenGenqlSelection & { __args: {passwordResetToken: Scalars['String']} }) getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection + messageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} }) + messageFolder?: (MessageFolderGenqlSelection & { __args: {id: Scalars['UUID']} }) + calendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} }) + calendarChannel?: (CalendarChannelGenqlSelection & { __args: {id: Scalars['UUID']} }) + messageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} }) + messageChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} }) + connectedAccounts?: ConnectedAccountDTOGenqlSelection + connectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} }) webhooks?: WebhookGenqlSelection webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) minimalMetadata?: MinimalMetadataGenqlSelection @@ -5891,6 +6103,18 @@ export interface MutationGenqlSelection{ createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} }) deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} }) editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} }) + createMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: CreateMessageFolderInput} }) + updateMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFolderInput} }) + deleteMessageFolder?: (MessageFolderGenqlSelection & { __args: {id: Scalars['UUID']} }) + createCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: CreateCalendarChannelInput} }) + updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} }) + deleteCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {id: Scalars['UUID']} }) + createMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: CreateMessageChannelInput} }) + updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} }) + deleteMessageChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} }) + createConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {input: CreateConnectedAccountInput} }) + updateConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {input: UpdateConnectedAccountInput} }) + deleteConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} }) createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} }) updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} }) deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) @@ -6215,6 +6439,30 @@ export interface DeleteSsoInput {identityProviderId: Scalars['UUID']} export interface EditSsoInput {id: Scalars['UUID'],status: SSOIdentityProviderStatus} +export interface CreateMessageFolderInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),isSentFolder: Scalars['Boolean'],isSynced: Scalars['Boolean'],externalId?: (Scalars['String'] | null),pendingSyncAction: MessageFolderPendingSyncAction,messageChannelId: Scalars['UUID'],parentFolderId?: (Scalars['UUID'] | null)} + +export interface UpdateMessageFolderInput {id: Scalars['UUID'],update: UpdateMessageFolderInputUpdates} + +export interface UpdateMessageFolderInputUpdates {name?: (Scalars['String'] | null),syncCursor?: (Scalars['String'] | null),isSynced?: (Scalars['Boolean'] | null),pendingSyncAction?: (MessageFolderPendingSyncAction | null)} + +export interface CreateCalendarChannelInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],visibility: CalendarChannelVisibility,syncStage: CalendarChannelSyncStage,connectedAccountId: Scalars['UUID'],isContactAutoCreationEnabled: Scalars['Boolean'],contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy,isSyncEnabled: Scalars['Boolean']} + +export interface UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateCalendarChannelInputUpdates} + +export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)} + +export interface CreateMessageChannelInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],visibility: MessageChannelVisibility,type: MessageChannelType,syncStage: MessageChannelSyncStage,connectedAccountId: Scalars['UUID'],isContactAutoCreationEnabled: Scalars['Boolean'],contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy,messageFolderImportPolicy: MessageFolderImportPolicy,excludeNonProfessionalEmails: Scalars['Boolean'],excludeGroupEmails: Scalars['Boolean'],pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction,isSyncEnabled: Scalars['Boolean']} + +export interface UpdateMessageChannelInput {id: Scalars['UUID'],update: UpdateMessageChannelInputUpdates} + +export interface UpdateMessageChannelInputUpdates {visibility?: (MessageChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (MessageChannelContactAutoCreationPolicy | null),messageFolderImportPolicy?: (MessageFolderImportPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null),excludeNonProfessionalEmails?: (Scalars['Boolean'] | null),excludeGroupEmails?: (Scalars['Boolean'] | null)} + +export interface CreateConnectedAccountInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],provider: Scalars['String'],accessToken?: (Scalars['String'] | null),refreshToken?: (Scalars['String'] | null),scopes?: (Scalars['String'][] | null),userWorkspaceId: Scalars['UUID']} + +export interface UpdateConnectedAccountInput {id: Scalars['UUID'],update: UpdateConnectedAccountInputUpdates} + +export interface UpdateConnectedAccountInputUpdates {accessToken?: (Scalars['String'] | null),refreshToken?: (Scalars['String'] | null),handleAliases?: (Scalars['String'][] | null),scopes?: (Scalars['String'][] | null)} + export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} export interface UpdateWebhookInput { @@ -7647,6 +7895,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null + const CalendarChannel_possibleTypes: string[] = ['CalendarChannel'] + export const isCalendarChannel = (obj?: { __typename?: any } | null): obj is CalendarChannel => { + if (!obj?.__typename) throw new Error('__typename is missing in "isCalendarChannel"') + return CalendarChannel_possibleTypes.includes(obj.__typename) + } + + + + const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO'] + export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => { + if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"') + return ConnectedAccountDTO_possibleTypes.includes(obj.__typename) + } + + + + const MessageChannel_possibleTypes: string[] = ['MessageChannel'] + export const isMessageChannel = (obj?: { __typename?: any } | null): obj is MessageChannel => { + if (!obj?.__typename) throw new Error('__typename is missing in "isMessageChannel"') + return MessageChannel_possibleTypes.includes(obj.__typename) + } + + + + const MessageFolder_possibleTypes: string[] = ['MessageFolder'] + export const isMessageFolder = (obj?: { __typename?: any } | null): obj is MessageFolder => { + if (!obj?.__typename) throw new Error('__typename is missing in "isMessageFolder"') + return MessageFolder_possibleTypes.includes(obj.__typename) + } + + + const CollectionHash_possibleTypes: string[] = ['CollectionHash'] export const isCollectionHash = (obj?: { __typename?: any } | null): obj is CollectionHash => { if (!obj?.__typename) throw new Error('__typename is missing in "isCollectionHash"') @@ -8630,7 +8910,8 @@ export const enumFeatureFlagKey = { IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED: 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' as const, IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const, IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const, - IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const + IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const, + IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const } export const enumRelationType = { @@ -8733,6 +9014,89 @@ export const enumCommandMenuItemAvailabilityType = { FALLBACK: 'FALLBACK' as const } +export const enumCalendarChannelSyncStatus = { + NOT_SYNCED: 'NOT_SYNCED' as const, + ONGOING: 'ONGOING' as const, + ACTIVE: 'ACTIVE' as const, + FAILED_INSUFFICIENT_PERMISSIONS: 'FAILED_INSUFFICIENT_PERMISSIONS' as const, + FAILED_UNKNOWN: 'FAILED_UNKNOWN' as const +} + +export const enumCalendarChannelSyncStage = { + PENDING_CONFIGURATION: 'PENDING_CONFIGURATION' as const, + CALENDAR_EVENT_LIST_FETCH_PENDING: 'CALENDAR_EVENT_LIST_FETCH_PENDING' as const, + CALENDAR_EVENT_LIST_FETCH_SCHEDULED: 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED' as const, + CALENDAR_EVENT_LIST_FETCH_ONGOING: 'CALENDAR_EVENT_LIST_FETCH_ONGOING' as const, + CALENDAR_EVENTS_IMPORT_PENDING: 'CALENDAR_EVENTS_IMPORT_PENDING' as const, + CALENDAR_EVENTS_IMPORT_SCHEDULED: 'CALENDAR_EVENTS_IMPORT_SCHEDULED' as const, + CALENDAR_EVENTS_IMPORT_ONGOING: 'CALENDAR_EVENTS_IMPORT_ONGOING' as const, + FAILED: 'FAILED' as const +} + +export const enumCalendarChannelVisibility = { + METADATA: 'METADATA' as const, + SHARE_EVERYTHING: 'SHARE_EVERYTHING' as const +} + +export const enumCalendarChannelContactAutoCreationPolicy = { + AS_PARTICIPANT_AND_ORGANIZER: 'AS_PARTICIPANT_AND_ORGANIZER' as const, + AS_PARTICIPANT: 'AS_PARTICIPANT' as const, + AS_ORGANIZER: 'AS_ORGANIZER' as const, + NONE: 'NONE' as const +} + +export const enumMessageChannelVisibility = { + METADATA: 'METADATA' as const, + SUBJECT: 'SUBJECT' as const, + SHARE_EVERYTHING: 'SHARE_EVERYTHING' as const +} + +export const enumMessageChannelType = { + EMAIL: 'EMAIL' as const, + SMS: 'SMS' as const +} + +export const enumMessageChannelContactAutoCreationPolicy = { + SENT_AND_RECEIVED: 'SENT_AND_RECEIVED' as const, + SENT: 'SENT' as const, + NONE: 'NONE' as const +} + +export const enumMessageFolderImportPolicy = { + ALL_FOLDERS: 'ALL_FOLDERS' as const, + SELECTED_FOLDERS: 'SELECTED_FOLDERS' as const +} + +export const enumMessageChannelPendingGroupEmailsAction = { + GROUP_EMAILS_DELETION: 'GROUP_EMAILS_DELETION' as const, + GROUP_EMAILS_IMPORT: 'GROUP_EMAILS_IMPORT' as const, + NONE: 'NONE' as const +} + +export const enumMessageChannelSyncStatus = { + NOT_SYNCED: 'NOT_SYNCED' as const, + ONGOING: 'ONGOING' as const, + ACTIVE: 'ACTIVE' as const, + FAILED_INSUFFICIENT_PERMISSIONS: 'FAILED_INSUFFICIENT_PERMISSIONS' as const, + FAILED_UNKNOWN: 'FAILED_UNKNOWN' as const +} + +export const enumMessageChannelSyncStage = { + PENDING_CONFIGURATION: 'PENDING_CONFIGURATION' as const, + MESSAGE_LIST_FETCH_PENDING: 'MESSAGE_LIST_FETCH_PENDING' as const, + MESSAGE_LIST_FETCH_SCHEDULED: 'MESSAGE_LIST_FETCH_SCHEDULED' as const, + MESSAGE_LIST_FETCH_ONGOING: 'MESSAGE_LIST_FETCH_ONGOING' as const, + MESSAGES_IMPORT_PENDING: 'MESSAGES_IMPORT_PENDING' as const, + MESSAGES_IMPORT_SCHEDULED: 'MESSAGES_IMPORT_SCHEDULED' as const, + MESSAGES_IMPORT_ONGOING: 'MESSAGES_IMPORT_ONGOING' as const, + FAILED: 'FAILED' as const +} + +export const enumMessageFolderPendingSyncAction = { + FOLDER_DELETION: 'FOLDER_DELETION' as const, + NONE: 'NONE' as const +} + export const enumAllMetadataName = { fieldMetadata: 'fieldMetadata' as const, objectMetadata: 'objectMetadata' as const, diff --git a/packages/twenty-sdk/src/clients/generated/metadata/types.ts b/packages/twenty-sdk/src/clients/generated/metadata/types.ts index 3924df046a1..1ff22db52aa 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/types.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/types.ts @@ -55,28 +55,40 @@ export default { 225, 226, 236, + 237, + 238, + 239, + 242, + 243, 244, 245, + 246, + 247, + 248, 250, - 253, - 259, + 252, 260, - 262, - 267, - 272, - 273, - 285, + 261, + 266, + 269, + 275, + 276, + 278, + 283, + 288, + 289, 301, - 302, - 325, - 332, - 333, - 334, - 336, + 317, + 318, + 341, 348, 349, - 455, - 456 + 350, + 352, + 364, + 365, + 483, + 484 ], "types": { "BillingProductDTO": { @@ -4309,9 +4321,227 @@ export default { 1 ] }, + "CalendarChannel": { + "id": [ + 3 + ], + "handle": [ + 1 + ], + "syncStatus": [ + 236 + ], + "syncStage": [ + 237 + ], + "visibility": [ + 238 + ], + "isContactAutoCreationEnabled": [ + 6 + ], + "contactAutoCreationPolicy": [ + 239 + ], + "isSyncEnabled": [ + 6 + ], + "syncCursor": [ + 1 + ], + "syncedAt": [ + 4 + ], + "syncStageStartedAt": [ + 4 + ], + "throttleFailureCount": [ + 11 + ], + "connectedAccountId": [ + 3 + ], + "createdAt": [ + 4 + ], + "updatedAt": [ + 4 + ], + "__typename": [ + 1 + ] + }, + "CalendarChannelSyncStatus": {}, + "CalendarChannelSyncStage": {}, + "CalendarChannelVisibility": {}, + "CalendarChannelContactAutoCreationPolicy": {}, + "ConnectedAccountDTO": { + "id": [ + 3 + ], + "handle": [ + 1 + ], + "provider": [ + 1 + ], + "accessToken": [ + 1 + ], + "refreshToken": [ + 1 + ], + "lastCredentialsRefreshedAt": [ + 4 + ], + "authFailedAt": [ + 4 + ], + "handleAliases": [ + 1 + ], + "scopes": [ + 1 + ], + "connectionParameters": [ + 15 + ], + "lastSignedInAt": [ + 4 + ], + "oidcTokenClaims": [ + 15 + ], + "userWorkspaceId": [ + 3 + ], + "createdAt": [ + 4 + ], + "updatedAt": [ + 4 + ], + "__typename": [ + 1 + ] + }, + "MessageChannel": { + "id": [ + 3 + ], + "visibility": [ + 242 + ], + "handle": [ + 1 + ], + "type": [ + 243 + ], + "isContactAutoCreationEnabled": [ + 6 + ], + "contactAutoCreationPolicy": [ + 244 + ], + "messageFolderImportPolicy": [ + 245 + ], + "excludeNonProfessionalEmails": [ + 6 + ], + "excludeGroupEmails": [ + 6 + ], + "pendingGroupEmailsAction": [ + 246 + ], + "isSyncEnabled": [ + 6 + ], + "syncCursor": [ + 1 + ], + "syncedAt": [ + 4 + ], + "syncStatus": [ + 247 + ], + "syncStage": [ + 248 + ], + "syncStageStartedAt": [ + 4 + ], + "throttleFailureCount": [ + 11 + ], + "throttleRetryAfter": [ + 4 + ], + "connectedAccountId": [ + 3 + ], + "createdAt": [ + 4 + ], + "updatedAt": [ + 4 + ], + "__typename": [ + 1 + ] + }, + "MessageChannelVisibility": {}, + "MessageChannelType": {}, + "MessageChannelContactAutoCreationPolicy": {}, + "MessageFolderImportPolicy": {}, + "MessageChannelPendingGroupEmailsAction": {}, + "MessageChannelSyncStatus": {}, + "MessageChannelSyncStage": {}, + "MessageFolder": { + "id": [ + 3 + ], + "name": [ + 1 + ], + "syncCursor": [ + 1 + ], + "isSentFolder": [ + 6 + ], + "isSynced": [ + 6 + ], + "parentFolderId": [ + 3 + ], + "externalId": [ + 1 + ], + "pendingSyncAction": [ + 250 + ], + "messageChannelId": [ + 3 + ], + "createdAt": [ + 4 + ], + "updatedAt": [ + 4 + ], + "__typename": [ + 1 + ] + }, + "MessageFolderPendingSyncAction": {}, "CollectionHash": { "collectionName": [ - 236 + 252 ], "hash": [ 1 @@ -4378,13 +4608,13 @@ export default { }, "MinimalMetadata": { "objectMetadataItems": [ - 237 + 253 ], "views": [ - 238 + 254 ], "collectionHashes": [ - 235 + 251 ], "__typename": [ 1 @@ -4452,10 +4682,10 @@ export default { 1 ], "modelFamily": [ - 244 + 260 ], "inferenceProvider": [ - 245 + 261 ], "inputCostPerMillionTokensInCredits": [ 11 @@ -4464,7 +4694,7 @@ export default { 11 ], "nativeCapabilities": [ - 242 + 258 ], "deprecated": [ 6 @@ -4486,10 +4716,10 @@ export default { 1 ], "modelFamily": [ - 244 + 260 ], "inferenceProvider": [ - 245 + 261 ], "isAvailable": [ 6 @@ -4512,7 +4742,7 @@ export default { 6 ], "models": [ - 246 + 262 ], "__typename": [ 1 @@ -4526,7 +4756,7 @@ export default { 1 ], "trialPeriods": [ - 241 + 257 ], "__typename": [ 1 @@ -4534,7 +4764,7 @@ export default { }, "Support": { "supportDriver": [ - 250 + 266 ], "supportFrontChatId": [ 1 @@ -4560,7 +4790,7 @@ export default { }, "Captcha": { "provider": [ - 253 + 269 ], "siteKey": [ 1 @@ -4597,7 +4827,7 @@ export default { 155 ], "metadata": [ - 255 + 271 ], "__typename": [ 1 @@ -4611,10 +4841,10 @@ export default { 157 ], "billing": [ - 248 + 264 ], "aiModels": [ - 243 + 259 ], "signInPrefilled": [ 6 @@ -4635,25 +4865,25 @@ export default { 6 ], "support": [ - 249 + 265 ], "isAttachmentPreviewEnabled": [ 6 ], "sentry": [ - 251 + 267 ], "captcha": [ - 252 + 268 ], "api": [ - 254 + 270 ], "canManageFeatureFlags": [ 6 ], "publicFeatureFlags": [ - 256 + 272 ], "isMicrosoftMessagingEnabled": [ 6 @@ -4703,13 +4933,13 @@ export default { 6 ], "source": [ - 259 + 275 ], "isEnvOnly": [ 6 ], "type": [ - 260 + 276 ], "options": [ 15 @@ -4722,10 +4952,10 @@ export default { "ConfigVariableType": {}, "ConfigVariablesGroupData": { "variables": [ - 258 + 274 ], "name": [ - 262 + 278 ], "description": [ 1 @@ -4740,7 +4970,7 @@ export default { "ConfigVariablesGroup": {}, "ConfigVariables": { "groups": [ - 261 + 277 ], "__typename": [ 1 @@ -4765,7 +4995,7 @@ export default { 21 ], "results": [ - 264 + 280 ], "__typename": [ 1 @@ -4782,7 +5012,7 @@ export default { 15 ], "state": [ - 267 + 283 ], "timestamp": [ 11 @@ -4832,7 +5062,7 @@ export default { }, "QueueJobsResponse": { "jobs": [ - 266 + 282 ], "count": [ 11 @@ -4844,7 +5074,7 @@ export default { 6 ], "retentionConfig": [ - 268 + 284 ], "__typename": [ 1 @@ -4855,7 +5085,7 @@ export default { 21 ], "results": [ - 264 + 280 ], "__typename": [ 1 @@ -4863,13 +5093,13 @@ export default { }, "SystemHealthService": { "id": [ - 272 + 288 ], "label": [ 1 ], "status": [ - 273 + 289 ], "__typename": [ 1 @@ -4879,7 +5109,7 @@ export default { "AdminPanelHealthServiceStatus": {}, "SystemHealth": { "services": [ - 271 + 287 ], "__typename": [ 1 @@ -4922,7 +5152,7 @@ export default { 143 ], "users": [ - 275 + 291 ], "featureFlags": [ 154 @@ -4933,10 +5163,10 @@ export default { }, "UserLookup": { "user": [ - 275 + 291 ], "workspaces": [ - 276 + 292 ], "__typename": [ 1 @@ -4961,7 +5191,7 @@ export default { 1 ], "status": [ - 273 + 289 ], "__typename": [ 1 @@ -4969,7 +5199,7 @@ export default { }, "AdminPanelHealthServiceData": { "id": [ - 272 + 288 ], "label": [ 1 @@ -4978,7 +5208,7 @@ export default { 1 ], "status": [ - 273 + 289 ], "errorMessage": [ 1 @@ -4987,7 +5217,7 @@ export default { 1 ], "queues": [ - 279 + 295 ], "__typename": [ 1 @@ -5009,7 +5239,7 @@ export default { 1 ], "data": [ - 281 + 297 ], "__typename": [ 1 @@ -5052,13 +5282,13 @@ export default { 11 ], "timeRange": [ - 285 + 301 ], "details": [ - 283 + 299 ], "data": [ - 282 + 298 ], "__typename": [ 1 @@ -5164,7 +5394,7 @@ export default { 1 ], "fields": [ - 290 + 306 ], "__typename": [ 1 @@ -5261,10 +5491,10 @@ export default { 6 ], "objectPermissions": [ - 294 + 310 ], "fieldPermissions": [ - 295 + 311 ], "permissionFlags": [ 1 @@ -5314,19 +5544,19 @@ export default { 1 ], "objects": [ - 291 + 307 ], "fields": [ - 290 + 306 ], "logicFunctions": [ - 292 + 308 ], "frontComponents": [ - 293 + 309 ], "defaultRole": [ - 296 + 312 ], "sourcePackage": [ 1 @@ -5386,13 +5616,13 @@ export default { 1 ], "driver": [ - 301 + 317 ], "status": [ - 302 + 318 ], "verificationRecords": [ - 299 + 315 ], "verifiedAt": [ 4 @@ -5439,7 +5669,7 @@ export default { 1 ], "location": [ - 304 + 320 ], "__typename": [ 1 @@ -5467,13 +5697,13 @@ export default { }, "ImapSmtpCaldavConnectionParameters": { "IMAP": [ - 306 + 322 ], "SMTP": [ - 306 + 322 ], "CALDAV": [ - 306 + 322 ], "__typename": [ 1 @@ -5493,7 +5723,7 @@ export default { 3 ], "connectionParameters": [ - 307 + 323 ], "__typename": [ 1 @@ -5557,7 +5787,7 @@ export default { 1 ], "series": [ - 312 + 328 ], "xAxisLabel": [ 1 @@ -5606,7 +5836,7 @@ export default { 1 ], "data": [ - 314 + 330 ], "__typename": [ 1 @@ -5614,7 +5844,7 @@ export default { }, "LineChartData": { "series": [ - 315 + 331 ], "xAxisLabel": [ 1 @@ -5651,7 +5881,7 @@ export default { }, "PieChartData": { "data": [ - 317 + 333 ], "showLegend": [ 6 @@ -5734,13 +5964,13 @@ export default { }, "EventLogQueryResult": { "records": [ - 320 + 336 ], "totalCount": [ 21 ], "pageInfo": [ - 321 + 337 ], "__typename": [ 1 @@ -5819,7 +6049,7 @@ export default { 32, { "input": [ - 324, + 340, "LogicFunctionIdInput!" ] } @@ -5831,7 +6061,7 @@ export default { 15, { "input": [ - 324, + 340, "LogicFunctionIdInput!" ] } @@ -5840,7 +6070,7 @@ export default { 1, { "input": [ - 324, + 340, "LogicFunctionIdInput!" ] } @@ -5997,7 +6227,7 @@ export default { 25, { "input": [ - 326, + 342, "AgentIdInput!" ] } @@ -6051,7 +6281,7 @@ export default { 2, { "input": [ - 327, + 343, "GetApiKeyInput!" ] } @@ -6254,10 +6484,61 @@ export default { "getSSOIdentityProviders": [ 184 ], - "webhooks": [ + "messageFolders": [ + 249, + { + "messageChannelId": [ + 3 + ] + } + ], + "messageFolder": [ + 249, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "calendarChannels": [ + 235, + { + "connectedAccountId": [ + 3 + ] + } + ], + "calendarChannel": [ + 235, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "messageChannels": [ + 241, + { + "connectedAccountId": [ + 3 + ] + } + ], + "messageChannel": [ + 241, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "connectedAccounts": [ 240 ], - "webhook": [ + "connectedAccount": [ 240, { "id": [ @@ -6266,8 +6547,20 @@ export default { ] } ], + "webhooks": [ + 256 + ], + "webhook": [ + 256, + { + "id": [ + 3, + "UUID!" + ] + } + ], "minimalMetadata": [ - 239 + 255 ], "chatThread": [ 227, @@ -6310,11 +6603,11 @@ export default { "CursorPaging!" ], "filter": [ - 328, + 344, "AgentChatThreadFilter!" ], "sorting": [ - 331, + 347, "[AgentChatThreadSort!]!" ] } @@ -6329,43 +6622,43 @@ export default { } ], "eventLogs": [ - 322, + 338, { "input": [ - 335, + 351, "EventLogQueryInput!" ] } ], "pieChartData": [ - 318, + 334, { "input": [ - 339, + 355, "PieChartDataInput!" ] } ], "lineChartData": [ - 316, + 332, { "input": [ - 340, + 356, "LineChartDataInput!" ] } ], "barChartData": [ - 313, + 329, { "input": [ - 341, + 357, "BarChartDataInput!" ] } ], "getConnectedImapSmtpCaldavAccount": [ - 308, + 324, { "id": [ 3, @@ -6374,7 +6667,7 @@ export default { } ], "getAutoCompleteAddress": [ - 303, + 319, { "address": [ 1, @@ -6393,7 +6686,7 @@ export default { } ], "getAddressDetails": [ - 305, + 321, { "placeId": [ 1, @@ -6406,40 +6699,40 @@ export default { } ], "getConfigVariablesGrouped": [ - 263 + 279 ], "getSystemHealthStatus": [ - 274 + 290 ], "getIndicatorHealthStatus": [ - 280, + 296, { "indicatorId": [ - 272, + 288, "HealthIndicatorId!" ] } ], "getQueueMetrics": [ - 284, + 300, { "queueName": [ 1, "String!" ], "timeRange": [ - 285 + 301 ] } ], "versionInfo": [ - 278 + 294 ], "getAdminAiModels": [ - 247 + 263 ], "getDatabaseConfigVariable": [ - 258, + 274, { "key": [ 1, @@ -6448,14 +6741,14 @@ export default { } ], "getQueueJobs": [ - 269, + 285, { "queueName": [ 1, "String!" ], "state": [ - 267, + 283, "JobState!" ], "limit": [ @@ -6470,19 +6763,19 @@ export default { 7 ], "getPostgresCredentials": [ - 310 + 326 ], "findManyPublicDomains": [ - 298 + 314 ], "getEmailingDomains": [ - 300 + 316 ], "findManyMarketplaceApps": [ - 297 + 313 ], "findOneMarketplaceApp": [ - 297, + 313, { "universalIdentifier": [ 1, @@ -6510,7 +6803,7 @@ export default { }, "LogicFunctionIdInput": { "id": [ - 325 + 341 ], "__typename": [ 1 @@ -6535,16 +6828,16 @@ export default { }, "AgentChatThreadFilter": { "and": [ - 328 + 344 ], "or": [ - 328 + 344 ], "id": [ 42 ], "updatedAt": [ - 329 + 345 ], "__typename": [ 1 @@ -6582,10 +6875,10 @@ export default { 4 ], "between": [ - 330 + 346 ], "notBetween": [ - 330 + 346 ], "__typename": [ 1 @@ -6604,13 +6897,13 @@ export default { }, "AgentChatThreadSort": { "field": [ - 332 + 348 ], "direction": [ - 333 + 349 ], "nulls": [ - 334 + 350 ], "__typename": [ 1 @@ -6621,10 +6914,10 @@ export default { "SortNulls": {}, "EventLogQueryInput": { "table": [ - 336 + 352 ], "filters": [ - 337 + 353 ], "first": [ 21 @@ -6645,7 +6938,7 @@ export default { 1 ], "dateRange": [ - 338 + 354 ], "recordId": [ 1 @@ -6706,7 +6999,7 @@ export default { 6, { "input": [ - 343, + 359, "AddQuerySubscriptionInput!" ] } @@ -6715,7 +7008,7 @@ export default { 6, { "input": [ - 344, + 360, "RemoveQueryFromEventStreamInput!" ] } @@ -6724,7 +7017,7 @@ export default { 207, { "input": [ - 345, + 361, "CreateNavigationMenuItemInput!" ] } @@ -6733,7 +7026,7 @@ export default { 207, { "input": [ - 346, + 362, "UpdateOneNavigationMenuItemInput!" ] } @@ -6751,7 +7044,7 @@ export default { 139, { "file": [ - 348, + 364, "Upload!" ] } @@ -6760,7 +7053,7 @@ export default { 139, { "file": [ - 348, + 364, "Upload!" ] } @@ -6769,7 +7062,7 @@ export default { 139, { "file": [ - 348, + 364, "Upload!" ] } @@ -6778,7 +7071,7 @@ export default { 139, { "file": [ - 348, + 364, "Upload!" ] } @@ -6787,7 +7080,7 @@ export default { 139, { "file": [ - 348, + 364, "Upload!" ], "fieldMetadataId": [ @@ -6800,7 +7093,7 @@ export default { 139, { "file": [ - 348, + 364, "Upload!" ], "fieldMetadataUniversalIdentifier": [ @@ -6833,7 +7126,7 @@ export default { 113, { "type": [ - 349, + 365, "AnalyticsType!" ], "name": [ @@ -6851,7 +7144,7 @@ export default { 75, { "input": [ - 350, + 366, "CreatePageLayoutWidgetInput!" ] } @@ -6864,7 +7157,7 @@ export default { "String!" ], "input": [ - 352, + 368, "UpdatePageLayoutWidgetInput!" ] } @@ -6882,7 +7175,7 @@ export default { 110, { "input": [ - 353, + 369, "CreatePageLayoutTabInput!" ] } @@ -6895,7 +7188,7 @@ export default { "String!" ], "input": [ - 354, + 370, "UpdatePageLayoutTabInput!" ] } @@ -6913,7 +7206,7 @@ export default { 111, { "input": [ - 355, + 371, "CreatePageLayoutInput!" ] } @@ -6926,7 +7219,7 @@ export default { "String!" ], "input": [ - 356, + 372, "UpdatePageLayoutInput!" ] } @@ -6948,7 +7241,7 @@ export default { "String!" ], "input": [ - 357, + 373, "UpdatePageLayoutWithTabsInput!" ] } @@ -6957,7 +7250,7 @@ export default { 32, { "input": [ - 324, + 340, "LogicFunctionIdInput!" ] } @@ -6966,7 +7259,7 @@ export default { 32, { "input": [ - 360, + 376, "CreateLogicFunctionFromSourceInput!" ] } @@ -6975,7 +7268,7 @@ export default { 216, { "input": [ - 361, + 377, "ExecuteOneLogicFunctionInput!" ] } @@ -6984,7 +7277,7 @@ export default { 6, { "input": [ - 362, + 378, "UpdateLogicFunctionFromSourceInput!" ] } @@ -6993,7 +7286,7 @@ export default { 46, { "input": [ - 364, + 380, "CreateOneObjectInput!" ] } @@ -7002,7 +7295,7 @@ export default { 46, { "input": [ - 366, + 382, "DeleteOneObjectInput!" ] } @@ -7011,7 +7304,7 @@ export default { 46, { "input": [ - 367, + 383, "UpdateOneObjectInput!" ] } @@ -7020,7 +7313,7 @@ export default { 50, { "input": [ - 369, + 385, "UpdateViewFieldInput!" ] } @@ -7029,7 +7322,7 @@ export default { 50, { "input": [ - 371, + 387, "CreateViewFieldInput!" ] } @@ -7038,7 +7331,7 @@ export default { 50, { "inputs": [ - 371, + 387, "[CreateViewFieldInput!]!" ] } @@ -7047,7 +7340,7 @@ export default { 50, { "input": [ - 372, + 388, "DeleteViewFieldInput!" ] } @@ -7056,7 +7349,7 @@ export default { 50, { "input": [ - 373, + 389, "DestroyViewFieldInput!" ] } @@ -7065,7 +7358,7 @@ export default { 60, { "input": [ - 374, + 390, "CreateViewInput!" ] } @@ -7078,7 +7371,7 @@ export default { "String!" ], "input": [ - 375, + 391, "UpdateViewInput!" ] } @@ -7105,7 +7398,7 @@ export default { 57, { "input": [ - 376, + 392, "CreateViewSortInput!" ] } @@ -7114,7 +7407,7 @@ export default { 57, { "input": [ - 377, + 393, "UpdateViewSortInput!" ] } @@ -7123,7 +7416,7 @@ export default { 6, { "input": [ - 379, + 395, "DeleteViewSortInput!" ] } @@ -7132,7 +7425,7 @@ export default { 6, { "input": [ - 380, + 396, "DestroyViewSortInput!" ] } @@ -7141,7 +7434,7 @@ export default { 59, { "input": [ - 381, + 397, "UpdateViewFieldGroupInput!" ] } @@ -7150,7 +7443,7 @@ export default { 59, { "input": [ - 383, + 399, "CreateViewFieldGroupInput!" ] } @@ -7159,7 +7452,7 @@ export default { 59, { "inputs": [ - 383, + 399, "[CreateViewFieldGroupInput!]!" ] } @@ -7168,7 +7461,7 @@ export default { 59, { "input": [ - 384, + 400, "DeleteViewFieldGroupInput!" ] } @@ -7177,7 +7470,7 @@ export default { 59, { "input": [ - 385, + 401, "DestroyViewFieldGroupInput!" ] } @@ -7186,7 +7479,7 @@ export default { 60, { "input": [ - 386, + 402, "UpsertFieldsWidgetInput!" ] } @@ -7195,7 +7488,7 @@ export default { 224, { "input": [ - 389, + 405, "CreateCommandMenuItemInput!" ] } @@ -7204,7 +7497,7 @@ export default { 224, { "input": [ - 390, + 406, "UpdateCommandMenuItemInput!" ] } @@ -7222,7 +7515,7 @@ export default { 223, { "input": [ - 391, + 407, "CreateFrontComponentInput!" ] } @@ -7231,7 +7524,7 @@ export default { 223, { "input": [ - 392, + 408, "UpdateFrontComponentInput!" ] } @@ -7249,7 +7542,7 @@ export default { 25, { "input": [ - 394, + 410, "CreateAgentInput!" ] } @@ -7258,7 +7551,7 @@ export default { 25, { "input": [ - 395, + 411, "UpdateAgentInput!" ] } @@ -7267,7 +7560,7 @@ export default { 25, { "input": [ - 326, + 342, "AgentIdInput!" ] } @@ -7348,7 +7641,7 @@ export default { 29, { "createRoleInput": [ - 396, + 412, "CreateRoleInput!" ] } @@ -7357,7 +7650,7 @@ export default { 29, { "updateRoleInput": [ - 397, + 413, "UpdateRoleInput!" ] } @@ -7375,7 +7668,7 @@ export default { 16, { "upsertObjectPermissionsInput": [ - 399, + 415, "UpsertObjectPermissionsInput!" ] } @@ -7384,7 +7677,7 @@ export default { 27, { "upsertPermissionFlagsInput": [ - 401, + 417, "UpsertPermissionFlagsInput!" ] } @@ -7393,7 +7686,7 @@ export default { 26, { "upsertFieldPermissionsInput": [ - 402, + 418, "UpsertFieldPermissionsInput!" ] } @@ -7402,7 +7695,7 @@ export default { 172, { "input": [ - 404, + 420, "UpsertRowLevelPermissionPredicatesInput!" ] } @@ -7469,7 +7762,7 @@ export default { 138, { "input": [ - 407, + 423, "CreateApprovedAccessDomainInput!" ] } @@ -7478,7 +7771,7 @@ export default { 6, { "input": [ - 408, + 424, "DeleteApprovedAccessDomainInput!" ] } @@ -7487,7 +7780,7 @@ export default { 138, { "input": [ - 409, + 425, "ValidateApprovedAccessDomainInput!" ] } @@ -7496,7 +7789,7 @@ export default { 2, { "input": [ - 410, + 426, "CreateApiKeyInput!" ] } @@ -7505,7 +7798,7 @@ export default { 2, { "input": [ - 411, + 427, "UpdateApiKeyInput!" ] } @@ -7514,7 +7807,7 @@ export default { 2, { "input": [ - 412, + 428, "RevokeApiKeyInput!" ] } @@ -7536,7 +7829,7 @@ export default { 34, { "input": [ - 413, + 429, "CreateOneFieldMetadataInput!" ] } @@ -7545,7 +7838,7 @@ export default { 34, { "input": [ - 415, + 431, "UpdateOneFieldMetadataInput!" ] } @@ -7554,7 +7847,7 @@ export default { 34, { "input": [ - 417, + 433, "DeleteOneFieldInput!" ] } @@ -7563,7 +7856,7 @@ export default { 56, { "input": [ - 418, + 434, "CreateViewGroupInput!" ] } @@ -7572,7 +7865,7 @@ export default { 56, { "inputs": [ - 418, + 434, "[CreateViewGroupInput!]!" ] } @@ -7581,7 +7874,7 @@ export default { 56, { "input": [ - 419, + 435, "UpdateViewGroupInput!" ] } @@ -7590,7 +7883,7 @@ export default { 56, { "input": [ - 421, + 437, "DeleteViewGroupInput!" ] } @@ -7599,7 +7892,7 @@ export default { 56, { "input": [ - 422, + 438, "DestroyViewGroupInput!" ] } @@ -7608,7 +7901,7 @@ export default { 54, { "input": [ - 423, + 439, "CreateViewFilterInput!" ] } @@ -7617,7 +7910,7 @@ export default { 54, { "input": [ - 424, + 440, "UpdateViewFilterInput!" ] } @@ -7626,7 +7919,7 @@ export default { 54, { "input": [ - 426, + 442, "DeleteViewFilterInput!" ] } @@ -7635,7 +7928,7 @@ export default { 54, { "input": [ - 427, + 443, "DestroyViewFilterInput!" ] } @@ -7644,7 +7937,7 @@ export default { 52, { "input": [ - 428, + 444, "CreateViewFilterGroupInput!" ] } @@ -7657,7 +7950,7 @@ export default { "String!" ], "input": [ - 429, + 445, "UpdateViewFilterGroupInput!" ] } @@ -7721,7 +8014,7 @@ export default { 66, { "data": [ - 430, + 446, "ActivateWorkspaceInput!" ] } @@ -7730,7 +8023,7 @@ export default { 66, { "data": [ - 431, + 447, "UpdateWorkspaceInput!" ] } @@ -7745,7 +8038,7 @@ export default { 175, { "input": [ - 432, + 448, "CreateApplicationRegistrationInput!" ] } @@ -7754,7 +8047,7 @@ export default { 7, { "input": [ - 433, + 449, "UpdateApplicationRegistrationInput!" ] } @@ -7781,7 +8074,7 @@ export default { 5, { "input": [ - 435, + 451, "CreateApplicationRegistrationVariableInput!" ] } @@ -7790,7 +8083,7 @@ export default { 5, { "input": [ - 436, + 452, "UpdateApplicationRegistrationVariableInput!" ] } @@ -7808,7 +8101,7 @@ export default { 7, { "file": [ - 348, + 364, "Upload!" ], "universalIdentifier": [ @@ -7833,7 +8126,7 @@ export default { 194, { "input": [ - 438, + 454, "GetAuthorizationUrlForSSOInput!" ] } @@ -8121,7 +8414,7 @@ export default { 185, { "input": [ - 439, + 455, "SetupOIDCSsoInput!" ] } @@ -8130,7 +8423,7 @@ export default { 185, { "input": [ - 440, + 456, "SetupSAMLSsoInput!" ] } @@ -8139,7 +8432,7 @@ export default { 181, { "input": [ - 441, + 457, "DeleteSsoInput!" ] } @@ -8148,31 +8441,139 @@ export default { 182, { "input": [ - 442, + 458, "EditSsoInput!" ] } ], - "createWebhook": [ + "createMessageFolder": [ + 249, + { + "input": [ + 459, + "CreateMessageFolderInput!" + ] + } + ], + "updateMessageFolder": [ + 249, + { + "input": [ + 460, + "UpdateMessageFolderInput!" + ] + } + ], + "deleteMessageFolder": [ + 249, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "createCalendarChannel": [ + 235, + { + "input": [ + 462, + "CreateCalendarChannelInput!" + ] + } + ], + "updateCalendarChannel": [ + 235, + { + "input": [ + 463, + "UpdateCalendarChannelInput!" + ] + } + ], + "deleteCalendarChannel": [ + 235, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "createMessageChannel": [ + 241, + { + "input": [ + 465, + "CreateMessageChannelInput!" + ] + } + ], + "updateMessageChannel": [ + 241, + { + "input": [ + 466, + "UpdateMessageChannelInput!" + ] + } + ], + "deleteMessageChannel": [ + 241, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "createConnectedAccount": [ 240, { "input": [ - 443, + 468, + "CreateConnectedAccountInput!" + ] + } + ], + "updateConnectedAccount": [ + 240, + { + "input": [ + 469, + "UpdateConnectedAccountInput!" + ] + } + ], + "deleteConnectedAccount": [ + 240, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "createWebhook": [ + 256, + { + "input": [ + 471, "CreateWebhookInput!" ] } ], "updateWebhook": [ - 240, + 256, { "input": [ - 444, + 472, "UpdateWebhookInput!" ] } ], "deleteWebhook": [ - 240, + 256, { "id": [ 3, @@ -8187,7 +8588,7 @@ export default { 221, { "input": [ - 446, + 474, "CreateSkillInput!" ] } @@ -8196,7 +8597,7 @@ export default { 221, { "input": [ - 447, + 475, "UpdateSkillInput!" ] } @@ -8251,7 +8652,7 @@ export default { } ], "duplicateDashboard": [ - 319, + 335, { "id": [ 3, @@ -8260,7 +8661,7 @@ export default { } ], "impersonate": [ - 286, + 302, { "userId": [ 3, @@ -8273,7 +8674,7 @@ export default { } ], "startChannelSync": [ - 311, + 327, { "connectedAccountId": [ 3, @@ -8282,7 +8683,7 @@ export default { } ], "saveImapSmtpCaldavAccount": [ - 309, + 325, { "accountOwnerId": [ 3, @@ -8293,7 +8694,7 @@ export default { "String!" ], "connectionParameters": [ - 448, + 476, "EmailAccountConnectionParameters!" ], "id": [ @@ -8305,13 +8706,13 @@ export default { 154, { "input": [ - 450, + 478, "UpdateLabPublicFeatureFlagInput!" ] } ], "userLookupAdminPanel": [ - 277, + 293, { "userIdentifier": [ 1, @@ -8385,7 +8786,7 @@ export default { } ], "retryJobs": [ - 270, + 286, { "queueName": [ 1, @@ -8398,7 +8799,7 @@ export default { } ], "deleteJobs": [ - 265, + 281, { "queueName": [ 1, @@ -8411,13 +8812,13 @@ export default { } ], "enablePostgresProxy": [ - 310 + 326 ], "disablePostgresProxy": [ - 310 + 326 ], "createPublicDomain": [ - 298, + 314, { "domain": [ 1, @@ -8444,14 +8845,14 @@ export default { } ], "createEmailingDomain": [ - 300, + 316, { "domain": [ 1, "String!" ], "driver": [ - 301, + 317, "EmailingDomainDriver!" ] } @@ -8466,7 +8867,7 @@ export default { } ], "verifyEmailingDomain": [ - 300, + 316, { "id": [ 1, @@ -8478,7 +8879,7 @@ export default { 68, { "input": [ - 451, + 479, "CreateOneAppTokenInput!" ] } @@ -8511,7 +8912,7 @@ export default { 6, { "workspaceMigration": [ - 453, + 481, "WorkspaceMigrationInput!" ] } @@ -8543,7 +8944,7 @@ export default { } ], "createDevelopmentApplication": [ - 287, + 303, { "universalIdentifier": [ 1, @@ -8565,7 +8966,7 @@ export default { } ], "syncApplication": [ - 288, + 304, { "manifest": [ 15, @@ -8574,10 +8975,10 @@ export default { } ], "uploadApplicationFile": [ - 289, + 305, { "file": [ - 348, + 364, "Upload!" ], "applicationUniversalIdentifier": [ @@ -8585,7 +8986,7 @@ export default { "String!" ], "fileFolder": [ - 456, + 484, "FileFolder!" ], "filePath": [ @@ -8691,7 +9092,7 @@ export default { 3 ], "update": [ - 347 + 363 ], "__typename": [ 1 @@ -8736,7 +9137,7 @@ export default { 3 ], "gridPosition": [ - 351 + 367 ], "position": [ 15 @@ -8776,7 +9177,7 @@ export default { 3 ], "gridPosition": [ - 351 + 367 ], "position": [ 15 @@ -8864,7 +9265,7 @@ export default { 3 ], "tabs": [ - 358 + 374 ], "__typename": [ 1 @@ -8887,7 +9288,7 @@ export default { 79 ], "widgets": [ - 359 + 375 ], "__typename": [ 1 @@ -8910,7 +9311,7 @@ export default { 3 ], "gridPosition": [ - 351 + 367 ], "position": [ 15 @@ -8979,7 +9380,7 @@ export default { 3 ], "update": [ - 363 + 379 ], "__typename": [ 1 @@ -9025,7 +9426,7 @@ export default { }, "CreateOneObjectInput": { "object": [ - 365 + 381 ], "__typename": [ 1 @@ -9085,7 +9486,7 @@ export default { }, "UpdateOneObjectInput": { "update": [ - 368 + 384 ], "id": [ 3 @@ -9140,7 +9541,7 @@ export default { 3 ], "update": [ - 370 + 386 ], "__typename": [ 1 @@ -9339,7 +9740,7 @@ export default { 3 ], "update": [ - 378 + 394 ], "__typename": [ 1 @@ -9374,7 +9775,7 @@ export default { 3 ], "update": [ - 382 + 398 ], "__typename": [ 1 @@ -9438,10 +9839,10 @@ export default { 3 ], "groups": [ - 387 + 403 ], "fields": [ - 388 + 404 ], "__typename": [ 1 @@ -9461,7 +9862,7 @@ export default { 6 ], "fields": [ - 388 + 404 ], "__typename": [ 1 @@ -9588,7 +9989,7 @@ export default { 3 ], "update": [ - 393 + 409 ], "__typename": [ 1 @@ -9724,7 +10125,7 @@ export default { }, "UpdateRoleInput": { "update": [ - 398 + 414 ], "id": [ 3 @@ -9779,7 +10180,7 @@ export default { 3 ], "objectPermissions": [ - 400 + 416 ], "__typename": [ 1 @@ -9821,7 +10222,7 @@ export default { 3 ], "fieldPermissions": [ - 403 + 419 ], "__typename": [ 1 @@ -9852,10 +10253,10 @@ export default { 3 ], "predicates": [ - 405 + 421 ], "predicateGroups": [ - 406 + 422 ], "__typename": [ 1 @@ -9987,7 +10388,7 @@ export default { }, "CreateOneFieldMetadataInput": { "field": [ - 414 + 430 ], "__typename": [ 1 @@ -10060,7 +10461,7 @@ export default { 3 ], "update": [ - 416 + 432 ], "__typename": [ 1 @@ -10149,7 +10550,7 @@ export default { 3 ], "update": [ - 420 + 436 ], "__typename": [ 1 @@ -10222,7 +10623,7 @@ export default { 3 ], "update": [ - 425 + 441 ], "__typename": [ 1 @@ -10432,7 +10833,7 @@ export default { 1 ], "update": [ - 434 + 450 ], "__typename": [ 1 @@ -10495,7 +10896,7 @@ export default { 1 ], "update": [ - 437 + 453 ], "__typename": [ 1 @@ -10582,6 +10983,255 @@ export default { 1 ] }, + "CreateMessageFolderInput": { + "id": [ + 3 + ], + "name": [ + 1 + ], + "isSentFolder": [ + 6 + ], + "isSynced": [ + 6 + ], + "externalId": [ + 1 + ], + "pendingSyncAction": [ + 250 + ], + "messageChannelId": [ + 3 + ], + "parentFolderId": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "UpdateMessageFolderInput": { + "id": [ + 3 + ], + "update": [ + 461 + ], + "__typename": [ + 1 + ] + }, + "UpdateMessageFolderInputUpdates": { + "name": [ + 1 + ], + "syncCursor": [ + 1 + ], + "isSynced": [ + 6 + ], + "pendingSyncAction": [ + 250 + ], + "__typename": [ + 1 + ] + }, + "CreateCalendarChannelInput": { + "id": [ + 3 + ], + "handle": [ + 1 + ], + "visibility": [ + 238 + ], + "syncStage": [ + 237 + ], + "connectedAccountId": [ + 3 + ], + "isContactAutoCreationEnabled": [ + 6 + ], + "contactAutoCreationPolicy": [ + 239 + ], + "isSyncEnabled": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "UpdateCalendarChannelInput": { + "id": [ + 3 + ], + "update": [ + 464 + ], + "__typename": [ + 1 + ] + }, + "UpdateCalendarChannelInputUpdates": { + "visibility": [ + 238 + ], + "isContactAutoCreationEnabled": [ + 6 + ], + "contactAutoCreationPolicy": [ + 239 + ], + "isSyncEnabled": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "CreateMessageChannelInput": { + "id": [ + 3 + ], + "handle": [ + 1 + ], + "visibility": [ + 242 + ], + "type": [ + 243 + ], + "syncStage": [ + 248 + ], + "connectedAccountId": [ + 3 + ], + "isContactAutoCreationEnabled": [ + 6 + ], + "contactAutoCreationPolicy": [ + 244 + ], + "messageFolderImportPolicy": [ + 245 + ], + "excludeNonProfessionalEmails": [ + 6 + ], + "excludeGroupEmails": [ + 6 + ], + "pendingGroupEmailsAction": [ + 246 + ], + "isSyncEnabled": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "UpdateMessageChannelInput": { + "id": [ + 3 + ], + "update": [ + 467 + ], + "__typename": [ + 1 + ] + }, + "UpdateMessageChannelInputUpdates": { + "visibility": [ + 242 + ], + "isContactAutoCreationEnabled": [ + 6 + ], + "contactAutoCreationPolicy": [ + 244 + ], + "messageFolderImportPolicy": [ + 245 + ], + "isSyncEnabled": [ + 6 + ], + "excludeNonProfessionalEmails": [ + 6 + ], + "excludeGroupEmails": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "CreateConnectedAccountInput": { + "id": [ + 3 + ], + "handle": [ + 1 + ], + "provider": [ + 1 + ], + "accessToken": [ + 1 + ], + "refreshToken": [ + 1 + ], + "scopes": [ + 1 + ], + "userWorkspaceId": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "UpdateConnectedAccountInput": { + "id": [ + 3 + ], + "update": [ + 470 + ], + "__typename": [ + 1 + ] + }, + "UpdateConnectedAccountInputUpdates": { + "accessToken": [ + 1 + ], + "refreshToken": [ + 1 + ], + "handleAliases": [ + 1 + ], + "scopes": [ + 1 + ], + "__typename": [ + 1 + ] + }, "CreateWebhookInput": { "id": [ 3 @@ -10607,7 +11257,7 @@ export default { 3 ], "update": [ - 445 + 473 ], "__typename": [ 1 @@ -10681,13 +11331,13 @@ export default { }, "EmailAccountConnectionParameters": { "IMAP": [ - 449 + 477 ], "SMTP": [ - 449 + 477 ], "CALDAV": [ - 449 + 477 ], "__typename": [ 1 @@ -10726,7 +11376,7 @@ export default { }, "CreateOneAppTokenInput": { "appToken": [ - 452 + 480 ], "__typename": [ 1 @@ -10742,7 +11392,7 @@ export default { }, "WorkspaceMigrationInput": { "actions": [ - 454 + 482 ], "__typename": [ 1 @@ -10750,10 +11400,10 @@ export default { }, "WorkspaceMigrationDeleteActionInput": { "type": [ - 455 + 483 ], "metadataName": [ - 236 + 252 ], "universalIdentifier": [ 1 @@ -10778,7 +11428,7 @@ export default { 218, { "input": [ - 458, + 486, "LogicFunctionLogsInput!" ] } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts new file mode 100644 index 00000000000..5f1f3082c28 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command.ts @@ -0,0 +1,382 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { isNonEmptyString } from '@sniptt/guards'; +import { Command } from 'nest-commander'; +import { FeatureFlagKey } from 'twenty-shared/types'; +import { Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@Command({ + name: 'upgrade:1-20:migrate-messaging-infrastructure-to-metadata', + description: + 'Backfill connectedAccount, messageChannel, calendarChannel, and messageFolder to core metadata schema', +}) +export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + @InjectRepository(ConnectedAccountEntity) + private readonly connectedAccountRepository: Repository, + @InjectRepository(MessageChannelEntity) + private readonly messageChannelRepository: Repository, + @InjectRepository(CalendarChannelEntity) + private readonly calendarChannelRepository: Repository, + @InjectRepository(MessageFolderEntity) + private readonly messageFolderRepository: Repository, + @InjectRepository(UserWorkspaceEntity) + private readonly userWorkspaceRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + const isAlreadyMigrated = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + workspaceId, + ); + + if (isAlreadyMigrated) { + this.logger.log( + `IS_CONNECTED_ACCOUNT_MIGRATED already enabled for workspace ${workspaceId}, skipping`, + ); + + return; + } + + const connectedAccountWorkspaceRepository = + await this.twentyORMGlobalManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + const messageChannelWorkspaceRepository = + await this.twentyORMGlobalManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const calendarChannelWorkspaceRepository = + await this.twentyORMGlobalManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const messageFolderWorkspaceRepository = + await this.twentyORMGlobalManager.getRepository( + workspaceId, + 'messageFolder', + ); + + const connectedAccounts = await connectedAccountWorkspaceRepository.find(); + const messageChannels = await messageChannelWorkspaceRepository.find(); + const calendarChannels = await calendarChannelWorkspaceRepository.find(); + const messageFolders = await messageFolderWorkspaceRepository.find(); + + const workspaceMemberIdToUserWorkspaceIdMap = + await this.buildWorkspaceMemberIdToUserWorkspaceIdMap(workspaceId); + + const connectedAccountsWithMissingHandle = connectedAccounts.filter( + (account) => !account.handle, + ); + const connectedAccountsWithUnresolvedOwner = connectedAccounts.filter( + (account) => + !workspaceMemberIdToUserWorkspaceIdMap.has(account.accountOwnerId), + ); + const messageChannelsWithMissingHandle = messageChannels.filter( + (channel) => !channel.handle, + ); + const calendarChannelsWithMissingHandle = calendarChannels.filter( + (channel) => !channel.handle, + ); + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Workspace ${workspaceId}: ` + + `${connectedAccounts.length} connected accounts, ` + + `${messageChannels.length} message channels, ` + + `${calendarChannels.length} calendar channels, ` + + `${messageFolders.length} message folders`, + ); + + if (connectedAccountsWithMissingHandle.length > 0) { + this.logger.warn( + `[DRY RUN] ${connectedAccountsWithMissingHandle.length} connected accounts have empty handle`, + ); + } + + if (connectedAccountsWithUnresolvedOwner.length > 0) { + this.logger.warn( + `[DRY RUN] ${connectedAccountsWithUnresolvedOwner.length} connected accounts have unresolvable accountOwnerId (no matching userWorkspace)`, + ); + } + + if (messageChannelsWithMissingHandle.length > 0) { + this.logger.warn( + `[DRY RUN] ${messageChannelsWithMissingHandle.length} message channels have empty handle`, + ); + } + + if (calendarChannelsWithMissingHandle.length > 0) { + this.logger.warn( + `[DRY RUN] ${calendarChannelsWithMissingHandle.length} calendar channels have empty handle`, + ); + } + + return; + } + + let migratedConnectedAccountIds = new Set( + connectedAccounts.map((account) => account.id), + ); + let migratedMessageChannelIds = new Set( + messageChannels.map((channel) => channel.id), + ); + + if (connectedAccounts.length > 0) { + const coreConnectedAccounts = connectedAccounts + .filter((workspaceEntity) => { + const userWorkspaceId = workspaceMemberIdToUserWorkspaceIdMap.get( + workspaceEntity.accountOwnerId, + ); + + if (!userWorkspaceId) { + this.logger.warn( + `Skipping connected account ${workspaceEntity.id}: no userWorkspace found for workspaceMember ${workspaceEntity.accountOwnerId}`, + ); + + return false; + } + + return true; + }) + .map((workspaceEntity) => { + const handleAliases = isNonEmptyString(workspaceEntity.handleAliases) + ? workspaceEntity.handleAliases + .split(',') + .map((alias) => alias.trim()) + : null; + + return { + id: workspaceEntity.id, + handle: workspaceEntity.handle ?? '', + provider: workspaceEntity.provider, + accessToken: workspaceEntity.accessToken, + refreshToken: workspaceEntity.refreshToken, + lastCredentialsRefreshedAt: + workspaceEntity.lastCredentialsRefreshedAt, + authFailedAt: workspaceEntity.authFailedAt, + handleAliases, + scopes: workspaceEntity.scopes, + connectionParameters: + workspaceEntity.connectionParameters as Record< + string, + unknown + > | null, + userWorkspaceId: workspaceMemberIdToUserWorkspaceIdMap.get( + workspaceEntity.accountOwnerId, + )!, + workspaceId, + createdAt: workspaceEntity.createdAt, + updatedAt: workspaceEntity.updatedAt, + }; + }); + + if (coreConnectedAccounts.length > 0) { + await this.connectedAccountRepository.save( + coreConnectedAccounts as unknown as ConnectedAccountEntity[], + ); + this.logger.log( + `Migrated ${coreConnectedAccounts.length} connected accounts for workspace ${workspaceId}`, + ); + } + + migratedConnectedAccountIds = new Set( + coreConnectedAccounts.map((account) => account.id), + ); + } + + if (messageChannels.length > 0) { + const coreMessageChannels = messageChannels + .filter((workspaceEntity) => + migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId), + ) + .map((workspaceEntity) => ({ + id: workspaceEntity.id, + visibility: workspaceEntity.visibility, + handle: workspaceEntity.handle ?? '', + type: workspaceEntity.type, + isContactAutoCreationEnabled: + workspaceEntity.isContactAutoCreationEnabled, + contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy, + messageFolderImportPolicy: workspaceEntity.messageFolderImportPolicy, + excludeNonProfessionalEmails: + workspaceEntity.excludeNonProfessionalEmails, + excludeGroupEmails: workspaceEntity.excludeGroupEmails, + pendingGroupEmailsAction: workspaceEntity.pendingGroupEmailsAction, + isSyncEnabled: workspaceEntity.isSyncEnabled, + syncCursor: workspaceEntity.syncCursor, + syncedAt: workspaceEntity.syncedAt + ? new Date(workspaceEntity.syncedAt) + : null, + syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED', + syncStage: workspaceEntity.syncStage, + syncStageStartedAt: workspaceEntity.syncStageStartedAt + ? new Date(workspaceEntity.syncStageStartedAt) + : null, + throttleFailureCount: workspaceEntity.throttleFailureCount, + throttleRetryAfter: workspaceEntity.throttleRetryAfter + ? new Date(workspaceEntity.throttleRetryAfter) + : null, + connectedAccountId: workspaceEntity.connectedAccountId, + workspaceId, + createdAt: workspaceEntity.createdAt, + updatedAt: workspaceEntity.updatedAt, + })); + + if (coreMessageChannels.length > 0) { + await this.messageChannelRepository.save( + coreMessageChannels as unknown as MessageChannelEntity[], + ); + this.logger.log( + `Migrated ${coreMessageChannels.length} message channels for workspace ${workspaceId}`, + ); + } + + migratedMessageChannelIds = new Set( + coreMessageChannels.map((channel) => channel.id), + ); + } + + if (calendarChannels.length > 0) { + const coreCalendarChannels = calendarChannels + .filter((workspaceEntity) => + migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId), + ) + .map((workspaceEntity) => ({ + id: workspaceEntity.id, + handle: workspaceEntity.handle ?? '', + syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED', + syncStage: workspaceEntity.syncStage, + visibility: workspaceEntity.visibility, + isContactAutoCreationEnabled: + workspaceEntity.isContactAutoCreationEnabled, + contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy, + isSyncEnabled: workspaceEntity.isSyncEnabled, + syncCursor: workspaceEntity.syncCursor, + syncedAt: workspaceEntity.syncedAt + ? new Date(workspaceEntity.syncedAt) + : null, + syncStageStartedAt: workspaceEntity.syncStageStartedAt + ? new Date(workspaceEntity.syncStageStartedAt) + : null, + throttleFailureCount: workspaceEntity.throttleFailureCount, + connectedAccountId: workspaceEntity.connectedAccountId, + workspaceId, + createdAt: workspaceEntity.createdAt, + updatedAt: workspaceEntity.updatedAt, + })); + + await this.calendarChannelRepository.save( + coreCalendarChannels as unknown as CalendarChannelEntity[], + ); + this.logger.log( + `Migrated ${coreCalendarChannels.length} calendar channels for workspace ${workspaceId}`, + ); + } + + if (messageFolders.length > 0) { + const coreMessageFolders = messageFolders + .filter((workspaceEntity) => + migratedMessageChannelIds.has(workspaceEntity.messageChannelId), + ) + .map((workspaceEntity) => ({ + id: workspaceEntity.id, + name: workspaceEntity.name, + syncCursor: workspaceEntity.syncCursor, + isSentFolder: workspaceEntity.isSentFolder, + isSynced: workspaceEntity.isSynced, + parentFolderId: isNonEmptyString(workspaceEntity.parentFolderId) + ? workspaceEntity.parentFolderId + : null, + externalId: workspaceEntity.externalId, + pendingSyncAction: workspaceEntity.pendingSyncAction, + messageChannelId: workspaceEntity.messageChannelId, + workspaceId, + createdAt: workspaceEntity.createdAt, + updatedAt: workspaceEntity.updatedAt, + })); + + await this.messageFolderRepository.save( + coreMessageFolders as unknown as MessageFolderEntity[], + ); + this.logger.log( + `Migrated ${coreMessageFolders.length} message folders for workspace ${workspaceId}`, + ); + } + + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED], + workspaceId, + ); + + this.logger.log( + `Enabled IS_CONNECTED_ACCOUNT_MIGRATED for workspace ${workspaceId}`, + ); + } + + private async buildWorkspaceMemberIdToUserWorkspaceIdMap( + workspaceId: string, + ): Promise> { + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepository( + workspaceId, + 'workspaceMember', + ); + + const workspaceMembers = await workspaceMemberRepository.find(); + const userWorkspaces = await this.userWorkspaceRepository.find({ + where: { workspaceId }, + select: ['id', 'userId'], + }); + + const userWorkspaceIdByUserId = new Map( + userWorkspaces.map((userWorkspace) => [ + userWorkspace.userId, + userWorkspace.id, + ]), + ); + + return new Map( + workspaceMembers + .filter((member) => userWorkspaceIdByUserId.has(member.userId)) + .map((member) => [ + member.id, + userWorkspaceIdByUserId.get(member.userId)!, + ]), + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts index b3e35ab4f0f..f605a10cf61 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts @@ -8,13 +8,19 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command'; import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @@ -23,7 +29,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace @Module({ imports: [ - TypeOrmModule.forFeature([WorkspaceEntity]), + TypeOrmModule.forFeature([ + WorkspaceEntity, + ConnectedAccountEntity, + MessageChannelEntity, + CalendarChannelEntity, + MessageFolderEntity, + UserWorkspaceEntity, + ]), DataSourceModule, WorkspaceCacheModule, WorkspaceCacheStorageModule, @@ -44,6 +57,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace BackfillPageLayoutsCommand, SeedCliApplicationRegistrationCommand, MigrateRichTextToTextCommand, + MigrateMessagingInfrastructureToMetadataCommand, ], exports: [ IdentifyPermissionFlagMetadataCommand, @@ -55,6 +69,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace BackfillPageLayoutsCommand, SeedCliApplicationRegistrationCommand, MigrateRichTextToTextCommand, + MigrateMessagingInfrastructureToMetadataCommand, ], }) export class V1_20_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 8eec277e653..0c9202120f2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -40,6 +40,7 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command'; import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -101,6 +102,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand, protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand, protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand, + protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand, ) { super( workspaceRepository, @@ -159,6 +161,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.backfillCommandMenuItemsCommand, this.backfillPageLayoutsCommand, this.seedCliApplicationRegistrationCommand, + this.migrateMessagingInfrastructureToMetadataCommand, ]; this.allCommands = { diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1773945207801-add-messaging-infrastructure-metadata-entities.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773945207801-add-messaging-infrastructure-metadata-entities.ts new file mode 100644 index 00000000000..bbcd3e1eeb7 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773945207801-add-messaging-infrastructure-metadata-entities.ts @@ -0,0 +1,139 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMessagingInfrastructureMetadataEntities1773945207801 + implements MigrationInterface +{ + name = 'AddMessagingInfrastructureMetadataEntities1773945207801'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."connectedAccount" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "provider" character varying NOT NULL, "accessToken" character varying, "refreshToken" character varying, "lastCredentialsRefreshedAt" TIMESTAMP WITH TIME ZONE, "authFailedAt" TIMESTAMP WITH TIME ZONE, "handleAliases" character varying array, "scopes" character varying array, "connectionParameters" jsonb, "lastSignedInAt" TIMESTAMP WITH TIME ZONE, "oidcTokenClaims" jsonb, "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8e7a0a0bbc2e06ac2acf89b7f3a" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_visibility_enum" AS ENUM('METADATA', 'SUBJECT', 'SHARE_EVERYTHING')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_type_enum" AS ENUM('EMAIL', 'SMS')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_contactautocreationpolicy_enum" AS ENUM('SENT_AND_RECEIVED', 'SENT', 'NONE')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_messagefolderimportpolicy_enum" AS ENUM('ALL_FOLDERS', 'SELECTED_FOLDERS')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_pendinggroupemailsaction_enum" AS ENUM('GROUP_EMAILS_DELETION', 'GROUP_EMAILS_IMPORT', 'NONE')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'MESSAGE_LIST_FETCH_PENDING', 'MESSAGE_LIST_FETCH_SCHEDULED', 'MESSAGE_LIST_FETCH_ONGOING', 'MESSAGES_IMPORT_PENDING', 'MESSAGES_IMPORT_SCHEDULED', 'MESSAGES_IMPORT_ONGOING', 'FAILED')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."messageChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "visibility" "core"."messageChannel_visibility_enum" NOT NULL, "handle" character varying NOT NULL, "type" "core"."messageChannel_type_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL, "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL, "excludeNonProfessionalEmails" boolean NOT NULL, "excludeGroupEmails" boolean NOT NULL, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStatus" "core"."messageChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."messageChannel_syncstage_enum" NOT NULL, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "throttleRetryAfter" TIMESTAMP WITH TIME ZONE, "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_438b9412475f39712ed065f77af" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM('FOLDER_DELETION', 'NONE')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."messageFolder" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "syncCursor" character varying, "isSentFolder" boolean NOT NULL, "isSynced" boolean NOT NULL, "parentFolderId" uuid, "externalId" character varying, "pendingSyncAction" "core"."messageFolder_pendingsyncaction_enum" NOT NULL, "messageChannelId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_85cb5a339d9f7f1106dde9e4db8" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "core"."calendarChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."calendarChannel_syncstage_enum" AS ENUM('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')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."calendarChannel_visibility_enum" AS ENUM('METADATA', 'SHARE_EVERYTHING')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."calendarChannel_contactautocreationpolicy_enum" AS ENUM('AS_PARTICIPANT_AND_ORGANIZER', 'AS_PARTICIPANT', 'AS_ORGANIZER', 'NONE')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."calendarChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "syncStatus" "core"."calendarChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."calendarChannel_syncstage_enum" NOT NULL, "visibility" "core"."calendarChannel_visibility_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_796d701c0c35518517d0f3e0e0b" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."connectedAccount" ADD CONSTRAINT "FK_1c7af038a011e99c27044793c6a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_2e966cbb240771c67630d52895c" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe" FOREIGN KEY ("messageChannelId") REFERENCES "core"."messageChannel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_c7bc368c97a18a072413d67cf45" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_c7bc368c97a18a072413d67cf45"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_2e966cbb240771c67630d52895c"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."connectedAccount" DROP CONSTRAINT "FK_1c7af038a011e99c27044793c6a"`, + ); + await queryRunner.query(`DROP TABLE "core"."calendarChannel"`); + await queryRunner.query( + `DROP TYPE "core"."calendarChannel_contactautocreationpolicy_enum"`, + ); + await queryRunner.query( + `DROP TYPE "core"."calendarChannel_visibility_enum"`, + ); + await queryRunner.query( + `DROP TYPE "core"."calendarChannel_syncstage_enum"`, + ); + await queryRunner.query( + `DROP TYPE "core"."calendarChannel_syncstatus_enum"`, + ); + await queryRunner.query(`DROP TABLE "core"."messageFolder"`); + await queryRunner.query( + `DROP TYPE "core"."messageFolder_pendingsyncaction_enum"`, + ); + await queryRunner.query(`DROP TABLE "core"."messageChannel"`); + await queryRunner.query(`DROP TYPE "core"."messageChannel_syncstage_enum"`); + await queryRunner.query( + `DROP TYPE "core"."messageChannel_syncstatus_enum"`, + ); + await queryRunner.query( + `DROP TYPE "core"."messageChannel_pendinggroupemailsaction_enum"`, + ); + await queryRunner.query( + `DROP TYPE "core"."messageChannel_messagefolderimportpolicy_enum"`, + ); + await queryRunner.query( + `DROP TYPE "core"."messageChannel_contactautocreationpolicy_enum"`, + ); + await queryRunner.query(`DROP TYPE "core"."messageChannel_type_enum"`); + await queryRunner.query( + `DROP TYPE "core"."messageChannel_visibility_enum"`, + ); + await queryRunner.query(`DROP TABLE "core"."connectedAccount"`); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 793221ac8ad..98f3885ec68 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -59,6 +59,9 @@ import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-inv import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -104,6 +107,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; ConnectedAccountModule, MessagingCommonModule, MessagingFolderSyncManagerModule, + CalendarChannelDataAccessModule, + ConnectedAccountDataAccessModule, + MessageChannelDataAccessModule, WorkspaceSSOModule, FeatureFlagModule, WorkspaceInvitationModule, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts index e7ce8c8d21f..3341ac6c9a2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { v4 } from 'uuid'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; @@ -9,7 +10,6 @@ import { CalendarChannelSyncStage, CalendarChannelSyncStatus, CalendarChannelVisibility, - type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; export type CreateCalendarChannelInput = { @@ -25,6 +25,7 @@ export type CreateCalendarChannelInput = { export class CreateCalendarChannelService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, ) {} async createCalendarChannel( @@ -43,15 +44,12 @@ export class CreateCalendarChannelService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); + const newCalendarChannelId = v4(); - const newCalendarChannel = await calendarChannelRepository.save( + await this.calendarChannelDataAccessService.save( + workspaceId, { - id: v4(), + id: newCalendarChannelId, connectedAccountId, handle, visibility: @@ -63,11 +61,10 @@ export class CreateCalendarChannelService { ? CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING : CalendarChannelSyncStage.PENDING_CONFIGURATION, }, - {}, manager, ); - return newCalendarChannel.id; + return newCalendarChannelId; }, authContext, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts index b174a8c01ae..58eda495c51 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; import { type ConnectedAccountProvider } from 'twenty-shared/types'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; export type CreateConnectedAccountInput = { workspaceId: string; @@ -23,6 +23,7 @@ export type CreateConnectedAccountInput = { export class CreateConnectedAccountService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} async createConnectedAccount( @@ -43,13 +44,8 @@ export class CreateConnectedAccountService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - await connectedAccountRepository.save( + await this.connectedAccountDataAccessService.save( + workspaceId, { id: connectedAccountId, handle, @@ -59,7 +55,6 @@ export class CreateConnectedAccountService { accountOwnerId, scopes, }, - {}, manager, ); }, authContext); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts index 09b14bcda9c..437a597273a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; @@ -29,6 +30,7 @@ export type CreateMessageChannelInput = { export class CreateMessageChannelService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly syncMessageFoldersService: SyncMessageFoldersService, ) {} @@ -48,15 +50,10 @@ export class CreateMessageChannelService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - const newMessageChannelId = v4(); - await messageChannelRepository.insert( + await this.messageChannelDataAccessService.save( + workspaceId, { id: newMessageChannelId, connectedAccountId, @@ -76,17 +73,19 @@ export class CreateMessageChannelService { manager, ); - const createdMessageChannel = await messageChannelRepository.findOne({ - where: { id: newMessageChannelId }, - relations: ['connectedAccount', 'messageFolders'], - }); + const createdMessageChannel = + await this.messageChannelDataAccessService.findOne(workspaceId, { + where: { id: newMessageChannelId }, + relations: ['connectedAccount', 'messageFolders'], + }); if (!isDefined(createdMessageChannel)) { throw new Error('Message channel not found'); } await this.syncMessageFoldersService.syncMessageFolders({ - messageChannel: createdMessageChannel, + messageChannel: + createdMessageChannel as unknown as MessageChannelWorkspaceEntity, workspaceId, }); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts index b9524d2391d..5ecb22f374d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts @@ -11,6 +11,9 @@ import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/au import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -36,22 +39,16 @@ describe('GoogleAPIsService', () => { let messagingChannelSyncStatusService: MessageChannelSyncStatusService; let createMessageChannelService: CreateMessageChannelService; - const mockConnectedAccountRepository = { + const mockConnectedAccountDataAccessService = { findOne: jest.fn(), - find: jest.fn(), - update: jest.fn(), }; - const mockCalendarChannelRepository = { - findOne: jest.fn(), + const mockMessageChannelDataAccessService = { find: jest.fn(), - update: jest.fn(), }; - const mockMessageChannelRepository = { - findOne: jest.fn(), + const mockCalendarChannelDataAccessService = { find: jest.fn(), - update: jest.fn(), }; const mockWorkspaceMemberRepository = { @@ -84,12 +81,6 @@ describe('GoogleAPIsService', () => { getRepository: jest .fn() .mockImplementation((_workspaceId, entity) => { - if (entity === 'connectedAccount') - return mockConnectedAccountRepository; - if (entity === 'calendarChannel') - return mockCalendarChannelRepository; - if (entity === 'messageChannel') - return mockMessageChannelRepository; if (entity === 'workspaceMember') return mockWorkspaceMemberRepository; @@ -188,6 +179,18 @@ describe('GoogleAPIsService', () => { isFeatureEnabled: jest.fn().mockResolvedValue(false), }, }, + { + provide: ConnectedAccountDataAccessService, + useValue: mockConnectedAccountDataAccessService, + }, + { + provide: MessageChannelDataAccessService, + useValue: mockMessageChannelDataAccessService, + }, + { + provide: CalendarChannelDataAccessService, + useValue: mockCalendarChannelDataAccessService, + }, ], }).compile(); @@ -221,7 +224,7 @@ describe('GoogleAPIsService', () => { provider: ConnectedAccountProvider.GOOGLE, } as ConnectedAccountWorkspaceEntity; - mockConnectedAccountRepository.findOne.mockResolvedValue( + mockConnectedAccountDataAccessService.findOne.mockResolvedValue( existingConnectedAccount, ); @@ -237,11 +240,11 @@ describe('GoogleAPIsService', () => { syncStage: CalendarChannelSyncStage.FAILED, }; - mockCalendarChannelRepository.find.mockResolvedValue([ + mockCalendarChannelDataAccessService.find.mockResolvedValue([ failedCalendarChannel, ]); - mockMessageChannelRepository.find.mockResolvedValue([]); + mockMessageChannelDataAccessService.find.mockResolvedValue([]); await service.refreshGoogleRefreshToken({ handle: 'test@example.com', diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index de41eb1dfde..073de4ce7e2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -18,6 +18,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; @@ -29,16 +32,14 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se import { CalendarChannelSyncStage, type CalendarChannelVisibility, - type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { MessageChannelSyncStage, MessageChannelSyncStatus, type MessageChannelVisibility, - type MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingMessageListFetchJob, @@ -65,6 +66,9 @@ export class GoogleAPIsService { private readonly googleAPIScopesService: GoogleAPIScopesService, private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService, private readonly featureFlagService: FeatureFlagService, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, ) {} async refreshGoogleRefreshToken(input: { @@ -128,31 +132,14 @@ export class GoogleAPIsService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - const connectedAccount = await connectedAccountRepository.findOne({ - where: { handle, accountOwnerId: workspaceMemberId }, - }); + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { handle, accountOwnerId: workspaceMemberId }, + }); const existingAccountId = connectedAccount?.id; const newOrExistingConnectedAccountId = existingAccountId ?? v4(); - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - const workspaceDataSource = await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource(); @@ -200,7 +187,6 @@ export class GoogleAPIsService { accessToken: input.accessToken, refreshToken: input.refreshToken, scopes, - connectedAccount, manager, }, ); @@ -229,9 +215,10 @@ export class GoogleAPIsService { ); if (isMessagingEnabled) { - const messageChannels = await messageChannelRepository.find({ - where: { connectedAccountId: newOrExistingConnectedAccountId }, - }); + const messageChannels = + await this.messageChannelDataAccessService.find(workspaceId, { + connectedAccountId: newOrExistingConnectedAccountId, + }); if (!isMessagingAvailable && messageChannels.length > 0) { await this.messagingChannelSyncStatusService.markAsFailed( @@ -261,9 +248,12 @@ export class GoogleAPIsService { } if (isCalendarEnabled) { - const calendarChannels = await calendarChannelRepository.find({ - where: { connectedAccountId: newOrExistingConnectedAccountId }, - }); + const calendarChannels = + await this.calendarChannelDataAccessService.find(workspaceId, { + where: { + connectedAccountId: newOrExistingConnectedAccountId, + }, + }); if (!isCalendarAvailable && calendarChannels.length > 0) { await this.calendarChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts index ba5114910a1..86eddd77ae4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts @@ -8,6 +8,9 @@ import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/serv import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service'; import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -33,22 +36,16 @@ describe('MicrosoftAPIsService', () => { let calendarChannelSyncStatusService: CalendarChannelSyncStatusService; let createMessageChannelService: CreateMessageChannelService; - const mockConnectedAccountRepository = { + const mockConnectedAccountDataAccessService = { findOne: jest.fn(), - find: jest.fn(), - update: jest.fn(), }; - const mockCalendarChannelRepository = { - findOne: jest.fn(), + const mockMessageChannelDataAccessService = { find: jest.fn(), - update: jest.fn(), }; - const mockMessageChannelRepository = { - findOne: jest.fn(), + const mockCalendarChannelDataAccessService = { find: jest.fn(), - update: jest.fn(), }; const mockWorkspaceMemberRepository = { @@ -81,12 +78,6 @@ describe('MicrosoftAPIsService', () => { getRepository: jest .fn() .mockImplementation((_workspaceId, entity) => { - if (entity === 'connectedAccount') - return mockConnectedAccountRepository; - if (entity === 'calendarChannel') - return mockCalendarChannelRepository; - if (entity === 'messageChannel') - return mockMessageChannelRepository; if (entity === 'workspaceMember') return mockWorkspaceMemberRepository; @@ -162,6 +153,18 @@ describe('MicrosoftAPIsService', () => { provide: getQueueToken(MessageQueue.calendarQueue), useValue: mockCalendarQueueService, }, + { + provide: ConnectedAccountDataAccessService, + useValue: mockConnectedAccountDataAccessService, + }, + { + provide: MessageChannelDataAccessService, + useValue: mockMessageChannelDataAccessService, + }, + { + provide: CalendarChannelDataAccessService, + useValue: mockCalendarChannelDataAccessService, + }, ], }).compile(); @@ -195,7 +198,7 @@ describe('MicrosoftAPIsService', () => { provider: ConnectedAccountProvider.MICROSOFT, } as ConnectedAccountWorkspaceEntity; - mockConnectedAccountRepository.findOne.mockResolvedValue( + mockConnectedAccountDataAccessService.findOne.mockResolvedValue( existingConnectedAccount, ); @@ -211,11 +214,11 @@ describe('MicrosoftAPIsService', () => { syncStage: CalendarChannelSyncStage.FAILED, }; - mockCalendarChannelRepository.find.mockResolvedValue([ + mockCalendarChannelDataAccessService.find.mockResolvedValue([ failedCalendarChannel, ]); - mockMessageChannelRepository.find.mockResolvedValue([ + mockMessageChannelDataAccessService.find.mockResolvedValue([ { id: 'message-channel-id', connectedAccountId: 'existing-account-id', diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts index f637168ecc5..90772e97c22 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts @@ -12,6 +12,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; @@ -23,15 +26,13 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se import { CalendarChannelSyncStage, type CalendarChannelVisibility, - type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { MessageChannelSyncStage, type MessageChannelVisibility, - type MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingMessageListFetchJob, @@ -55,6 +56,9 @@ export class MicrosoftAPIsService { private readonly createConnectedAccountService: CreateConnectedAccountService, private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService, private readonly twentyConfigService: TwentyConfigService, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, ) {} async refreshMicrosoftRefreshToken(input: { @@ -82,31 +86,14 @@ export class MicrosoftAPIsService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - const connectedAccount = await connectedAccountRepository.findOne({ - where: { handle, accountOwnerId: workspaceMemberId }, - }); + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { handle, accountOwnerId: workspaceMemberId }, + }); const existingAccountId = connectedAccount?.id; const newOrExistingConnectedAccountId = existingAccountId ?? v4(); - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - const workspaceDataSource = await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource(); @@ -156,7 +143,6 @@ export class MicrosoftAPIsService { accessToken: input.accessToken, refreshToken: input.refreshToken, scopes, - connectedAccount, manager, }, ); @@ -197,11 +183,10 @@ export class MicrosoftAPIsService { if ( this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED') ) { - const messageChannels = await messageChannelRepository.find({ - where: { + const messageChannels = + await this.messageChannelDataAccessService.find(workspaceId, { connectedAccountId: newOrExistingConnectedAccountId, - }, - }); + }); for (const messageChannel of messageChannels) { if ( @@ -222,11 +207,12 @@ export class MicrosoftAPIsService { if ( this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED') ) { - const calendarChannels = await calendarChannelRepository.find({ - where: { - connectedAccountId: newOrExistingConnectedAccountId, - }, - }); + const calendarChannels = + await this.calendarChannelDataAccessService.find(workspaceId, { + where: { + connectedAccountId: newOrExistingConnectedAccountId, + }, + }); for (const calendarChannel of calendarChannels) { if ( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts index 2f6c72cec65..4847906c225 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; export type UpdateConnectedAccountOnReconnectInput = { workspaceId: string; @@ -11,7 +11,6 @@ export type UpdateConnectedAccountOnReconnectInput = { accessToken: string; refreshToken: string; scopes: string[]; - connectedAccount: ConnectedAccountWorkspaceEntity; manager: WorkspaceEntityManager; }; @@ -19,6 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = { export class UpdateConnectedAccountOnReconnectService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} async updateConnectedAccountOnReconnect( @@ -30,19 +30,13 @@ export class UpdateConnectedAccountOnReconnectService { accessToken, refreshToken, scopes, - manager, } = input; const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - await connectedAccountRepository.update( + await this.connectedAccountDataAccessService.update( + workspaceId, { id: connectedAccountId, }, @@ -52,7 +46,6 @@ export class UpdateConnectedAccountOnReconnectService { scopes, authFailedAt: null, }, - manager, ); }, authContext); } diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.module.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.module.ts index 2fb080560eb..b6ad67a7636 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.module.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { ImapSmtpCaldavValidatorModule } from 'src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.module'; import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; @@ -21,6 +22,7 @@ import { ImapSmtpCaldavService } from './services/imap-smtp-caldav-connection.se IMAPAPIsModule, MessagingImportManagerModule, MessageQueueModule, + ConnectedAccountDataAccessModule, TwentyORMModule, FeatureFlagModule, ImapSmtpCaldavValidatorModule, diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts index 448384a5273..6e26f3a0197 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts @@ -6,6 +6,7 @@ import { createTransport } from 'nodemailer'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; import { type AccountType, type ConnectionParameters, @@ -21,6 +22,7 @@ export class ImapSmtpCaldavService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} async testImapConnection( @@ -188,20 +190,15 @@ export class ImapSmtpCaldavService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { + id: connectionId, + provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, + }, + }); - const connectedAccount = await connectedAccountRepository.findOne({ - where: { - id: connectionId, - provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, - }, - }); - - return connectedAccount; + return connectedAccount as ConnectedAccountWorkspaceEntity | null; }, authContext, ); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts b/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts index 5cf2b517f1c..9cf9b505a65 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts index 1f93b0a5eae..0ad458df321 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts @@ -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, @@ -54,20 +56,15 @@ export class EmailComposerService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - 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( - 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); diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module.ts new file mode 100644 index 00000000000..b1b68ec33f9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service'; +import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; +import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor'; +import { CalendarChannelResolver } from 'src/engine/metadata-modules/calendar-channel/resolvers/calendar-channel.resolver'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([CalendarChannelEntity]), + AuthModule, + PermissionsModule, + FeatureFlagModule, + ], + providers: [ + CalendarChannelMetadataService, + CalendarChannelResolver, + CalendarChannelGraphqlApiExceptionInterceptor, + ], + exports: [CalendarChannelMetadataService], +}) +export class CalendarChannelMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service.ts new file mode 100644 index 00000000000..61a2217c9dd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { + CalendarChannelSyncStage, + CalendarChannelVisibility, +} from 'twenty-shared/types'; + +import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto'; +import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; + +@Injectable() +export class CalendarChannelMetadataService { + constructor( + @InjectRepository(CalendarChannelEntity) + private readonly repository: Repository, + ) {} + + async findAll(workspaceId: string): Promise { + return this.repository.find({ where: { workspaceId } }); + } + + async findByConnectedAccountId( + connectedAccountId: string, + workspaceId: string, + ): Promise { + return this.repository.find({ + where: { connectedAccountId, workspaceId }, + }); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + return this.repository.findOne({ where: { id, workspaceId } }); + } + + async create( + data: Partial & { + workspaceId: string; + handle: string; + connectedAccountId: string; + visibility: CalendarChannelVisibility; + syncStage: CalendarChannelSyncStage; + }, + ): Promise { + const entity = this.repository.create(data); + + return this.repository.save(entity); + } + + async update( + id: string, + workspaceId: string, + data: Partial, + ): Promise { + await this.repository.update( + { id, workspaceId }, + data as Record, + ); + + return this.repository.findOneOrFail({ where: { id, workspaceId } }); + } + + async delete(id: string, workspaceId: string): Promise { + const entity = await this.repository.findOneOrFail({ + where: { id, workspaceId }, + }); + + await this.repository.delete({ id, workspaceId }); + + return entity; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts new file mode 100644 index 00000000000..004d1ff4972 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/calendar-channel.exception.ts @@ -0,0 +1,37 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { CustomException } from 'src/utils/custom-exception'; + +export enum CalendarChannelExceptionCode { + CALENDAR_CHANNEL_NOT_FOUND = 'CALENDAR_CHANNEL_NOT_FOUND', + INVALID_CALENDAR_CHANNEL_INPUT = 'INVALID_CALENDAR_CHANNEL_INPUT', +} + +const getCalendarChannelExceptionUserFriendlyMessage = ( + code: CalendarChannelExceptionCode, +) => { + switch (code) { + case CalendarChannelExceptionCode.CALENDAR_CHANNEL_NOT_FOUND: + return msg`Calendar channel not found.`; + case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT: + return msg`Invalid calendar channel input.`; + default: + assertUnreachable(code); + } +}; + +export class CalendarChannelException extends CustomException { + constructor( + message: string, + code: CalendarChannelExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? + getCalendarChannelExceptionUserFriendlyMessage(code), + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module.ts new file mode 100644 index 00000000000..6c2f244af2c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([CalendarChannelEntity]), + FeatureFlagModule, + ConnectedAccountDataAccessModule, + ], + providers: [CalendarChannelDataAccessService], + exports: [CalendarChannelDataAccessService], +}) +export class CalendarChannelDataAccessModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service.ts new file mode 100644 index 00000000000..0b15a0f654b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service.ts @@ -0,0 +1,280 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { FeatureFlagKey } from 'twenty-shared/types'; +import { + type FindManyOptions, + type FindOneOptions, + type FindOptionsWhere, + Repository, +} from 'typeorm'; + +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; + +@Injectable() +export class CalendarChannelDataAccessService { + private readonly logger = new Logger(CalendarChannelDataAccessService.name); + + constructor( + @InjectRepository(CalendarChannelEntity) + private readonly coreRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, + ) {} + + private async isMigrated(workspaceId: string): Promise { + return this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + workspaceId, + ); + } + + private async toCoreWhere( + workspaceId: string, + where: Record, + ): Promise> { + const coreWhere: Record = { ...where, workspaceId }; + + if ( + coreWhere.connectedAccount && + typeof coreWhere.connectedAccount === 'object' + ) { + const connectedAccountWhere = { + ...(coreWhere.connectedAccount as Record), + }; + + if ('accountOwnerId' in connectedAccountWhere) { + const { accountOwnerId, ...restConnectedAccount } = + connectedAccountWhere; + + const resolvedConnectedAccounts = + await this.connectedAccountDataAccessService.find(workspaceId, { + accountOwnerId, + } as never); + + if (resolvedConnectedAccounts.length > 0) { + coreWhere.connectedAccountId = resolvedConnectedAccounts[0].id; + } else { + coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000'; + } + + if (Object.keys(restConnectedAccount).length > 0) { + coreWhere.connectedAccount = restConnectedAccount; + } else { + delete coreWhere.connectedAccount; + } + } + } + + return coreWhere; + } + + async getWorkspaceRepository(workspaceId: string) { + return this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + } + + async findOne( + workspaceId: string, + options: FindOneOptions, + ): Promise { + if (await this.isMigrated(workspaceId)) { + const where = options.where as Record; + const coreWhere = Array.isArray(where) + ? await Promise.all( + where.map((whereItem: Record) => + this.toCoreWhere(workspaceId, whereItem), + ), + ) + : await this.toCoreWhere(workspaceId, where); + + return this.coreRepository.findOne({ + ...options, + where: coreWhere, + } as FindOneOptions) as unknown as Promise; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.findOne(options); + } + + async find( + workspaceId: string, + whereOrOptions?: + | FindOptionsWhere + | FindManyOptions, + ): Promise { + if ( + whereOrOptions !== undefined && + typeof whereOrOptions === 'object' && + whereOrOptions !== null && + !Array.isArray(whereOrOptions) && + 'where' in whereOrOptions + ) { + const options = + whereOrOptions as FindManyOptions; + + if (await this.isMigrated(workspaceId)) { + const { where } = options; + + const coreWhere = Array.isArray(where) + ? await Promise.all( + where.map((whereItem) => + this.toCoreWhere( + workspaceId, + whereItem as Record, + ), + ), + ) + : await this.toCoreWhere( + workspaceId, + where as Record, + ); + + return this.coreRepository.find({ + ...options, + where: coreWhere, + } as FindManyOptions) as unknown as Promise< + CalendarChannelWorkspaceEntity[] + >; + } + + const workspaceRepository = + await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.find(options); + } + + const where = whereOrOptions as + | FindOptionsWhere + | undefined; + + if (await this.isMigrated(workspaceId)) { + const coreWhere = where + ? await this.toCoreWhere(workspaceId, where as Record) + : { workspaceId }; + + return this.coreRepository.find({ + where: coreWhere, + } as FindManyOptions) as unknown as Promise< + CalendarChannelWorkspaceEntity[] + >; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.find({ where }); + } + + async save( + workspaceId: string, + data: Partial, + manager?: WorkspaceEntityManager, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.save(data, {}, manager); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.save({ + ...data, + workspaceId, + } as unknown as CalendarChannelEntity); + } catch (error) { + this.logger.error( + `Failed to dual-write calendarChannel to core: ${error}`, + ); + throw error; + } + } + } + + async update( + workspaceId: string, + where: FindOptionsWhere, + data: Partial, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.update(where, data); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.update( + { ...where, workspaceId } as FindOptionsWhere, + data as Record, + ); + } catch (error) { + this.logger.error( + `Failed to dual-write calendarChannel update to core: ${error}`, + ); + throw error; + } + } + } + + async increment( + workspaceId: string, + where: FindOptionsWhere, + propertyPath: string, + value: number, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.increment(where, propertyPath, value, undefined, [ + propertyPath, + 'id', + ]); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.increment( + { + ...where, + workspaceId, + } as FindOptionsWhere, + propertyPath, + value, + ); + } catch (error) { + this.logger.error( + `Failed to dual-write calendarChannel increment to core: ${error}`, + ); + throw error; + } + } + } + + async delete( + workspaceId: string, + where: FindOptionsWhere, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.delete(where); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.delete({ + ...where, + workspaceId, + } as FindOptionsWhere); + } catch (error) { + this.logger.error( + `Failed to dual-write calendarChannel delete to core: ${error}`, + ); + throw error; + } + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto.ts new file mode 100644 index 00000000000..076db9816b6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto.ts @@ -0,0 +1,96 @@ +import { Field, HideField, ObjectType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsDateString, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { + CalendarChannelContactAutoCreationPolicy, + CalendarChannelSyncStage, + CalendarChannelSyncStatus, + CalendarChannelVisibility, +} from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('CalendarChannel') +export class CalendarChannelDTO { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @IsString() + @IsNotEmpty() + @Field() + handle: string; + + @IsEnum(CalendarChannelSyncStatus) + @IsNotEmpty() + @Field(() => CalendarChannelSyncStatus) + syncStatus: CalendarChannelSyncStatus; + + @IsEnum(CalendarChannelSyncStage) + @IsNotEmpty() + @Field(() => CalendarChannelSyncStage) + syncStage: CalendarChannelSyncStage; + + @IsEnum(CalendarChannelVisibility) + @IsNotEmpty() + @Field(() => CalendarChannelVisibility) + visibility: CalendarChannelVisibility; + + @IsBoolean() + @Field() + isContactAutoCreationEnabled: boolean; + + @IsEnum(CalendarChannelContactAutoCreationPolicy) + @IsNotEmpty() + @Field(() => CalendarChannelContactAutoCreationPolicy) + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy; + + @IsBoolean() + @Field() + isSyncEnabled: boolean; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + syncCursor: string | null; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + syncedAt: Date | null; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + syncStageStartedAt: Date | null; + + @IsInt() + @Field() + throttleFailureCount: number; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + connectedAccountId: string; + + @HideField() + workspaceId: string; + + @IsDateString() + @Field() + createdAt: Date; + + @IsDateString() + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/create-calendar-channel.input.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/create-calendar-channel.input.ts new file mode 100644 index 00000000000..0ce13fd8351 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/create-calendar-channel.input.ts @@ -0,0 +1,60 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { + CalendarChannelContactAutoCreationPolicy, + CalendarChannelSyncStage, + CalendarChannelVisibility, +} from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class CreateCalendarChannelInput { + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + + @IsString() + @IsNotEmpty() + @Field() + handle: string; + + @IsEnum(CalendarChannelVisibility) + @IsNotEmpty() + @Field(() => CalendarChannelVisibility) + visibility: CalendarChannelVisibility; + + @IsEnum(CalendarChannelSyncStage) + @IsNotEmpty() + @Field(() => CalendarChannelSyncStage) + syncStage: CalendarChannelSyncStage; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + connectedAccountId: string; + + @IsBoolean() + @IsNotEmpty() + @Field() + isContactAutoCreationEnabled: boolean; + + @IsEnum(CalendarChannelContactAutoCreationPolicy) + @IsNotEmpty() + @Field(() => CalendarChannelContactAutoCreationPolicy) + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy; + + @IsBoolean() + @IsNotEmpty() + @Field() + isSyncEnabled: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input.ts new file mode 100644 index 00000000000..01517afba69 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input.ts @@ -0,0 +1,53 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsUUID, + ValidateNested, +} from 'class-validator'; +import { + CalendarChannelContactAutoCreationPolicy, + CalendarChannelVisibility, +} from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class UpdateCalendarChannelInputUpdates { + @IsOptional() + @IsEnum(CalendarChannelVisibility) + @Field(() => CalendarChannelVisibility, { nullable: true }) + visibility?: CalendarChannelVisibility; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + isContactAutoCreationEnabled?: boolean; + + @IsOptional() + @IsEnum(CalendarChannelContactAutoCreationPolicy) + @Field(() => CalendarChannelContactAutoCreationPolicy, { nullable: true }) + contactAutoCreationPolicy?: CalendarChannelContactAutoCreationPolicy; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + isSyncEnabled?: boolean; +} + +@InputType() +export class UpdateCalendarChannelInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @Type(() => UpdateCalendarChannelInputUpdates) + @ValidateNested() + @Field(() => UpdateCalendarChannelInputUpdates) + update: UpdateCalendarChannelInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts new file mode 100644 index 00000000000..8f4220dc33f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts @@ -0,0 +1,93 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { + CalendarChannelContactAutoCreationPolicy, + CalendarChannelSyncStage, + CalendarChannelSyncStatus, + CalendarChannelVisibility, +} from 'twenty-shared/types'; + +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; +import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; + +@Entity({ name: 'calendarChannel', schema: 'core' }) +export class CalendarChannelEntity extends WorkspaceRelatedEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', nullable: false }) + handle: string; + + @Column({ + type: 'enum', + enum: CalendarChannelSyncStatus, + nullable: false, + default: CalendarChannelSyncStatus.NOT_SYNCED, + }) + syncStatus: CalendarChannelSyncStatus; + + @Column({ + type: 'enum', + enum: CalendarChannelSyncStage, + nullable: false, + }) + syncStage: CalendarChannelSyncStage; + + @Column({ + type: 'enum', + enum: CalendarChannelVisibility, + nullable: false, + }) + visibility: CalendarChannelVisibility; + + @Column({ type: 'boolean', nullable: false }) + isContactAutoCreationEnabled: boolean; + + @Column({ + type: 'enum', + enum: CalendarChannelContactAutoCreationPolicy, + nullable: false, + }) + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy; + + @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: 'timestamptz', nullable: true }) + syncStageStartedAt: Date | null; + + @Column({ type: 'integer', nullable: false, default: 0 }) + throttleFailureCount: number; + + @Column({ type: 'uuid', nullable: false }) + connectedAccountId: string; + + @ManyToOne( + () => ConnectedAccountEntity, + (connectedAccount) => connectedAccount.calendarChannels, + { onDelete: 'CASCADE' }, + ) + @JoinColumn({ name: 'connectedAccountId' }) + connectedAccount: Relation; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor.ts new file mode 100644 index 00000000000..6b3b66d4c8f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor.ts @@ -0,0 +1,24 @@ +import { + type CallHandler, + type ExecutionContext, + Injectable, + type NestInterceptor, +} from '@nestjs/common'; + +import { type Observable, catchError } from 'rxjs'; + +import { calendarChannelGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/calendar-channel/utils/calendar-channel-graphql-api-exception-handler.util'; + +@Injectable() +export class CalendarChannelGraphqlApiExceptionInterceptor + implements NestInterceptor +{ + intercept( + _context: ExecutionContext, + next: CallHandler, + ): Observable { + return next + .handle() + .pipe(catchError(calendarChannelGraphqlApiExceptionHandler)); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/resolvers/calendar-channel.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/resolvers/calendar-channel.resolver.ts new file mode 100644 index 00000000000..4f22f2cd188 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/resolvers/calendar-channel.resolver.ts @@ -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 { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service'; +import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto'; +import { CreateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/create-calendar-channel.input'; +import { UpdateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input'; +import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor'; + +@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard) +@UseInterceptors(CalendarChannelGraphqlApiExceptionInterceptor) +@MetadataResolver(() => CalendarChannelDTO) +export class CalendarChannelResolver { + constructor( + private readonly calendarChannelMetadataService: CalendarChannelMetadataService, + ) {} + + @Query(() => [CalendarChannelDTO]) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async calendarChannels( + @AuthWorkspace() workspace: WorkspaceEntity, + @Args('connectedAccountId', { + type: () => UUIDScalarType, + nullable: true, + }) + connectedAccountId?: string, + ): Promise { + if (connectedAccountId) { + return this.calendarChannelMetadataService.findByConnectedAccountId( + connectedAccountId, + workspace.id, + ); + } + + return this.calendarChannelMetadataService.findAll(workspace.id); + } + + @Query(() => CalendarChannelDTO, { nullable: true }) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async calendarChannel( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.calendarChannelMetadataService.findById(id, workspace.id); + } + + @Mutation(() => CalendarChannelDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async createCalendarChannel( + @Args('input') input: CreateCalendarChannelInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.calendarChannelMetadataService.create({ + ...input, + workspaceId: workspace.id, + }); + } + + @Mutation(() => CalendarChannelDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async updateCalendarChannel( + @Args('input') input: UpdateCalendarChannelInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.calendarChannelMetadataService.update( + input.id, + workspace.id, + input.update, + ); + } + + @Mutation(() => CalendarChannelDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async deleteCalendarChannel( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.calendarChannelMetadataService.delete(id, workspace.id); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/utils/calendar-channel-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/utils/calendar-channel-graphql-api-exception-handler.util.ts new file mode 100644 index 00000000000..7662580b6d9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/utils/calendar-channel-graphql-api-exception-handler.util.ts @@ -0,0 +1,26 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + CalendarChannelException, + CalendarChannelExceptionCode, +} from 'src/engine/metadata-modules/calendar-channel/calendar-channel.exception'; + +export const calendarChannelGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof CalendarChannelException) { + switch (error.code) { + case CalendarChannelExceptionCode.CALENDAR_CHANNEL_NOT_FOUND: + throw new NotFoundError(error); + case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT: + throw new UserInputError(error); + default: { + return assertUnreachable(error.code); + } + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.module.ts new file mode 100644 index 00000000000..724a7959672 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; +import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor'; +import { ConnectedAccountResolver } from 'src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ConnectedAccountEntity]), + AuthModule, + PermissionsModule, + FeatureFlagModule, + ], + providers: [ + ConnectedAccountMetadataService, + ConnectedAccountResolver, + ConnectedAccountGraphqlApiExceptionInterceptor, + ], + exports: [ConnectedAccountMetadataService], +}) +export class ConnectedAccountMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts new file mode 100644 index 00000000000..a6cf6e9b1c5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto'; +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; + +@Injectable() +export class ConnectedAccountMetadataService { + constructor( + @InjectRepository(ConnectedAccountEntity) + private readonly repository: Repository, + ) {} + + async findAll(workspaceId: string): Promise { + return this.repository.find({ where: { workspaceId } }); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + return this.repository.findOne({ where: { id, workspaceId } }); + } + + async create( + data: Partial & { + workspaceId: string; + handle: string; + provider: string; + userWorkspaceId: string; + }, + ): Promise { + const entity = this.repository.create(data); + + return this.repository.save(entity); + } + + async update( + id: string, + workspaceId: string, + data: Partial, + ): Promise { + await this.repository.update( + { id, workspaceId }, + data as Record, + ); + + return this.repository.findOneOrFail({ where: { id, workspaceId } }); + } + + async delete(id: string, workspaceId: string): Promise { + const entity = await this.repository.findOneOrFail({ + where: { id, workspaceId }, + }); + + await this.repository.delete({ id, workspaceId }); + + return entity; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account.exception.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account.exception.ts new file mode 100644 index 00000000000..d026f6752f7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account.exception.ts @@ -0,0 +1,37 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { CustomException } from 'src/utils/custom-exception'; + +export enum ConnectedAccountExceptionCode { + CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND', + INVALID_CONNECTED_ACCOUNT_INPUT = 'INVALID_CONNECTED_ACCOUNT_INPUT', +} + +const getConnectedAccountExceptionUserFriendlyMessage = ( + code: ConnectedAccountExceptionCode, +) => { + switch (code) { + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND: + return msg`Connected account not found.`; + case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT: + return msg`Invalid connected account input.`; + default: + assertUnreachable(code); + } +}; + +export class ConnectedAccountException extends CustomException { + constructor( + message: string, + code: ConnectedAccountExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? + getConnectedAccountExceptionUserFriendlyMessage(code), + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module.ts new file mode 100644 index 00000000000..3426b62fbf6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ConnectedAccountEntity, UserWorkspaceEntity]), + FeatureFlagModule, + ], + providers: [ConnectedAccountDataAccessService], + exports: [ConnectedAccountDataAccessService], +}) +export class ConnectedAccountDataAccessModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service.ts new file mode 100644 index 00000000000..336bec30149 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service.ts @@ -0,0 +1,346 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { isNonEmptyString } from '@sniptt/guards'; + +import { FeatureFlagKey } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { + type FindOneOptions, + type FindOptionsWhere, + In, + Repository, +} from 'typeorm'; + +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@Injectable() +export class ConnectedAccountDataAccessService { + private readonly logger = new Logger(ConnectedAccountDataAccessService.name); + + constructor( + @InjectRepository(ConnectedAccountEntity) + private readonly coreRepository: Repository, + @InjectRepository(UserWorkspaceEntity) + private readonly userWorkspaceRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + ) {} + + private async isMigrated(workspaceId: string): Promise { + return this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + workspaceId, + ); + } + + private async resolveUserWorkspaceId( + workspaceId: string, + workspaceMemberId: string, + ): Promise { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + ); + + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { id: workspaceMemberId }, + }); + + if (!workspaceMember) { + return null; + } + + const userWorkspace = await this.userWorkspaceRepository.findOne({ + where: { userId: workspaceMember.userId, workspaceId }, + }); + + return userWorkspace?.id ?? null; + } + + private async toCore( + workspaceId: string, + data: Partial, + ): Promise> { + const { + handleAliases, + lastSyncHistoryId: _lastSyncHistoryId, + accountOwnerId, + ...rest + } = data as Record; + + const coreData: Record = { ...rest }; + + if (handleAliases !== undefined) { + coreData.handleAliases = isNonEmptyString(handleAliases) + ? handleAliases.split(',').map((alias: string) => alias.trim()) + : null; + } + + if (accountOwnerId !== undefined) { + const userWorkspaceId = await this.resolveUserWorkspaceId( + workspaceId, + accountOwnerId as string, + ); + + if (!userWorkspaceId) { + this.logger.warn( + `Could not resolve userWorkspaceId for workspaceMember ${accountOwnerId}`, + ); + } + + coreData.userWorkspaceId = userWorkspaceId; + } + + return coreData as Partial; + } + + private async toCoreWhere( + workspaceId: string, + where: Record, + ): Promise> { + const { accountOwnerId, ...rest } = where; + const coreWhere: Record = { ...rest, workspaceId }; + + if (accountOwnerId !== undefined) { + const userWorkspaceId = await this.resolveUserWorkspaceId( + workspaceId, + accountOwnerId as string, + ); + + if (userWorkspaceId) { + coreWhere.userWorkspaceId = userWorkspaceId; + } else { + this.logger.warn( + `toCoreWhere: could not resolve userWorkspaceId for workspaceMember ${accountOwnerId}, returning empty result`, + ); + coreWhere.id = '00000000-0000-0000-0000-000000000000'; + } + } + + return coreWhere as FindOptionsWhere; + } + + private async fromCoreEntities( + workspaceId: string, + entities: ConnectedAccountEntity[], + ): Promise { + if (entities.length === 0) { + return []; + } + + const userWorkspaceIds = entities + .map((entity) => entity.userWorkspaceId) + .filter(isDefined); + + const userWorkspaces = + userWorkspaceIds.length > 0 + ? await this.userWorkspaceRepository.find({ + where: { id: In(userWorkspaceIds) }, + select: ['id', 'userId'], + }) + : []; + + const userIdByUserWorkspaceId = new Map( + userWorkspaces.map((userWorkspace) => [ + userWorkspace.id, + userWorkspace.userId, + ]), + ); + + const uniqueUserIds = [ + ...new Set(userWorkspaces.map((userWorkspace) => userWorkspace.userId)), + ]; + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + ); + + const workspaceMembers = + uniqueUserIds.length > 0 + ? await workspaceMemberRepository.find({ + where: { userId: In(uniqueUserIds) }, + }) + : []; + + const workspaceMemberIdByUserId = new Map( + workspaceMembers.map((workspaceMember) => [ + workspaceMember.userId, + workspaceMember.id, + ]), + ); + + return entities.map((entity) => { + const userId = entity.userWorkspaceId + ? userIdByUserWorkspaceId.get(entity.userWorkspaceId) + : undefined; + + const accountOwnerId = userId + ? workspaceMemberIdByUserId.get(userId) + : undefined; + + const handleAliases = Array.isArray(entity.handleAliases) + ? entity.handleAliases.join(',') + : (entity.handleAliases ?? ''); + + return { + ...entity, + handleAliases, + accountOwnerId: accountOwnerId ?? null, + } as unknown as ConnectedAccountWorkspaceEntity; + }); + } + + async getWorkspaceRepository(workspaceId: string) { + return this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + } + + async findOne( + workspaceId: string, + options: FindOneOptions, + ): Promise { + if (await this.isMigrated(workspaceId)) { + const where = options.where as Record; + const coreWhere = Array.isArray(where) + ? await Promise.all( + where.map((whereItem: Record) => + this.toCoreWhere(workspaceId, whereItem), + ), + ) + : await this.toCoreWhere(workspaceId, where); + + const coreResult = await this.coreRepository.findOne({ + ...options, + where: coreWhere, + } as FindOneOptions); + + if (!coreResult) { + return null; + } + + const [transformed] = await this.fromCoreEntities(workspaceId, [ + coreResult, + ]); + + return transformed ?? null; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.findOne(options); + } + + async find( + workspaceId: string, + where?: FindOptionsWhere, + ): Promise { + if (await this.isMigrated(workspaceId)) { + const coreWhere = where + ? await this.toCoreWhere(workspaceId, where as Record) + : { workspaceId }; + + const coreResults = await this.coreRepository.find({ + where: coreWhere, + }); + + return this.fromCoreEntities(workspaceId, coreResults); + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.find({ where }); + } + + async save( + workspaceId: string, + data: Partial, + manager?: WorkspaceEntityManager, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.save(data, {}, manager); + + if (await this.isMigrated(workspaceId)) { + try { + const coreData = await this.toCore(workspaceId, data); + + await this.coreRepository.save({ + ...coreData, + workspaceId, + } as ConnectedAccountEntity); + } catch (error) { + this.logger.error( + `Failed to dual-write connectedAccount to core: ${error}`, + ); + throw error; + } + } + } + + async update( + workspaceId: string, + where: FindOptionsWhere, + data: Partial, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.update(where, data); + + if (await this.isMigrated(workspaceId)) { + try { + const coreData = await this.toCore(workspaceId, data); + const coreWhere = await this.toCoreWhere( + workspaceId, + where as Record, + ); + + await this.coreRepository.update( + coreWhere, + coreData as Record, + ); + } catch (error) { + this.logger.error( + `Failed to dual-write connectedAccount update to core: ${error}`, + ); + throw error; + } + } + } + + async delete( + workspaceId: string, + where: FindOptionsWhere, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.delete(where); + + if (await this.isMigrated(workspaceId)) { + try { + const coreWhere = await this.toCoreWhere( + workspaceId, + where as Record, + ); + + await this.coreRepository.delete(coreWhere); + } catch (error) { + this.logger.error( + `Failed to dual-write connectedAccount delete to core: ${error}`, + ); + throw error; + } + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts new file mode 100644 index 00000000000..bf8a785ec09 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts @@ -0,0 +1,90 @@ +import { Field, HideField, ObjectType } from '@nestjs/graphql'; + +import { + IsArray, + IsDateString, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import GraphQLJSON from 'graphql-type-json'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('ConnectedAccountDTO') +export class ConnectedAccountDTO { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @IsString() + @IsNotEmpty() + @Field() + handle: string; + + @IsString() + @IsNotEmpty() + @Field() + provider: string; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + accessToken: string | null; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + refreshToken: string | null; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + lastCredentialsRefreshedAt: Date | null; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + authFailedAt: Date | null; + + @IsArray() + @IsOptional() + @Field(() => [String], { nullable: true }) + handleAliases: string[] | null; + + @IsArray() + @IsOptional() + @Field(() => [String], { nullable: true }) + scopes: string[] | null; + + @IsOptional() + @Field(() => GraphQLJSON, { nullable: true }) + connectionParameters: Record | null; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + lastSignedInAt: Date | null; + + @IsOptional() + @Field(() => GraphQLJSON, { nullable: true }) + oidcTokenClaims: Record | null; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + userWorkspaceId: string; + + @HideField() + workspaceId: string; + + @IsDateString() + @Field() + createdAt: Date; + + @IsDateString() + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/create-connected-account.input.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/create-connected-account.input.ts new file mode 100644 index 00000000000..7562ab7ae08 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/create-connected-account.input.ts @@ -0,0 +1,49 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class CreateConnectedAccountInput { + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + + @IsString() + @IsNotEmpty() + @Field() + handle: string; + + @IsString() + @IsNotEmpty() + @Field() + provider: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + accessToken?: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + refreshToken?: string; + + @IsArray() + @IsOptional() + @Field(() => [String], { nullable: true }) + scopes?: string[]; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + userWorkspaceId: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/update-connected-account.input.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/update-connected-account.input.ts new file mode 100644 index 00000000000..83c93e70451 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/update-connected-account.input.ts @@ -0,0 +1,49 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class UpdateConnectedAccountInputUpdates { + @IsOptional() + @IsString() + @Field({ nullable: true }) + accessToken?: string; + + @IsOptional() + @IsString() + @Field({ nullable: true }) + refreshToken?: string; + + @IsOptional() + @IsArray() + @Field(() => [String], { nullable: true }) + handleAliases?: string[]; + + @IsOptional() + @IsArray() + @Field(() => [String], { nullable: true }) + scopes?: string[]; +} + +@InputType() +export class UpdateConnectedAccountInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @Type(() => UpdateConnectedAccountInputUpdates) + @ValidateNested() + @Field(() => UpdateConnectedAccountInputUpdates) + update: UpdateConnectedAccountInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts new file mode 100644 index 00000000000..17b27697a5b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts @@ -0,0 +1,74 @@ +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryGeneratedColumn, + type Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity'; +import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; + +@Entity({ name: 'connectedAccount', schema: 'core' }) +export class ConnectedAccountEntity extends WorkspaceRelatedEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', nullable: false }) + handle: string; + + @Column({ type: 'varchar', nullable: false }) + provider: string; + + @Column({ type: 'varchar', nullable: true }) + accessToken: string | null; + + @Column({ type: 'varchar', nullable: true }) + refreshToken: string | null; + + @Column({ type: 'timestamptz', nullable: true }) + lastCredentialsRefreshedAt: Date | null; + + @Column({ type: 'timestamptz', nullable: true }) + authFailedAt: Date | null; + + @Column({ type: 'varchar', array: true, nullable: true }) + handleAliases: string[] | null; + + @Column({ type: 'varchar', array: true, nullable: true }) + scopes: string[] | null; + + @Column({ type: 'jsonb', nullable: true }) + connectionParameters: Record | null; + + @Column({ type: 'timestamptz', nullable: true }) + lastSignedInAt: Date | null; + + @Column({ type: 'jsonb', nullable: true }) + oidcTokenClaims: Record | null; + + @Column({ type: 'uuid', nullable: false }) + userWorkspaceId: string; + + @OneToMany( + 'MessageChannelEntity', + (messageChannel: MessageChannelEntity) => messageChannel.connectedAccount, + ) + messageChannels: Relation; + + @OneToMany( + 'CalendarChannelEntity', + (calendarChannel: CalendarChannelEntity) => + calendarChannel.connectedAccount, + ) + calendarChannels: Relation; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor.ts new file mode 100644 index 00000000000..1cc726e4307 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor.ts @@ -0,0 +1,24 @@ +import { + type CallHandler, + type ExecutionContext, + Injectable, + type NestInterceptor, +} from '@nestjs/common'; + +import { type Observable, catchError } from 'rxjs'; + +import { connectedAccountGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/connected-account/utils/connected-account-graphql-api-exception-handler.util'; + +@Injectable() +export class ConnectedAccountGraphqlApiExceptionInterceptor + implements NestInterceptor +{ + intercept( + _context: ExecutionContext, + next: CallHandler, + ): Observable { + return next + .handle() + .pipe(catchError(connectedAccountGraphqlApiExceptionHandler)); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver.ts new file mode 100644 index 00000000000..9a9637f7df2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver.ts @@ -0,0 +1,86 @@ +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 { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; +import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto'; +import { CreateConnectedAccountInput } from 'src/engine/metadata-modules/connected-account/dtos/create-connected-account.input'; +import { UpdateConnectedAccountInput } from 'src/engine/metadata-modules/connected-account/dtos/update-connected-account.input'; +import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor'; + +@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard) +@UseInterceptors(ConnectedAccountGraphqlApiExceptionInterceptor) +@MetadataResolver(() => ConnectedAccountDTO) +export class ConnectedAccountResolver { + constructor( + private readonly connectedAccountMetadataService: ConnectedAccountMetadataService, + ) {} + + @Query(() => [ConnectedAccountDTO]) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async connectedAccounts( + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.connectedAccountMetadataService.findAll(workspace.id); + } + + @Query(() => ConnectedAccountDTO, { nullable: true }) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async connectedAccount( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.connectedAccountMetadataService.findById(id, workspace.id); + } + + @Mutation(() => ConnectedAccountDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async createConnectedAccount( + @Args('input') input: CreateConnectedAccountInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.connectedAccountMetadataService.create({ + ...input, + workspaceId: workspace.id, + }); + } + + @Mutation(() => ConnectedAccountDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async updateConnectedAccount( + @Args('input') input: UpdateConnectedAccountInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.connectedAccountMetadataService.update( + input.id, + workspace.id, + input.update, + ); + } + + @Mutation(() => ConnectedAccountDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async deleteConnectedAccount( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.connectedAccountMetadataService.delete(id, workspace.id); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/utils/connected-account-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/utils/connected-account-graphql-api-exception-handler.util.ts new file mode 100644 index 00000000000..b181c92e3ba --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/utils/connected-account-graphql-api-exception-handler.util.ts @@ -0,0 +1,26 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + ConnectedAccountException, + ConnectedAccountExceptionCode, +} from 'src/engine/metadata-modules/connected-account/connected-account.exception'; + +export const connectedAccountGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof ConnectedAccountException) { + switch (error.code) { + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND: + throw new NotFoundError(error); + case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT: + throw new UserInputError(error); + default: { + return assertUnreachable(error.code); + } + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module.ts new file mode 100644 index 00000000000..73a1ab61c46 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([MessageChannelEntity]), + FeatureFlagModule, + ConnectedAccountDataAccessModule, + ], + providers: [MessageChannelDataAccessService], + exports: [MessageChannelDataAccessService], +}) +export class MessageChannelDataAccessModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service.ts new file mode 100644 index 00000000000..d3e1e82161e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service.ts @@ -0,0 +1,280 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { FeatureFlagKey } from 'twenty-shared/types'; +import { + type FindManyOptions, + type FindOneOptions, + type FindOptionsWhere, + Repository, +} from 'typeorm'; + +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; + +@Injectable() +export class MessageChannelDataAccessService { + private readonly logger = new Logger(MessageChannelDataAccessService.name); + + constructor( + @InjectRepository(MessageChannelEntity) + private readonly coreRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, + ) {} + + private async isMigrated(workspaceId: string): Promise { + return this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + workspaceId, + ); + } + + private async toCoreWhere( + workspaceId: string, + where: Record, + ): Promise> { + const coreWhere: Record = { ...where, workspaceId }; + + if ( + coreWhere.connectedAccount && + typeof coreWhere.connectedAccount === 'object' + ) { + const connectedAccountWhere = { + ...(coreWhere.connectedAccount as Record), + }; + + if ('accountOwnerId' in connectedAccountWhere) { + const { accountOwnerId, ...restConnectedAccount } = + connectedAccountWhere; + + const resolvedConnectedAccounts = + await this.connectedAccountDataAccessService.find(workspaceId, { + accountOwnerId, + } as never); + + if (resolvedConnectedAccounts.length > 0) { + coreWhere.connectedAccountId = resolvedConnectedAccounts[0].id; + } else { + coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000'; + } + + if (Object.keys(restConnectedAccount).length > 0) { + coreWhere.connectedAccount = restConnectedAccount; + } else { + delete coreWhere.connectedAccount; + } + } + } + + return coreWhere; + } + + async getWorkspaceRepository(workspaceId: string) { + return this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + } + + async findOne( + workspaceId: string, + options: FindOneOptions, + ): Promise { + if (await this.isMigrated(workspaceId)) { + const where = options.where as Record; + const coreWhere = Array.isArray(where) + ? await Promise.all( + where.map((whereItem: Record) => + this.toCoreWhere(workspaceId, whereItem), + ), + ) + : await this.toCoreWhere(workspaceId, where); + + return this.coreRepository.findOne({ + ...options, + where: coreWhere, + } as FindOneOptions) as unknown as Promise; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.findOne(options); + } + + async find( + workspaceId: string, + where?: FindOptionsWhere, + ): Promise { + if (await this.isMigrated(workspaceId)) { + return this.coreRepository.find({ + where: { + ...(where as Record), + workspaceId, + } as FindOptionsWhere, + }) as unknown as Promise; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.find({ where }); + } + + async findMany( + workspaceId: string, + options: FindManyOptions, + ): Promise { + if (await this.isMigrated(workspaceId)) { + const baseWhere = options.where; + + if (!baseWhere) { + return this.coreRepository.find({ + ...options, + where: { workspaceId }, + } as FindManyOptions) as unknown as Promise< + MessageChannelWorkspaceEntity[] + >; + } + + if (Array.isArray(baseWhere)) { + const coreWhereArray = await Promise.all( + baseWhere.map((whereItem) => + this.toCoreWhere(workspaceId, whereItem as Record), + ), + ); + + return this.coreRepository.find({ + ...options, + where: coreWhereArray, + } as FindManyOptions) as unknown as Promise< + MessageChannelWorkspaceEntity[] + >; + } + + const coreWhere = await this.toCoreWhere( + workspaceId, + baseWhere as Record, + ); + + return this.coreRepository.find({ + ...options, + where: coreWhere, + } as FindManyOptions) as unknown as Promise< + MessageChannelWorkspaceEntity[] + >; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.find(options); + } + + async save( + workspaceId: string, + data: Partial, + manager?: WorkspaceEntityManager, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.save(data, {}, manager); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.save({ + ...data, + workspaceId, + } as unknown as MessageChannelEntity); + } catch (error) { + this.logger.error( + `Failed to dual-write messageChannel to core: ${error}`, + ); + throw error; + } + } + } + + async update( + workspaceId: string, + where: FindOptionsWhere, + data: Partial, + manager?: WorkspaceEntityManager, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.update(where, data, manager); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.update( + { ...where, workspaceId } as FindOptionsWhere, + data as never, + ); + } catch (error) { + this.logger.error( + `Failed to dual-write messageChannel update to core: ${error}`, + ); + throw error; + } + } + } + + async increment( + workspaceId: string, + where: FindOptionsWhere, + propertyPath: string, + value: number, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.increment(where, propertyPath, value, undefined, [ + propertyPath, + 'id', + ]); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.increment( + { + ...where, + workspaceId, + } as FindOptionsWhere, + propertyPath, + value, + ); + } catch (error) { + this.logger.error( + `Failed to dual-write messageChannel increment to core: ${error}`, + ); + throw error; + } + } + } + + async delete( + workspaceId: string, + where: FindOptionsWhere, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.delete(where); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.delete({ + ...where, + workspaceId, + } as FindOptionsWhere); + } catch (error) { + this.logger.error( + `Failed to dual-write messageChannel delete to core: ${error}`, + ); + throw error; + } + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/create-message-channel.input.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/create-message-channel.input.ts new file mode 100644 index 00000000000..967c54196a7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/create-message-channel.input.ts @@ -0,0 +1,88 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { + MessageChannelContactAutoCreationPolicy, + MessageChannelPendingGroupEmailsAction, + MessageChannelSyncStage, + MessageChannelType, + MessageChannelVisibility, + MessageFolderImportPolicy, +} from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class CreateMessageChannelInput { + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + + @IsString() + @IsNotEmpty() + @Field() + handle: string; + + @IsEnum(MessageChannelVisibility) + @IsNotEmpty() + @Field(() => MessageChannelVisibility) + visibility: MessageChannelVisibility; + + @IsEnum(MessageChannelType) + @IsNotEmpty() + @Field(() => MessageChannelType) + type: MessageChannelType; + + @IsEnum(MessageChannelSyncStage) + @IsNotEmpty() + @Field(() => MessageChannelSyncStage) + syncStage: MessageChannelSyncStage; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + connectedAccountId: string; + + @IsBoolean() + @IsNotEmpty() + @Field() + isContactAutoCreationEnabled: boolean; + + @IsEnum(MessageChannelContactAutoCreationPolicy) + @IsNotEmpty() + @Field(() => MessageChannelContactAutoCreationPolicy) + contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy; + + @IsEnum(MessageFolderImportPolicy) + @IsNotEmpty() + @Field(() => MessageFolderImportPolicy) + messageFolderImportPolicy: MessageFolderImportPolicy; + + @IsBoolean() + @IsNotEmpty() + @Field() + excludeNonProfessionalEmails: boolean; + + @IsBoolean() + @IsNotEmpty() + @Field() + excludeGroupEmails: boolean; + + @IsEnum(MessageChannelPendingGroupEmailsAction) + @IsNotEmpty() + @Field(() => MessageChannelPendingGroupEmailsAction) + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction; + + @IsBoolean() + @IsNotEmpty() + @Field() + isSyncEnabled: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/message-channel.dto.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/message-channel.dto.ts new file mode 100644 index 00000000000..bfd51263913 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/message-channel.dto.ts @@ -0,0 +1,127 @@ +import { Field, HideField, ObjectType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsDateString, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { + MessageChannelContactAutoCreationPolicy, + MessageChannelPendingGroupEmailsAction, + MessageChannelSyncStage, + MessageChannelSyncStatus, + MessageChannelType, + MessageChannelVisibility, + MessageFolderImportPolicy, +} from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('MessageChannel') +export class MessageChannelDTO { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @IsEnum(MessageChannelVisibility) + @IsNotEmpty() + @Field(() => MessageChannelVisibility) + visibility: MessageChannelVisibility; + + @IsString() + @IsNotEmpty() + @Field() + handle: string; + + @IsEnum(MessageChannelType) + @IsNotEmpty() + @Field(() => MessageChannelType) + type: MessageChannelType; + + @IsBoolean() + @Field() + isContactAutoCreationEnabled: boolean; + + @IsEnum(MessageChannelContactAutoCreationPolicy) + @IsNotEmpty() + @Field(() => MessageChannelContactAutoCreationPolicy) + contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy; + + @IsEnum(MessageFolderImportPolicy) + @IsNotEmpty() + @Field(() => MessageFolderImportPolicy) + messageFolderImportPolicy: MessageFolderImportPolicy; + + @IsBoolean() + @Field() + excludeNonProfessionalEmails: boolean; + + @IsBoolean() + @Field() + excludeGroupEmails: boolean; + + @IsEnum(MessageChannelPendingGroupEmailsAction) + @IsNotEmpty() + @Field(() => MessageChannelPendingGroupEmailsAction) + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction; + + @IsBoolean() + @Field() + isSyncEnabled: boolean; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + syncCursor: string | null; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + syncedAt: Date | null; + + @IsEnum(MessageChannelSyncStatus) + @IsNotEmpty() + @Field(() => MessageChannelSyncStatus) + syncStatus: MessageChannelSyncStatus; + + @IsEnum(MessageChannelSyncStage) + @IsNotEmpty() + @Field(() => MessageChannelSyncStage) + syncStage: MessageChannelSyncStage; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + syncStageStartedAt: Date | null; + + @IsInt() + @Field() + throttleFailureCount: number; + + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + throttleRetryAfter: Date | null; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + connectedAccountId: string; + + @HideField() + workspaceId: string; + + @IsDateString() + @Field() + createdAt: Date; + + @IsDateString() + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/update-message-channel.input.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/update-message-channel.input.ts new file mode 100644 index 00000000000..32fe9fde311 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/dtos/update-message-channel.input.ts @@ -0,0 +1,69 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsUUID, + ValidateNested, +} from 'class-validator'; +import { + MessageChannelContactAutoCreationPolicy, + MessageChannelVisibility, + MessageFolderImportPolicy, +} from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class UpdateMessageChannelInputUpdates { + @IsOptional() + @IsEnum(MessageChannelVisibility) + @Field(() => MessageChannelVisibility, { nullable: true }) + visibility?: MessageChannelVisibility; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + isContactAutoCreationEnabled?: boolean; + + @IsOptional() + @IsEnum(MessageChannelContactAutoCreationPolicy) + @Field(() => MessageChannelContactAutoCreationPolicy, { nullable: true }) + contactAutoCreationPolicy?: MessageChannelContactAutoCreationPolicy; + + @IsOptional() + @IsEnum(MessageFolderImportPolicy) + @Field(() => MessageFolderImportPolicy, { nullable: true }) + messageFolderImportPolicy?: MessageFolderImportPolicy; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + isSyncEnabled?: boolean; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + excludeNonProfessionalEmails?: boolean; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + excludeGroupEmails?: boolean; +} + +@InputType() +export class UpdateMessageChannelInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @Type(() => UpdateMessageChannelInputUpdates) + @ValidateNested() + @Field(() => UpdateMessageChannelInputUpdates) + update: UpdateMessageChannelInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts new file mode 100644 index 00000000000..6782c57e250 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts @@ -0,0 +1,134 @@ +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; + + @OneToMany( + 'MessageFolderEntity', + (messageFolder: MessageFolderEntity) => messageFolder.messageChannel, + ) + messageFolders: Relation; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor.ts new file mode 100644 index 00000000000..eb49eb21972 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor.ts @@ -0,0 +1,24 @@ +import { + type CallHandler, + type ExecutionContext, + Injectable, + type NestInterceptor, +} from '@nestjs/common'; + +import { type Observable, catchError } from 'rxjs'; + +import { messageChannelGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/message-channel/utils/message-channel-graphql-api-exception-handler.util'; + +@Injectable() +export class MessageChannelGraphqlApiExceptionInterceptor + implements NestInterceptor +{ + intercept( + _context: ExecutionContext, + next: CallHandler, + ): Observable { + return next + .handle() + .pipe(catchError(messageChannelGraphqlApiExceptionHandler)); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel-metadata.module.ts new file mode 100644 index 00000000000..1defa96a44d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel-metadata.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor'; +import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service'; +import { MessageChannelResolver } from 'src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([MessageChannelEntity]), + AuthModule, + PermissionsModule, + FeatureFlagModule, + ], + providers: [ + MessageChannelMetadataService, + MessageChannelResolver, + MessageChannelGraphqlApiExceptionInterceptor, + ], + exports: [MessageChannelMetadataService], +}) +export class MessageChannelMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel-metadata.service.ts new file mode 100644 index 00000000000..da07a973929 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel-metadata.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { + MessageChannelSyncStage, + MessageChannelType, + MessageChannelVisibility, +} from 'twenty-shared/types'; + +import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto'; +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; + +@Injectable() +export class MessageChannelMetadataService { + constructor( + @InjectRepository(MessageChannelEntity) + private readonly repository: Repository, + ) {} + + async findAll(workspaceId: string): Promise { + return this.repository.find({ where: { workspaceId } }); + } + + async findByConnectedAccountId( + connectedAccountId: string, + workspaceId: string, + ): Promise { + return this.repository.find({ + where: { connectedAccountId, workspaceId }, + }); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + return this.repository.findOne({ where: { id, workspaceId } }); + } + + async create( + data: Partial & { + workspaceId: string; + handle: string; + connectedAccountId: string; + visibility: MessageChannelVisibility; + type: MessageChannelType; + syncStage: MessageChannelSyncStage; + }, + ): Promise { + const entity = this.repository.create(data); + + return this.repository.save(entity); + } + + async update( + id: string, + workspaceId: string, + data: Partial, + ): Promise { + await this.repository.update( + { id, workspaceId }, + data as Record, + ); + + return this.repository.findOneOrFail({ where: { id, workspaceId } }); + } + + async delete(id: string, workspaceId: string): Promise { + const entity = await this.repository.findOneOrFail({ + where: { id, workspaceId }, + }); + + await this.repository.delete({ id, workspaceId }); + + return entity; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel.exception.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel.exception.ts new file mode 100644 index 00000000000..17d96d17434 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/message-channel.exception.ts @@ -0,0 +1,37 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { CustomException } from 'src/utils/custom-exception'; + +export enum MessageChannelExceptionCode { + MESSAGE_CHANNEL_NOT_FOUND = 'MESSAGE_CHANNEL_NOT_FOUND', + INVALID_MESSAGE_CHANNEL_INPUT = 'INVALID_MESSAGE_CHANNEL_INPUT', +} + +const getMessageChannelExceptionUserFriendlyMessage = ( + code: MessageChannelExceptionCode, +) => { + switch (code) { + case MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND: + return msg`Message channel not found.`; + case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT: + return msg`Invalid message channel input.`; + default: + assertUnreachable(code); + } +}; + +export class MessageChannelException extends CustomException { + constructor( + message: string, + code: MessageChannelExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? + getMessageChannelExceptionUserFriendlyMessage(code), + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver.ts new file mode 100644 index 00000000000..e54273daf14 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver.ts @@ -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 { CreateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-message-channel.input'; +import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto'; +import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input'; +import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor'; +import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service'; + +@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard) +@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor) +@MetadataResolver(() => MessageChannelDTO) +export class MessageChannelResolver { + constructor( + private readonly messageChannelMetadataService: MessageChannelMetadataService, + ) {} + + @Query(() => [MessageChannelDTO]) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async messageChannels( + @AuthWorkspace() workspace: WorkspaceEntity, + @Args('connectedAccountId', { + type: () => UUIDScalarType, + nullable: true, + }) + connectedAccountId?: string, + ): Promise { + if (connectedAccountId) { + return this.messageChannelMetadataService.findByConnectedAccountId( + connectedAccountId, + workspace.id, + ); + } + + return this.messageChannelMetadataService.findAll(workspace.id); + } + + @Query(() => MessageChannelDTO, { nullable: true }) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async messageChannel( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.messageChannelMetadataService.findById(id, workspace.id); + } + + @Mutation(() => MessageChannelDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async createMessageChannel( + @Args('input') input: CreateMessageChannelInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.messageChannelMetadataService.create({ + ...input, + workspaceId: workspace.id, + }); + } + + @Mutation(() => MessageChannelDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async updateMessageChannel( + @Args('input') input: UpdateMessageChannelInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.messageChannelMetadataService.update( + input.id, + workspace.id, + input.update, + ); + } + + @Mutation(() => MessageChannelDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async deleteMessageChannel( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.messageChannelMetadataService.delete(id, workspace.id); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/utils/message-channel-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/utils/message-channel-graphql-api-exception-handler.util.ts new file mode 100644 index 00000000000..5f50e2647c2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/utils/message-channel-graphql-api-exception-handler.util.ts @@ -0,0 +1,26 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + MessageChannelException, + MessageChannelExceptionCode, +} from 'src/engine/metadata-modules/message-channel/message-channel.exception'; + +export const messageChannelGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof MessageChannelException) { + switch (error.code) { + case MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND: + throw new NotFoundError(error); + case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT: + throw new UserInputError(error); + default: { + return assertUnreachable(error.code); + } + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module.ts new file mode 100644 index 00000000000..a950922ff5d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([MessageFolderEntity]), FeatureFlagModule], + providers: [MessageFolderDataAccessService], + exports: [MessageFolderDataAccessService], +}) +export class MessageFolderDataAccessModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service.ts new file mode 100644 index 00000000000..83154b2dbdd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service.ts @@ -0,0 +1,157 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { FeatureFlagKey } from 'twenty-shared/types'; +import { + type FindOneOptions, + type FindOptionsWhere, + Repository, +} from 'typeorm'; + +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; + +@Injectable() +export class MessageFolderDataAccessService { + private readonly logger = new Logger(MessageFolderDataAccessService.name); + + constructor( + @InjectRepository(MessageFolderEntity) + private readonly coreRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + ) {} + + private async isMigrated(workspaceId: string): Promise { + return this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + workspaceId, + ); + } + + async getWorkspaceRepository(workspaceId: string) { + return this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageFolder', + ); + } + + async findOne( + workspaceId: string, + options: FindOneOptions, + ): Promise { + if (await this.isMigrated(workspaceId)) { + const where = options.where as Record; + const coreWhere = Array.isArray(where) + ? where.map((whereItem) => ({ + ...(whereItem as Record), + workspaceId, + })) + : { + ...(where as Record), + workspaceId, + }; + + return this.coreRepository.findOne({ + ...options, + where: coreWhere, + } as FindOneOptions) as unknown as Promise; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.findOne(options); + } + + async find( + workspaceId: string, + where?: FindOptionsWhere, + ): Promise { + if (await this.isMigrated(workspaceId)) { + return this.coreRepository.find({ + where: { + ...(where as Record), + workspaceId, + } as FindOptionsWhere, + }) as unknown as Promise; + } + + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + return workspaceRepository.find({ where }); + } + + async save( + workspaceId: string, + data: Partial, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.save(data); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.save({ + ...data, + workspaceId, + } as unknown as MessageFolderEntity); + } catch (error) { + this.logger.error( + `Failed to dual-write messageFolder to core: ${error}`, + ); + throw error; + } + } + } + + async update( + workspaceId: string, + where: FindOptionsWhere, + data: Partial, + manager?: WorkspaceEntityManager, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.update(where, data, manager); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.update( + { ...where, workspaceId } as FindOptionsWhere, + data as Record, + ); + } catch (error) { + this.logger.error( + `Failed to dual-write messageFolder update to core: ${error}`, + ); + throw error; + } + } + } + + async delete( + workspaceId: string, + where: FindOptionsWhere, + ): Promise { + const workspaceRepository = await this.getWorkspaceRepository(workspaceId); + + await workspaceRepository.delete(where); + + if (await this.isMigrated(workspaceId)) { + try { + await this.coreRepository.delete({ + ...where, + workspaceId, + } as FindOptionsWhere); + } catch (error) { + this.logger.error( + `Failed to dual-write messageFolder delete to core: ${error}`, + ); + throw error; + } + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/create-message-folder.input.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/create-message-folder.input.ts new file mode 100644 index 00000000000..2f1bf605dc6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/create-message-folder.input.ts @@ -0,0 +1,56 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class CreateMessageFolderInput { + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + name?: string; + + @IsBoolean() + @IsNotEmpty() + @Field() + isSentFolder: boolean; + + @IsBoolean() + @IsNotEmpty() + @Field() + isSynced: boolean; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + externalId?: string; + + @IsEnum(MessageFolderPendingSyncAction) + @IsNotEmpty() + @Field(() => MessageFolderPendingSyncAction) + pendingSyncAction: MessageFolderPendingSyncAction; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + messageChannelId: string; + + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + parentFolderId?: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/message-folder.dto.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/message-folder.dto.ts new file mode 100644 index 00000000000..c4137572e7c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/message-folder.dto.ts @@ -0,0 +1,71 @@ +import { Field, HideField, ObjectType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsDateString, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('MessageFolder') +export class MessageFolderDTO { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + name: string | null; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + syncCursor: string | null; + + @IsBoolean() + @Field() + isSentFolder: boolean; + + @IsBoolean() + @Field() + isSynced: boolean; + + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + parentFolderId: string | null; + + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + externalId: string | null; + + @IsEnum(MessageFolderPendingSyncAction) + @IsNotEmpty() + @Field(() => MessageFolderPendingSyncAction) + pendingSyncAction: MessageFolderPendingSyncAction; + + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + messageChannelId: string; + + @HideField() + workspaceId: string; + + @IsDateString() + @Field() + createdAt: Date; + + @IsDateString() + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts new file mode 100644 index 00000000000..eff88aa6ffb --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts @@ -0,0 +1,51 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from 'class-validator'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class UpdateMessageFolderInputUpdates { + @IsOptional() + @IsString() + @Field({ nullable: true }) + name?: string; + + @IsOptional() + @IsString() + @Field({ nullable: true }) + syncCursor?: string; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + isSynced?: boolean; + + @IsOptional() + @IsEnum(MessageFolderPendingSyncAction) + @Field(() => MessageFolderPendingSyncAction, { nullable: true }) + pendingSyncAction?: MessageFolderPendingSyncAction; +} + +@InputType() +export class UpdateMessageFolderInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @Type(() => UpdateMessageFolderInputUpdates) + @ValidateNested() + @Field(() => UpdateMessageFolderInputUpdates) + update: UpdateMessageFolderInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/entities/message-folder.entity.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/entities/message-folder.entity.ts new file mode 100644 index 00000000000..744220b2195 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/entities/message-folder.entity.ts @@ -0,0 +1,63 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; + +import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; + +@Entity({ name: 'messageFolder', schema: 'core' }) +export class MessageFolderEntity extends WorkspaceRelatedEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', nullable: true }) + name: string | null; + + @Column({ type: 'varchar', nullable: true }) + syncCursor: string | null; + + @Column({ type: 'boolean', nullable: false }) + isSentFolder: boolean; + + @Column({ type: 'boolean', nullable: false }) + isSynced: boolean; + + @Column({ type: 'uuid', nullable: true }) + parentFolderId: string | null; + + @Column({ type: 'varchar', nullable: true }) + externalId: string | null; + + @Column({ + type: 'enum', + enum: MessageFolderPendingSyncAction, + nullable: false, + }) + pendingSyncAction: MessageFolderPendingSyncAction; + + @Column({ type: 'uuid', nullable: false }) + messageChannelId: string; + + @ManyToOne( + () => MessageChannelEntity, + (messageChannel) => messageChannel.messageFolders, + { onDelete: 'CASCADE' }, + ) + @JoinColumn({ name: 'messageChannelId' }) + messageChannel: Relation; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor.ts new file mode 100644 index 00000000000..96fc3cd49f8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor.ts @@ -0,0 +1,24 @@ +import { + type CallHandler, + type ExecutionContext, + Injectable, + type NestInterceptor, +} from '@nestjs/common'; + +import { type Observable, catchError } from 'rxjs'; + +import { messageFolderGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/message-folder/utils/message-folder-graphql-api-exception-handler.util'; + +@Injectable() +export class MessageFolderGraphqlApiExceptionInterceptor + implements NestInterceptor +{ + intercept( + _context: ExecutionContext, + next: CallHandler, + ): Observable { + return next + .handle() + .pipe(catchError(messageFolderGraphqlApiExceptionHandler)); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.module.ts new file mode 100644 index 00000000000..68a46df838f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; +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'; +import { MessageFolderResolver } from 'src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([MessageFolderEntity]), + AuthModule, + PermissionsModule, + FeatureFlagModule, + ], + providers: [ + MessageFolderMetadataService, + MessageFolderResolver, + MessageFolderGraphqlApiExceptionInterceptor, + ], + exports: [MessageFolderMetadataService], +}) +export class MessageFolderMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts new file mode 100644 index 00000000000..c10350d3c2e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; + +import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; + +@Injectable() +export class MessageFolderMetadataService { + constructor( + @InjectRepository(MessageFolderEntity) + private readonly repository: Repository, + ) {} + + async findAll(workspaceId: string): Promise { + return this.repository.find({ where: { workspaceId } }); + } + + async findByMessageChannelId( + messageChannelId: string, + workspaceId: string, + ): Promise { + return this.repository.find({ + where: { messageChannelId, workspaceId }, + }); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + return this.repository.findOne({ where: { id, workspaceId } }); + } + + async create( + data: Partial & { + workspaceId: string; + messageChannelId: string; + pendingSyncAction: MessageFolderPendingSyncAction; + }, + ): Promise { + const entity = this.repository.create(data); + + return this.repository.save(entity); + } + + async update( + id: string, + workspaceId: string, + data: Partial, + ): Promise { + await this.repository.update( + { id, workspaceId }, + data as Record, + ); + + return this.repository.findOneOrFail({ where: { id, workspaceId } }); + } + + async delete(id: string, workspaceId: string): Promise { + const entity = await this.repository.findOneOrFail({ + where: { id, workspaceId }, + }); + + await this.repository.delete({ id, workspaceId }); + + return entity; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder.exception.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder.exception.ts new file mode 100644 index 00000000000..826152d5191 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder.exception.ts @@ -0,0 +1,37 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { CustomException } from 'src/utils/custom-exception'; + +export enum MessageFolderExceptionCode { + MESSAGE_FOLDER_NOT_FOUND = 'MESSAGE_FOLDER_NOT_FOUND', + INVALID_MESSAGE_FOLDER_INPUT = 'INVALID_MESSAGE_FOLDER_INPUT', +} + +const getMessageFolderExceptionUserFriendlyMessage = ( + code: MessageFolderExceptionCode, +) => { + switch (code) { + case MessageFolderExceptionCode.MESSAGE_FOLDER_NOT_FOUND: + return msg`Message folder not found.`; + case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT: + return msg`Invalid message folder input.`; + default: + assertUnreachable(code); + } +}; + +export class MessageFolderException extends CustomException { + constructor( + message: string, + code: MessageFolderExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? + getMessageFolderExceptionUserFriendlyMessage(code), + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts new file mode 100644 index 00000000000..d495d0b1d0b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + return this.messageFolderMetadataService.delete(id, workspace.id); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/utils/message-folder-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/utils/message-folder-graphql-api-exception-handler.util.ts new file mode 100644 index 00000000000..7a4f3c04777 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/utils/message-folder-graphql-api-exception-handler.util.ts @@ -0,0 +1,26 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + MessageFolderException, + MessageFolderExceptionCode, +} from 'src/engine/metadata-modules/message-folder/message-folder.exception'; + +export const messageFolderGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof MessageFolderException) { + switch (error.code) { + case MessageFolderExceptionCode.MESSAGE_FOLDER_NOT_FOUND: + throw new NotFoundError(error); + case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT: + throw new UserInputError(error); + default: { + return assertUnreachable(error.code); + } + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts index ed51770b286..b866a11b571 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts @@ -5,6 +5,8 @@ import { AiAgentMonitorModule } from 'src/engine/metadata-modules/ai/ai-agent-mo import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module'; import { AiGenerateTextModule } from 'src/engine/metadata-modules/ai/ai-generate-text/ai-generate-text.module'; +import { CalendarChannelMetadataModule } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module'; +import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module'; import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; @@ -12,6 +14,8 @@ import { FlatEntityMapsGraphqlApiExceptionFilter } from 'src/engine/metadata-mod import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module'; import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module'; import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module'; +import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.module'; +import { MessageFolderMetadataModule } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; @@ -47,6 +51,10 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work PermissionsModule, RouteTriggerModule, WebhookModule, + ConnectedAccountMetadataModule, + MessageChannelMetadataModule, + CalendarChannelMetadataModule, + MessageFolderMetadataModule, ], providers: [ { @@ -71,6 +79,10 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work RoleModule, PermissionsModule, WebhookModule, + ConnectedAccountMetadataModule, + MessageChannelMetadataModule, + CalendarChannelMetadataModule, + MessageFolderMetadataModule, ], }) export class MetadataEngineModule {} diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 922075d682d..28297aa9efa 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -250,6 +250,7 @@ describe('WorkspaceEntityManager', () => { IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED: false, IS_DRAFT_EMAIL_ENABLED: false, IS_RICH_TEXT_V1_MIGRATED: false, + IS_CONNECTED_ACCOUNT_MIGRATED: false, }, userWorkspaceRoleMap: {}, eventEmitterService: { diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/calendar-blocklist-manager.module.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/calendar-blocklist-manager.module.ts index 8f1224d5a5a..a58879f7d31 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/calendar-blocklist-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/calendar-blocklist-manager.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module'; import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job'; import { BlocklistReimportCalendarEventsJob } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job'; import { CalendarBlocklistListener } from 'src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener'; @@ -7,7 +8,11 @@ import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event- import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-common.module'; @Module({ - imports: [CalendarEventCleanerModule, CalendarCommonModule], + imports: [ + CalendarEventCleanerModule, + CalendarCommonModule, + CalendarChannelDataAccessModule, + ], providers: [ CalendarBlocklistListener, BlocklistItemDeleteCalendarEventsJob, diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts index e64e15ff7b4..f06df455cdd 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts @@ -7,13 +7,13 @@ import { type ObjectRecordCreateEvent } 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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service'; import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; -import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; export type BlocklistItemDeleteCalendarEventsJobData = WorkspaceEventBatch< ObjectRecordCreateEvent @@ -26,6 +26,7 @@ export type BlocklistItemDeleteCalendarEventsJobData = WorkspaceEventBatch< export class BlocklistItemDeleteCalendarEventsJob { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, private readonly calendarEventCleanerService: CalendarEventCleanerService, ) {} @@ -71,12 +72,6 @@ export class BlocklistItemDeleteCalendarEventsJob { new Map(), ); - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - const calendarChannelEventAssociationRepository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, @@ -91,29 +86,35 @@ export class BlocklistItemDeleteCalendarEventsJob { continue; } - const calendarChannels = await calendarChannelRepository.find({ - select: { - id: true, - handle: true, - connectedAccount: { - handleAliases: true, + const calendarChannels = + await this.calendarChannelDataAccessService.find(workspaceId, { + select: { + id: true, + handle: true, + connectedAccount: { + handleAliases: true, + }, }, - }, - where: { - connectedAccount: { - accountOwnerId: workspaceMemberId, + where: { + connectedAccount: { + accountOwnerId: workspaceMemberId, + }, }, - }, - relations: ['connectedAccount'], - }); + relations: ['connectedAccount'], + }); for (const calendarChannel of calendarChannels) { const calendarChannelHandles = [calendarChannel.handle]; if (calendarChannel.connectedAccount.handleAliases) { - calendarChannelHandles.push( - ...calendarChannel.connectedAccount.handleAliases.split(','), - ); + const rawAliases = calendarChannel.connectedAccount + .handleAliases as string | string[]; + + const aliasList = Array.isArray(rawAliases) + ? rawAliases + : rawAliases.split(',').map((alias: string) => alias.trim()); + + calendarChannelHandles.push(...aliasList); } const handleConditions = handles.map((handle) => { diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts index 9c6a77dc1c9..a0c81fea2be 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts @@ -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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; -import { - CalendarChannelSyncStage, - type CalendarChannelWorkspaceEntity, -} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch< ObjectRecordDeleteEvent @@ -27,6 +25,7 @@ export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch< export class BlocklistReimportCalendarEventsJob { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, ) {} @@ -37,27 +36,22 @@ export class BlocklistReimportCalendarEventsJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - for (const eventPayload of data.events) { const workspaceMemberId = eventPayload.properties.before.workspaceMemberId; - const calendarChannels = await calendarChannelRepository.find({ - select: ['id'], - where: { - connectedAccount: { - accountOwnerId: workspaceMemberId, + const calendarChannels = + await this.calendarChannelDataAccessService.find(workspaceId, { + select: ['id'], + where: { + connectedAccount: { + accountOwnerId: workspaceMemberId, + }, + syncStage: Not( + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + ), }, - syncStage: Not( - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - ), - }, - }); + }); await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( calendarChannels.map((calendarChannel) => calendarChannel.id), diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index 72372a7d3ef..8632e437700 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -6,6 +6,8 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; @@ -57,6 +59,8 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres ConnectedAccountModule, CalendarCommonModule, MetricsModule, + CalendarChannelDataAccessModule, + ConnectedAccountDataAccessModule, ], providers: [ CalendarAccountAuthenticationService, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/commands/calendar-trigger-event-list-fetch.command.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/commands/calendar-trigger-event-list-fetch.command.ts index 98ebe02d1f9..6674b94e1f3 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/commands/calendar-trigger-event-list-fetch.command.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/commands/calendar-trigger-event-list-fetch.command.ts @@ -5,16 +5,14 @@ import { Command, CommandRunner, Option } from 'nest-commander'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { CalendarEventListFetchJob, type CalendarEventListFetchJobData, } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; -import { - CalendarChannelSyncStage, - type CalendarChannelWorkspaceEntity, -} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; type CalendarTriggerEventListFetchCommandOptions = { workspaceId: string; @@ -33,6 +31,7 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, @InjectMessageQueue(MessageQueue.calendarQueue) private readonly messageQueueService: MessageQueueService, ) { @@ -52,23 +51,17 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const whereCondition: Record = { - isSyncEnabled: true, - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - }; - - if (calendarChannelId) { - whereCondition.id = calendarChannelId; - } - - const calendarChannels = - await calendarChannelRepository.find(whereCondition); + const calendarChannels = await this.calendarChannelDataAccessService.find( + workspaceId, + { + where: { + isSyncEnabled: true, + syncStage: + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + ...(calendarChannelId ? { id: calendarChannelId } : {}), + }, + }, + ); if (calendarChannels.length === 0) { this.logger.warn( @@ -83,11 +76,15 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner { ); for (const calendarChannel of calendarChannels) { - await calendarChannelRepository.update(calendarChannel.id, { - syncStage: - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, - syncStageStartedAt: new Date().toISOString(), - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: calendarChannel.id }, + { + syncStage: + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, + syncStageStartedAt: new Date().toISOString(), + }, + ); await this.messageQueueService.add( CalendarEventListFetchJob.name, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts index e66e46d9d9f..c51b0491c18 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts @@ -3,6 +3,7 @@ import { Scope } from '@nestjs/common'; 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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service'; @@ -11,6 +12,7 @@ import { CalendarChannelSyncStage, type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; export type CalendarEventListFetchJobData = { @@ -25,6 +27,7 @@ export type CalendarEventListFetchJobData = { export class CalendarEventListFetchJob { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly calendarFetchEventsService: CalendarFetchEventsService, ) {} @@ -36,19 +39,14 @@ export class CalendarEventListFetchJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const calendarChannel = await calendarChannelRepository.findOne({ - where: { - id: calendarChannelId, - isSyncEnabled: true, - }, - relations: ['connectedAccount'], - }); + const calendarChannel = + await this.calendarChannelDataAccessService.findOne(workspaceId, { + where: { + id: calendarChannelId, + isSyncEnabled: true, + }, + relations: ['connectedAccount'], + }); if (!calendarChannel) { return; @@ -61,11 +59,10 @@ export class CalendarEventListFetchJob { return; } + const syncStageStartedAt = calendarChannel.syncStageStartedAt; + if ( - isThrottled( - calendarChannel.syncStageStartedAt, - calendarChannel.throttleFailureCount, - ) + isThrottled(syncStageStartedAt, calendarChannel.throttleFailureCount) ) { await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( [calendarChannel.id], @@ -77,8 +74,8 @@ export class CalendarEventListFetchJob { } await this.calendarFetchEventsService.fetchCalendarEvents( - calendarChannel, - calendarChannel.connectedAccount, + calendarChannel as unknown as CalendarChannelWorkspaceEntity, + calendarChannel.connectedAccount as unknown as ConnectedAccountWorkspaceEntity, workspaceId, ); }, authContext); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts index c9aaa509e3c..f199309670f 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts @@ -3,6 +3,7 @@ import { Scope } from '@nestjs/common'; 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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; @@ -11,6 +12,7 @@ import { CalendarChannelSyncStage, type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; export type CalendarEventsImportJobData = { @@ -27,6 +29,7 @@ export class CalendarEventsImportJob { private readonly calendarEventsImportService: CalendarEventsImportService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, ) {} @Process(CalendarEventsImportJob.name) @@ -36,18 +39,14 @@ export class CalendarEventsImportJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - const calendarChannel = await calendarChannelRepository.findOne({ - where: { - id: calendarChannelId, - isSyncEnabled: true, - }, - relations: ['connectedAccount'], - }); + const calendarChannel = + await this.calendarChannelDataAccessService.findOne(workspaceId, { + where: { + id: calendarChannelId, + isSyncEnabled: true, + }, + relations: ['connectedAccount'], + }); if (!calendarChannel?.isSyncEnabled) { return; @@ -60,11 +59,10 @@ export class CalendarEventsImportJob { return; } + const syncStageStartedAt = calendarChannel.syncStageStartedAt; + if ( - isThrottled( - calendarChannel.syncStageStartedAt, - calendarChannel.throttleFailureCount, - ) + isThrottled(syncStageStartedAt, calendarChannel.throttleFailureCount) ) { await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( [calendarChannel.id], @@ -76,8 +74,8 @@ export class CalendarEventsImportJob { } await this.calendarEventsImportService.processCalendarEventsImport( - calendarChannel, - calendarChannel.connectedAccount, + calendarChannel as unknown as CalendarChannelWorkspaceEntity, + calendarChannel.connectedAccount as unknown as ConnectedAccountWorkspaceEntity, workspaceId, ); }, authContext); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts index 887e6aa200e..62b6844419c 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts @@ -5,14 +5,12 @@ import { In } from 'typeorm'; 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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { isSyncStale } from 'src/modules/calendar/calendar-event-import-manager/utils/is-sync-stale.util'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; -import { - CalendarChannelSyncStage, - type CalendarChannelWorkspaceEntity, -} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; export type CalendarOngoingStaleJobData = { workspaceId: string; @@ -26,6 +24,7 @@ export class CalendarOngoingStaleJob { private readonly logger = new Logger(CalendarOngoingStaleJob.name); constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, ) {} @@ -36,25 +35,24 @@ export class CalendarOngoingStaleJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const calendarChannels = await calendarChannelRepository.find({ - where: { - syncStage: In([ - CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, - CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED, - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, - ]), + const calendarChannels = await this.calendarChannelDataAccessService.find( + workspaceId, + { + where: { + syncStage: In([ + CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED, + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, + ]), + }, }, - }); + ); for (const calendarChannel of calendarChannels) { - if (isSyncStale(calendarChannel.syncStageStartedAt)) { + const syncStageStartedAt = calendarChannel.syncStageStartedAt; + + if (isSyncStale(syncStageStartedAt)) { await this.calendarChannelSyncStatusService.resetSyncStageStartedAt( [calendarChannel.id], workspaceId, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts index 72a5e697448..0aceb7b5180 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts @@ -3,12 +3,12 @@ import { Scope } from '@nestjs/common'; 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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { CalendarChannelSyncStage, CalendarChannelSyncStatus, - CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; export type CalendarRelaunchFailedCalendarChannelJobData = { @@ -23,6 +23,7 @@ export type CalendarRelaunchFailedCalendarChannelJobData = { export class CalendarRelaunchFailedCalendarChannelJob { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, ) {} @Process(CalendarRelaunchFailedCalendarChannelJob.name) @@ -32,23 +33,12 @@ export class CalendarRelaunchFailedCalendarChannelJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - { shouldBypassPermissionChecks: true }, - ); - - const calendarChannel = await calendarChannelRepository.findOne({ - where: { - id: calendarChannelId, - }, - relations: { - connectedAccount: { - accountOwner: true, + const calendarChannel = + await this.calendarChannelDataAccessService.findOne(workspaceId, { + where: { + id: calendarChannelId, }, - }, - }); + }); if ( !calendarChannel || @@ -58,10 +48,14 @@ export class CalendarRelaunchFailedCalendarChannelJob { return; } - await calendarChannelRepository.update(calendarChannelId, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - syncStatus: CalendarChannelSyncStatus.ACTIVE, - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: calendarChannelId }, + { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + syncStatus: CalendarChannelSyncStatus.ACTIVE, + }, + ); }, authContext); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts index f870f8fa01e..311020b4bdd 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts @@ -1,12 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; import { type TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; -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 { CALENDAR_THROTTLE_MAX_ATTEMPTS } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-throttle-max-attempts'; import { type CalendarEventImportDriverException, @@ -29,7 +28,7 @@ export class CalendarEventImportErrorHandlerService { CalendarEventImportErrorHandlerService.name, ); constructor( - private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -133,25 +132,14 @@ export class CalendarEventImportErrorHandlerService { throw calendarEventImportException; } - const authContext = buildSystemAuthContext(workspaceId); - - await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.increment( - { - id: calendarChannel.id, - }, - 'throttleFailureCount', - 1, - undefined, - ['throttleFailureCount', 'id'], - ); - }, authContext); + await this.calendarChannelDataAccessService.increment( + workspaceId, + { + id: calendarChannel.id, + }, + 'throttleFailureCount', + 1, + ); switch (syncStep) { case CalendarEventImportSyncStep.CALENDAR_EVENT_LIST_FETCH: diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts index 45076235c80..a441eedc144 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts @@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-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 { @@ -29,6 +30,7 @@ export class CalendarFetchEventsService { @InjectCacheStorage(CacheStorageNamespace.ModuleCalendar) private readonly cacheStorage: CacheStorageService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly getCalendarEventsService: CalendarGetCalendarEventsService, private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService, @@ -90,14 +92,9 @@ export class CalendarFetchEventsService { const calendarEventIds = getCalendarEventsResponse.calendarEventIds; const nextSyncCursor = getCalendarEventsResponse.nextSyncCursor; - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - if (!calendarEvents || calendarEvents?.length === 0) { - await calendarChannelRepository.update( + await this.calendarChannelDataAccessService.update( + workspaceId, { id: calendarChannel.id, }, @@ -112,7 +109,8 @@ export class CalendarFetchEventsService { ); } - await calendarChannelRepository.update( + await this.calendarChannelDataAccessService.update( + workspaceId, { id: calendarChannel.id, }, diff --git a/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts b/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts index eb69a30aca0..2a1659115b7 100644 --- a/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts +++ b/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts @@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; +import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; @@ -12,6 +14,8 @@ import { ConnectedAccountModule } from 'src/modules/connected-account/connected- WorkspaceDataSourceModule, TypeOrmModule.forFeature([FeatureFlagEntity]), ConnectedAccountModule, + CalendarChannelDataAccessModule, + ConnectedAccountDataAccessModule, MetricsModule, ], providers: [CalendarChannelSyncStatusService], diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts index 59b2379b733..61d11dde3d1 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts @@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants'; +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 { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; @@ -44,22 +45,19 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { find: jest.fn(), }; - const mockConnectedAccountRepository = { - find: jest.fn(), - }; - const mockWorkspaceMemberRepository = { findOneByOrFail: jest.fn(), }; + const mockConnectedAccountDataAccessService = { + find: jest.fn(), + }; + const mockGlobalWorkspaceOrmManager = { getRepository: jest.fn().mockImplementation((workspaceId, name) => { if (name === 'calendarChannelEventAssociation') { return mockCalendarEventAssociationRepository; } - if (name === 'connectedAccount') { - return mockConnectedAccountRepository; - } if (name === 'workspaceMember') { return mockWorkspaceMemberRepository; } @@ -77,6 +75,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { provide: GlobalWorkspaceOrmManager, useValue: mockGlobalWorkspaceOrmManager, }, + { + provide: ConnectedAccountDataAccessService, + useValue: mockConnectedAccountDataAccessService, + }, ], }).compile(); @@ -121,7 +123,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { item.description !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED, ), ).toBe(true); - expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled(); + expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled(); }); it('should return calendar event with obfuscated title and description if the visibility is METADATA', async () => { @@ -143,7 +145,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { id: 'workspace-member-id', }); - mockConnectedAccountRepository.find.mockResolvedValue([]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([]); const result = await service.applyCalendarEventsVisibilityRestrictions( calendarEvents, @@ -179,7 +181,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { id: 'workspace-member-account-owner-id', }); - mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]); const result = await service.applyCalendarEventsVisibilityRestrictions( calendarEvents, @@ -215,7 +217,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { id: 'workspace-member-not-account-owner-id', }); - mockConnectedAccountRepository.find.mockResolvedValue([]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([]); const result = await service.applyCalendarEventsVisibilityRestrictions( calendarEvents, @@ -261,7 +263,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { }, ]); - mockConnectedAccountRepository.find + mockConnectedAccountDataAccessService.find .mockResolvedValueOnce([]) // request for calendar event 3 .mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2 @@ -317,7 +319,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { }, ]); - mockConnectedAccountRepository.find + mockConnectedAccountDataAccessService.find .mockResolvedValueOnce([]) // request for calendar event 3 .mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2 diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts index 1432fe1f8b7..4ecbff6bbee 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts @@ -5,18 +5,19 @@ import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/ import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; +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 CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class ApplyCalendarEventsVisibilityRestrictionsService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} public async applyCalendarEventsVisibilityRestrictions( @@ -42,12 +43,6 @@ export class ApplyCalendarEventsVisibilityRestrictionsService { relations: ['calendarChannel'], }); - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - const workspaceMemberRepository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, @@ -84,15 +79,13 @@ export class ApplyCalendarEventsVisibilityRestrictionsService { userId: userId, }); - const connectedAccounts = await connectedAccountRepository.find({ - select: ['id'], - where: { + const connectedAccounts = + await this.connectedAccountDataAccessService.find(workspaceId, { calendarChannels: { id: In(calendarChannels.map((channel) => channel.id)), }, accountOwnerId: workspaceMember.id, - }, - }); + }); if (connectedAccounts.length > 0) { continue; diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts index 0723c3a2293..6890e6ba48d 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { CalendarEventFindManyPostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.post-query.hook'; import { CalendarEventFindOnePostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.post-query.hook'; import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service'; @Module({ + imports: [ConnectedAccountDataAccessModule], providers: [ ApplyCalendarEventsVisibilityRestrictionsService, CalendarEventFindOnePostQueryHook, diff --git a/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts b/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts index b6f6f3de141..90115f92595 100644 --- a/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts @@ -1,22 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { Any } from 'typeorm'; +import { Any, In } from 'typeorm'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +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 { CalendarChannelSyncStage, CalendarChannelSyncStatus, - type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class CalendarChannelSyncStatusService { @@ -24,6 +25,8 @@ export class CalendarChannelSyncStatusService { private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectCacheStorage(CacheStorageNamespace.ModuleCalendar) private readonly cacheStorage: CacheStorageService, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, private readonly accountsToReconnectService: AccountsToReconnectService, private readonly metricsService: MetricsService, ) {} @@ -40,16 +43,14 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }, + ); }, authContext); } @@ -64,17 +65,15 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, - syncStatus: CalendarChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + syncStatus: CalendarChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }, + ); }, authContext); } @@ -95,17 +94,15 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncCursor: '', - syncStageStartedAt: null, - throttleFailureCount: 0, - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncCursor: '', + syncStageStartedAt: null, + throttleFailureCount: 0, + }, + ); }, authContext); await this.markAsCalendarEventListFetchPending( @@ -125,15 +122,13 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStageStartedAt: null, - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStageStartedAt: null, + }, + ); }, authContext); } @@ -149,16 +144,14 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }, + ); }, authContext); } @@ -173,17 +166,15 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, - syncStatus: CalendarChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + syncStatus: CalendarChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }, + ); }, authContext); } @@ -198,19 +189,17 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - syncStatus: CalendarChannelSyncStatus.ACTIVE, - throttleFailureCount: 0, - syncStageStartedAt: null, - syncedAt: new Date().toISOString(), - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + syncStatus: CalendarChannelSyncStatus.ACTIVE, + throttleFailureCount: 0, + syncStageStartedAt: null, + syncedAt: new Date().toISOString(), + }, + ); }, authContext); await this.markAsCalendarEventListFetchPending( @@ -241,16 +230,14 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - await calendarChannelRepository.update(calendarChannelIds, { - syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN, - syncStage: CalendarChannelSyncStage.FAILED, - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN, + syncStage: CalendarChannelSyncStage.FAILED, + }, + ); }, authContext); await this.metricsService.batchIncrementCounter({ @@ -276,33 +263,29 @@ export class CalendarChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: In(calendarChannelIds) }, + { + syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, + syncStage: CalendarChannelSyncStage.FAILED, + }, + ); - await calendarChannelRepository.update(calendarChannelIds, { - syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, - syncStage: CalendarChannelSyncStage.FAILED, - }); - - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - const calendarChannels = await calendarChannelRepository.find({ - select: ['id', 'connectedAccountId'], - where: { id: Any(calendarChannelIds) }, - }); + const calendarChannels = await this.calendarChannelDataAccessService.find( + workspaceId, + { + select: ['id', 'connectedAccountId'], + where: { id: Any(calendarChannelIds) }, + }, + ); const connectedAccountIds = calendarChannels.map( (calendarChannel) => calendarChannel.connectedAccountId, ); - await connectedAccountRepository.update( + await this.connectedAccountDataAccessService.update( + workspaceId, { id: Any(connectedAccountIds) }, { authFailedAt: new Date(), @@ -329,26 +312,43 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const calendarChannels = await calendarChannelRepository.find({ - where: { - id: Any(calendarChannelIds), - }, - relations: { - connectedAccount: { - accountOwner: true, + const calendarChannels = await this.calendarChannelDataAccessService.find( + workspaceId, + { + select: ['id', 'connectedAccountId'], + where: { + id: Any(calendarChannelIds), }, }, - }); + ); + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); for (const calendarChannel of calendarChannels) { - const userId = calendarChannel.connectedAccount.accountOwner.userId; - const connectedAccountId = calendarChannel.connectedAccount.id; + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { id: calendarChannel.connectedAccountId }, + }); + + if (!connectedAccount) { + continue; + } + + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { id: connectedAccount.accountOwnerId }, + }); + + if (!workspaceMember) { + continue; + } + + const userId = workspaceMember.userId; + const connectedAccountId = connectedAccount.id; await this.accountsToReconnectService.addAccountToReconnectByKey( AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts index 387e8ff9e71..70b0d59b658 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts @@ -1,6 +1,12 @@ import { registerEnumType } from '@nestjs/graphql'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + CalendarChannelContactAutoCreationPolicy, + CalendarChannelSyncStage, + CalendarChannelSyncStatus, + CalendarChannelVisibility, + FieldMetadataType, +} from 'twenty-shared/types'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util'; @@ -8,36 +14,12 @@ import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migr import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -export enum CalendarChannelVisibility { - METADATA = 'METADATA', - SHARE_EVERYTHING = 'SHARE_EVERYTHING', -} - -export enum CalendarChannelSyncStatus { - NOT_SYNCED = 'NOT_SYNCED', - ONGOING = 'ONGOING', - ACTIVE = 'ACTIVE', - FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', - FAILED_UNKNOWN = 'FAILED_UNKNOWN', -} - -export enum CalendarChannelSyncStage { - PENDING_CONFIGURATION = 'PENDING_CONFIGURATION', - CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING', - CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED', - CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING', - CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING', - CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED', - CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING', - FAILED = 'FAILED', -} - -export enum CalendarChannelContactAutoCreationPolicy { - AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER', - AS_PARTICIPANT = 'AS_PARTICIPANT', - AS_ORGANIZER = 'AS_ORGANIZER', - NONE = 'NONE', -} +export { + CalendarChannelContactAutoCreationPolicy, + CalendarChannelSyncStage, + CalendarChannelSyncStatus, + CalendarChannelVisibility, +}; registerEnumType(CalendarChannelVisibility, { name: 'CalendarChannelVisibility', diff --git a/packages/twenty-server/src/modules/connected-account/channel-sync/channel-sync.module.ts b/packages/twenty-server/src/modules/connected-account/channel-sync/channel-sync.module.ts index a44b96552cf..59ffabf742c 100644 --- a/packages/twenty-server/src/modules/connected-account/channel-sync/channel-sync.module.ts +++ b/packages/twenty-server/src/modules/connected-account/channel-sync/channel-sync.module.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common'; +import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { ChannelSyncResolver } from 'src/modules/connected-account/channel-sync/channel-sync.resolver'; @@ -8,6 +10,8 @@ import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-co @Module({ imports: [ + CalendarChannelDataAccessModule, + MessageChannelDataAccessModule, PermissionsModule, WorkspaceDataSourceModule, MessagingCommonModule, diff --git a/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts b/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts index ccbf6c9776c..77bf30c912f 100644 --- a/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts +++ b/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts @@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +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 { @@ -12,13 +14,9 @@ import { import { CalendarChannelSyncStage, CalendarChannelSyncStatus, - type CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.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'; import { MessagingMessageListFetchJob, type MessagingMessageListFetchJobData, @@ -37,7 +35,9 @@ export class ChannelSyncService { private readonly messageQueueService: MessageQueueService, @InjectMessageQueue(MessageQueue.calendarQueue) private readonly calendarQueueService: MessageQueueService, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, ) {} async startChannelSync(input: StartChannelSyncInput): Promise { @@ -54,18 +54,13 @@ export class ChannelSyncService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const messageChannels = await messageChannelRepository.find({ - where: { + const messageChannels = await this.messageChannelDataAccessService.find( + workspaceId, + { connectedAccountId, syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, }, - }); + ); for (const messageChannel of messageChannels) { await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled( @@ -91,25 +86,26 @@ export class ChannelSyncService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - - const calendarChannels = await calendarChannelRepository.find({ - where: { - connectedAccountId, - syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, + const calendarChannels = await this.calendarChannelDataAccessService.find( + workspaceId, + { + where: { + connectedAccountId, + syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, + }, }, - }); + ); for (const calendarChannel of calendarChannels) { - await calendarChannelRepository.update(calendarChannel.id, { - syncStage: - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, - syncStatus: CalendarChannelSyncStatus.ONGOING, - }); + await this.calendarChannelDataAccessService.update( + workspaceId, + { id: calendarChannel.id }, + { + syncStage: + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, + syncStatus: CalendarChannelSyncStatus.ONGOING, + }, + ); await this.calendarQueueService.add( CalendarEventListFetchJob.name, diff --git a/packages/twenty-server/src/modules/connected-account/connected-account.module.ts b/packages/twenty-server/src/modules/connected-account/connected-account.module.ts index 3888f960366..1370fd1344d 100644 --- a/packages/twenty-server/src/modules/connected-account/connected-account.module.ts +++ b/packages/twenty-server/src/modules/connected-account/connected-account.module.ts @@ -1,13 +1,14 @@ import { Module } from '@nestjs/common'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { DeleteWorkspaceMemberConnectedAccountsCleanupJob } from 'src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job'; import { ConnectedAccountWorkspaceMemberListener } from 'src/modules/connected-account/listeners/connected-account-workspace-member.listener'; import { ConnectedAccountListener } from 'src/modules/connected-account/listeners/connected-account.listener'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; @Module({ - imports: [UserVarsModule], + imports: [ConnectedAccountDataAccessModule, UserVarsModule], providers: [ AccountsToReconnectService, ConnectedAccountListener, diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts index 32dc0d436b6..e2d17369bd3 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { GmailEmailAliasErrorHandlerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-error-handler.service'; import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service'; import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service'; @@ -7,7 +8,7 @@ import { EmailAliasManagerService } from 'src/modules/connected-account/email-al import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; @Module({ - imports: [OAuth2ClientManagerModule], + imports: [OAuth2ClientManagerModule, ConnectedAccountDataAccessModule], providers: [ EmailAliasManagerService, GoogleEmailAliasManagerService, diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts index 9f0862a9708..85550b2e432 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts @@ -1,8 +1,8 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; -import { type Repository } from 'typeorm'; +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 { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service'; import { microsoftGraphMeResponseWithProxyAddresses } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/mocks/microsoft-api-examples'; @@ -15,30 +15,27 @@ import { EmailAliasManagerService } from './email-alias-manager.service'; describe('Email Alias Manager Service', () => { let emailAliasManagerService: EmailAliasManagerService; let microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService; - let connectedAccountRepository: Partial< - Repository - >; + const mockConnectedAccountDataAccessService = { + // @ts-expect-error legacy noImplicitAny + update: jest.fn().mockResolvedValue((arg) => arg), + }; beforeEach(async () => { - connectedAccountRepository = { - // @ts-expect-error legacy noImplicitAny - update: jest.fn().mockResolvedValue((arg) => arg), - }; - const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: GlobalWorkspaceOrmManager, useValue: { - getRepository: jest - .fn() - .mockResolvedValue(connectedAccountRepository), executeInWorkspaceContext: jest .fn() .mockImplementation((fn: () => any, _authContext?: any) => fn()), }, }, EmailAliasManagerService, + { + provide: ConnectedAccountDataAccessService, + useValue: mockConnectedAccountDataAccessService, + }, { provide: GoogleEmailAliasManagerService, useValue: {}, @@ -96,7 +93,8 @@ describe('Email Alias Manager Service', () => { microsoftEmailAliasManagerService.getHandleAliases, ).toHaveBeenCalledWith(mockConnectedAccount); - expect(connectedAccountRepository.update).toHaveBeenCalledWith( + expect(mockConnectedAccountDataAccessService.update).toHaveBeenCalledWith( + 'test-workspace-id', { id: mockConnectedAccount.id }, { handleAliases: expectedAliases, diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts index 04800e1b51b..bcad7d39ad6 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { assertUnreachable } from 'twenty-shared/utils'; +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 { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service'; @@ -15,6 +16,7 @@ export class EmailAliasManagerService { private readonly googleEmailAliasManagerService: GoogleEmailAliasManagerService, private readonly microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} public async refreshHandleAliases( @@ -50,13 +52,8 @@ export class EmailAliasManagerService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - await connectedAccountRepository.update( + await this.connectedAccountDataAccessService.update( + workspaceId, { id: connectedAccount.id }, { handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array diff --git a/packages/twenty-server/src/modules/connected-account/imap-api/imap-apis.module.ts b/packages/twenty-server/src/modules/connected-account/imap-api/imap-apis.module.ts index 21898e4157b..be029ad60c8 100644 --- a/packages/twenty-server/src/modules/connected-account/imap-api/imap-apis.module.ts +++ b/packages/twenty-server/src/modules/connected-account/imap-api/imap-apis.module.ts @@ -5,6 +5,9 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; +import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; @@ -19,6 +22,9 @@ import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services TwentyORMModule, FeatureFlagModule, AuthModule, + CalendarChannelDataAccessModule, + ConnectedAccountDataAccessModule, + MessageChannelDataAccessModule, ], providers: [ImapSmtpCalDavAPIService], exports: [ImapSmtpCalDavAPIService], diff --git a/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts b/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts index aebc1b3cd9f..d8f24089aa6 100644 --- a/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts +++ b/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts @@ -1,9 +1,9 @@ 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 { 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'; export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = { workspaceId: string; @@ -14,6 +14,7 @@ export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = { export class DeleteWorkspaceMemberConnectedAccountsCleanupJob { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} @Process(DeleteWorkspaceMemberConnectedAccountsCleanupJob.name) @@ -25,13 +26,7 @@ export class DeleteWorkspaceMemberConnectedAccountsCleanupJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - await connectedAccountRepository.delete({ + await this.connectedAccountDataAccessService.delete(workspaceId, { accountOwnerId: workspaceMemberId, }); }, authContext); diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index ef6bd3bd07e..5eace0312f9 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -10,16 +10,16 @@ import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/wo import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +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 { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; -import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; - @WorkspaceQueryHook(`connectedAccount.destroyOne`) export class ConnectedAccountDeleteOnePreQueryHook implements WorkspacePreQueryHookInstance { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly workspaceEventEmitter: WorkspaceEventEmitter, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -38,13 +38,7 @@ export class ConnectedAccountDeleteOnePreQueryHook const messageChannels = await this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspace.id, - 'messageChannel', - ); - - return messageChannelRepository.findBy({ + return this.messageChannelDataAccessService.find(workspace.id, { connectedAccountId, }); }, diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts index 3970b4ade2e..726adcde16b 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook'; @@ -10,6 +11,7 @@ import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-acc imports: [ NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, + MessageChannelDataAccessModule, ], providers: [ConnectedAccountDeleteOnePreQueryHook], }) diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts index 4ebe6b164f2..ea2ed3ec9cc 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module'; import { MicrosoftAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module'; import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service'; @@ -8,6 +9,7 @@ import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-acco @Module({ imports: [ JwtModule, + ConnectedAccountDataAccessModule, GoogleAPIRefreshAccessTokenModule, MicrosoftAPIRefreshAccessTokenModule, ], diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts index a182e5a6737..d819d77f868 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts @@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +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 { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service'; import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service'; @@ -17,7 +18,7 @@ describe('ConnectedAccountRefreshTokensService', () => { let service: ConnectedAccountRefreshTokensService; let googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService; let microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService; - let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; + let connectedAccountDataAccessService: ConnectedAccountDataAccessService; const mockWorkspaceId = 'workspace-123'; const mockConnectedAccountId = 'account-456'; @@ -44,13 +45,18 @@ describe('ConnectedAccountRefreshTokensService', () => { { provide: GlobalWorkspaceOrmManager, useValue: { - getRepository: jest.fn(), executeInWorkspaceContext: jest .fn() .mockImplementation((fn: () => any, _authContext?: any) => fn()), }, }, + { + provide: ConnectedAccountDataAccessService, + useValue: { + update: jest.fn(), + }, + }, ], }).compile(); @@ -65,9 +71,10 @@ describe('ConnectedAccountRefreshTokensService', () => { module.get( MicrosoftAPIRefreshAccessTokenService, ); - globalWorkspaceOrmManager = module.get( - GlobalWorkspaceOrmManager, - ); + connectedAccountDataAccessService = + module.get( + ConnectedAccountDataAccessService, + ); }); afterEach(() => { @@ -96,7 +103,7 @@ describe('ConnectedAccountRefreshTokensService', () => { expect( microsoftAPIRefreshAccessTokenService.refreshTokens, ).not.toHaveBeenCalled(); - expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled(); + expect(connectedAccountDataAccessService.update).not.toHaveBeenCalled(); }); it('should refresh and save new Microsoft token when expired (lastCredentialsRefreshedAt is old)', async () => { @@ -108,7 +115,6 @@ describe('ConnectedAccountRefreshTokensService', () => { lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago } as ConnectedAccountWorkspaceEntity; - const mockRepository = { update: jest.fn() }; const newTokens = { accessToken: mockNewAccessToken, refreshToken: mockRefreshToken, @@ -117,9 +123,6 @@ describe('ConnectedAccountRefreshTokensService', () => { jest .spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens') .mockResolvedValue(newTokens); - jest - .spyOn(globalWorkspaceOrmManager, 'getRepository') - .mockResolvedValue(mockRepository as any); const result = await service.refreshAndSaveTokens( connectedAccount, @@ -130,7 +133,8 @@ describe('ConnectedAccountRefreshTokensService', () => { expect( microsoftAPIRefreshAccessTokenService.refreshTokens, ).toHaveBeenCalledWith(mockRefreshToken); - expect(mockRepository.update).toHaveBeenCalledWith( + expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith( + mockWorkspaceId, { id: mockConnectedAccountId }, expect.objectContaining({ ...newTokens, @@ -148,7 +152,6 @@ describe('ConnectedAccountRefreshTokensService', () => { lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago } as ConnectedAccountWorkspaceEntity; - const mockRepository = { update: jest.fn() }; const newTokens = { accessToken: mockNewAccessToken, refreshToken: mockRefreshToken, @@ -157,9 +160,6 @@ describe('ConnectedAccountRefreshTokensService', () => { jest .spyOn(googleAPIRefreshAccessTokenService, 'refreshTokens') .mockResolvedValue(newTokens); - jest - .spyOn(globalWorkspaceOrmManager, 'getRepository') - .mockResolvedValue(mockRepository as any); const result = await service.refreshAndSaveTokens( connectedAccount, @@ -170,7 +170,8 @@ describe('ConnectedAccountRefreshTokensService', () => { expect( googleAPIRefreshAccessTokenService.refreshTokens, ).toHaveBeenCalledWith(mockRefreshToken); - expect(mockRepository.update).toHaveBeenCalledWith( + expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith( + mockWorkspaceId, { id: mockConnectedAccountId }, expect.objectContaining({ ...newTokens, @@ -188,7 +189,6 @@ describe('ConnectedAccountRefreshTokensService', () => { lastCredentialsRefreshedAt: null, } as ConnectedAccountWorkspaceEntity; - const mockRepository = { update: jest.fn() }; const newTokens = { accessToken: mockNewAccessToken, refreshToken: mockRefreshToken, @@ -197,9 +197,6 @@ describe('ConnectedAccountRefreshTokensService', () => { jest .spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens') .mockResolvedValue(newTokens); - jest - .spyOn(globalWorkspaceOrmManager, 'getRepository') - .mockResolvedValue(mockRepository as any); const result = await service.refreshAndSaveTokens( connectedAccount, @@ -210,7 +207,8 @@ describe('ConnectedAccountRefreshTokensService', () => { expect( microsoftAPIRefreshAccessTokenService.refreshTokens, ).toHaveBeenCalledWith(mockRefreshToken); - expect(mockRepository.update).toHaveBeenCalledWith( + expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith( + mockWorkspaceId, { id: mockConnectedAccountId }, expect.objectContaining({ ...newTokens, diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts index fea8eaaa41b..ef7931ae435 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts @@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; +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 { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service'; @@ -30,6 +31,7 @@ export class ConnectedAccountRefreshTokensService { private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService, private readonly microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} async refreshAndSaveTokens( @@ -78,13 +80,8 @@ export class ConnectedAccountRefreshTokensService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - await connectedAccountRepository.update( + await this.connectedAccountDataAccessService.update( + workspaceId, { id: connectedAccount.id }, { ...connectedAccountTokens, diff --git a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts index 0df4de9c870..8e100b547ba 100644 --- a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts @@ -5,6 +5,9 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +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 { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service'; @@ -18,19 +21,17 @@ jest.mock('uuid', () => ({ describe('ImapSmtpCalDavAPIService', () => { let service: ImapSmtpCalDavAPIService; - const mockConnectedAccountRepository = { + const mockConnectedAccountDataAccessService = { findOne: jest.fn(), save: jest.fn(), }; - const mockMessageChannelRepository = { + const mockMessageChannelDataAccessService = { findOne: jest.fn(), - save: jest.fn(), }; - const mockCalendarChannelRepository = { + const mockCalendarChannelDataAccessService = { findOne: jest.fn(), - save: jest.fn(), }; const mockWorkspaceDataSource = { @@ -52,18 +53,6 @@ describe('ImapSmtpCalDavAPIService', () => { { provide: GlobalWorkspaceOrmManager, useValue: { - getRepository: jest - .fn() - .mockImplementation((_workspaceId, entity) => { - if (entity === 'connectedAccount') - return mockConnectedAccountRepository; - if (entity === 'messageChannel') - return mockMessageChannelRepository; - if (entity === 'calendarChannel') - return mockCalendarChannelRepository; - - return {}; - }), getGlobalWorkspaceDataSource: jest .fn() .mockResolvedValue(mockWorkspaceDataSource), @@ -81,6 +70,18 @@ describe('ImapSmtpCalDavAPIService', () => { provide: CreateCalendarChannelService, useValue: mockCreateCalendarChannelService, }, + { + provide: ConnectedAccountDataAccessService, + useValue: mockConnectedAccountDataAccessService, + }, + { + provide: MessageChannelDataAccessService, + useValue: mockMessageChannelDataAccessService, + }, + { + provide: CalendarChannelDataAccessService, + useValue: mockCalendarChannelDataAccessService, + }, ], }).compile(); @@ -112,13 +113,14 @@ describe('ImapSmtpCalDavAPIService', () => { }; it('should create new account with message channel when account does not exist and IMAP is configured', async () => { - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(baseInput); - expect(mockConnectedAccountRepository.save).toHaveBeenCalledWith( + expect(mockConnectedAccountDataAccessService.save).toHaveBeenCalledWith( + 'workspace-id', { id: 'mocked-uuid', handle: 'test@example.com', @@ -127,7 +129,6 @@ describe('ImapSmtpCalDavAPIService', () => { accountOwnerId: 'workspace-member-id', }, {}, - {}, ); expect( @@ -162,11 +163,13 @@ describe('ImapSmtpCalDavAPIService', () => { connectedAccountId: 'existing-account-id', } as CalendarChannelWorkspaceEntity; - mockConnectedAccountRepository.findOne.mockResolvedValue(existingAccount); - mockMessageChannelRepository.findOne.mockResolvedValue( + mockConnectedAccountDataAccessService.findOne.mockResolvedValue( + existingAccount, + ); + mockMessageChannelDataAccessService.findOne.mockResolvedValue( existingMessageChannel, ); - mockCalendarChannelRepository.findOne.mockResolvedValue( + mockCalendarChannelDataAccessService.findOne.mockResolvedValue( existingCalendarChannel, ); @@ -177,7 +180,8 @@ describe('ImapSmtpCalDavAPIService', () => { await service.processAccount(inputWithConnectedAccountId); - expect(mockConnectedAccountRepository.save).toHaveBeenCalledWith( + expect(mockConnectedAccountDataAccessService.save).toHaveBeenCalledWith( + 'workspace-id', { id: 'existing-account-id', handle: 'test@example.com', @@ -186,7 +190,6 @@ describe('ImapSmtpCalDavAPIService', () => { accountOwnerId: 'workspace-member-id', }, {}, - {}, ); expect( @@ -210,9 +213,9 @@ describe('ImapSmtpCalDavAPIService', () => { } as EmailAccountConnectionParameters, }; - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(imapOnlyInput); @@ -238,9 +241,9 @@ describe('ImapSmtpCalDavAPIService', () => { } as EmailAccountConnectionParameters, }; - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(caldavOnlyInput); @@ -272,9 +275,9 @@ describe('ImapSmtpCalDavAPIService', () => { } as EmailAccountConnectionParameters, }; - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(imapSmtpInput); @@ -313,9 +316,9 @@ describe('ImapSmtpCalDavAPIService', () => { } as EmailAccountConnectionParameters, }; - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(fullConfigInput); @@ -335,23 +338,26 @@ describe('ImapSmtpCalDavAPIService', () => { provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, } as ConnectedAccountWorkspaceEntity; - mockConnectedAccountRepository.findOne.mockResolvedValueOnce( + mockConnectedAccountDataAccessService.findOne.mockResolvedValueOnce( existingAccount, ); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(baseInput); - expect(mockConnectedAccountRepository.findOne).toHaveBeenCalledWith({ + expect( + mockConnectedAccountDataAccessService.findOne, + ).toHaveBeenCalledWith('workspace-id', { where: { handle: 'test@example.com', accountOwnerId: 'workspace-member-id', }, }); - expect(mockConnectedAccountRepository.save).toHaveBeenCalledWith( + expect(mockConnectedAccountDataAccessService.save).toHaveBeenCalledWith( + 'workspace-id', { id: 'existing-account-id', handle: 'test@example.com', @@ -360,7 +366,6 @@ describe('ImapSmtpCalDavAPIService', () => { accountOwnerId: 'workspace-member-id', }, {}, - {}, ); }); @@ -378,9 +383,9 @@ describe('ImapSmtpCalDavAPIService', () => { } as EmailAccountConnectionParameters, }; - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(smtpOnlyInput); @@ -393,9 +398,9 @@ describe('ImapSmtpCalDavAPIService', () => { }); it('should handle transaction correctly', async () => { - mockConnectedAccountRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.findOne.mockResolvedValue(null); - mockCalendarChannelRepository.findOne.mockResolvedValue(null); + mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null); + mockMessageChannelDataAccessService.findOne.mockResolvedValue(null); + mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null); await service.processAccount(baseInput); diff --git a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts index e342939519f..1907a96c4b1 100644 --- a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts +++ b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts @@ -7,17 +7,21 @@ import { v4 } from 'uuid'; import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto'; +import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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 CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @Injectable() export class ImapSmtpCalDavAPIService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly createMessageChannelService: CreateMessageChannelService, private readonly createCalendarChannelService: CreateCalendarChannelService, ) {} @@ -30,15 +34,12 @@ export class ImapSmtpCalDavAPIService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV }, + }); - return connectedAccountRepository.findOne({ - where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV }, - }); + return connectedAccount as ConnectedAccountWorkspaceEntity | null; }, authContext, ); @@ -58,29 +59,11 @@ export class ImapSmtpCalDavAPIService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const calendarChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'calendarChannel', - ); - const existingAccount = connectedAccountId - ? await connectedAccountRepository.findOne({ + ? await this.connectedAccountDataAccessService.findOne(workspaceId, { where: { id: connectedAccountId }, }) - : await connectedAccountRepository.findOne({ + : await this.connectedAccountDataAccessService.findOne(workspaceId, { where: { handle, accountOwnerId: workspaceMemberId }, }); @@ -91,13 +74,13 @@ export class ImapSmtpCalDavAPIService { await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource(); const existingMessageChannel = existingAccount - ? await messageChannelRepository.findOne({ + ? await this.messageChannelDataAccessService.findOne(workspaceId, { where: { connectedAccountId: existingAccount.id }, }) : null; const existingCalendarChannel = existingAccount - ? await calendarChannelRepository.findOne({ + ? await this.calendarChannelDataAccessService.findOne(workspaceId, { where: { connectedAccountId: existingAccount.id }, }) : null; @@ -112,7 +95,8 @@ export class ImapSmtpCalDavAPIService { await workspaceDataSource.transaction( async (manager: WorkspaceEntityManager) => { - await connectedAccountRepository.save( + await this.connectedAccountDataAccessService.save( + workspaceId, { id: newOrExistingAccountId, handle, @@ -120,7 +104,6 @@ export class ImapSmtpCalDavAPIService { connectionParameters: input.connectionParameters, accountOwnerId: workspaceMemberId, }, - {}, manager, ); diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts index 1a6ad8e2d96..3b62cd12441 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts @@ -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(), ); - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - const messageChannelMessageAssociationRepository = await this.globalWorkspaceOrmManager.getRepository( 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); 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) => { diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts index a2b6d1c9860..2033adf94ad 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts @@ -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 @@ -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( - 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), diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/messaging-blocklist-manager.module.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/messaging-blocklist-manager.module.ts index 716f67b8800..54ffeadfbe8 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/messaging-blocklist-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/messaging-blocklist-manager.module.ts @@ -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, diff --git a/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts b/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts index 5e44732ae26..5c131af3536 100644 --- a/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts +++ b/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts @@ -3,6 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; +import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; @@ -12,6 +15,9 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se WorkspaceDataSourceModule, TypeOrmModule.forFeature([FeatureFlagEntity]), ConnectedAccountModule, + ConnectedAccountDataAccessModule, + MessageChannelDataAccessModule, + MessageFolderDataAccessModule, MetricsModule, ], providers: [MessageChannelSyncStatusService], diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts index 3392eb1629f..86fdef25b2c 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts @@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants'; +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 { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -34,22 +35,19 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { find: jest.fn(), }; - const mockConnectedAccountRepository = { - find: jest.fn(), - }; - const mockWorkspaceMemberRepository = { findOneByOrFail: jest.fn(), }; + const mockConnectedAccountDataAccessService = { + find: jest.fn(), + }; + const mockGlobalWorkspaceOrmManager = { getRepository: jest.fn().mockImplementation((workspaceId, name) => { if (name === 'messageChannelMessageAssociation') { return mockMessageChannelMessageAssociationRepository; } - if (name === 'connectedAccount') { - return mockConnectedAccountRepository; - } if (name === 'workspaceMember') { return mockWorkspaceMemberRepository; } @@ -67,6 +65,10 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { provide: GlobalWorkspaceOrmManager, useValue: mockGlobalWorkspaceOrmManager, }, + { + provide: ConnectedAccountDataAccessService, + useValue: mockConnectedAccountDataAccessService, + }, ], }).compile(); @@ -105,7 +107,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { item.subject === 'Test Subject' && item.text === 'Test Message', ), ).toBe(true); - expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled(); + expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled(); }); it('should return message without obfuscated subject and with obfuscated text if the visibility is SUBJECT', async () => { @@ -123,7 +125,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { }, ]); - mockConnectedAccountRepository.find.mockResolvedValue([]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([]); mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({ id: 'workspace-member-id', @@ -158,7 +160,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { }, ]); - mockConnectedAccountRepository.find.mockResolvedValue([]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([]); mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({ id: 'workspace-member-id', @@ -198,7 +200,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { id: 'workspace-member-account-owner-id', }); - mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]); const result = await service.applyMessagesVisibilityRestrictions( messages, @@ -233,7 +235,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { id: 'workspace-member-not-account-owner-id', }); - mockConnectedAccountRepository.find.mockResolvedValue([]); + mockConnectedAccountDataAccessService.find.mockResolvedValue([]); const result = await service.applyMessagesVisibilityRestrictions( messages, @@ -279,7 +281,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { id: 'workspace-member-id', }); - mockConnectedAccountRepository.find + mockConnectedAccountDataAccessService.find .mockResolvedValueOnce([]) // request for message 3 .mockResolvedValueOnce([]); // request for message 2 @@ -338,7 +340,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { id: 'workspace-member-id', }); - mockConnectedAccountRepository.find + mockConnectedAccountDataAccessService.find .mockResolvedValueOnce([]) // request for message 3 .mockResolvedValueOnce([]); // request for message 2 diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts index c5e4551cf8f..b20ff2017bb 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts @@ -6,9 +6,9 @@ import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +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 { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -18,6 +18,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta export class ApplyMessagesVisibilityRestrictionsService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, ) {} public async applyMessagesVisibilityRestrictions( @@ -43,12 +44,6 @@ export class ApplyMessagesVisibilityRestrictionsService { relations: ['messageChannel'], }); - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - const workspaceMemberRepository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, @@ -91,15 +86,13 @@ export class ApplyMessagesVisibilityRestrictionsService { userId, }); - const connectedAccounts = await connectedAccountRepository.find({ - select: ['id'], - where: { + const connectedAccounts = + await this.connectedAccountDataAccessService.find(workspaceId, { messageChannels: { id: In(messageChannels.map((channel) => channel.id)), }, accountOwnerId: workspaceMember.id, - }, - }); + }); if (connectedAccounts.length > 0) { continue; diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts index 295829e98fd..912638ec423 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts @@ -14,6 +14,8 @@ import { } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-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 { @@ -21,10 +23,7 @@ import { MessageChannelSyncStage, type MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { - MessageFolderPendingSyncAction, - type MessageFolderWorkspaceEntity, -} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service'; const ONGOING_SYNC_STAGES = [ @@ -41,6 +40,8 @@ export class MessageChannelUpdateOnePreQueryHook constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService, ) {} @@ -57,15 +58,10 @@ export class MessageChannelUpdateOnePreQueryHook return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspace.id, - 'messageChannel', - ); - - const messageChannel = await messageChannelRepository.findOne({ - where: { id: payload.id }, - }); + const messageChannel = + await this.messageChannelDataAccessService.findOne(workspace.id, { + where: { id: payload.id }, + }); if (!isDefined(messageChannel)) { throw new WorkspaceQueryRunnerException( @@ -77,29 +73,27 @@ export class MessageChannelUpdateOnePreQueryHook ); } + const messageChannelWorkspace = + messageChannel as unknown as MessageChannelWorkspaceEntity; + const isSyncOngoing = ONGOING_SYNC_STAGES.includes( - messageChannel.syncStage, + messageChannelWorkspace.syncStage, ); - const messageFolderRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspace.id, - 'messageFolder', - ); + const messageFoldersWithPendingAction = + await this.messageFolderDataAccessService.find(workspace.id, { + messageChannelId: messageChannel.id, + pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE), + }); const messageFoldersWithPendingActionCount = - await messageFolderRepository.count({ - where: { - messageChannelId: messageChannel.id, - pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE), - }, - }); + messageFoldersWithPendingAction.length; const hasPendingFolderActions = messageFoldersWithPendingActionCount > 0; const hasPendingGroupEmailsAction = - messageChannel.pendingGroupEmailsAction !== + messageChannelWorkspace.pendingGroupEmailsAction !== MessageChannelPendingGroupEmailsAction.NONE; if ( @@ -116,12 +110,12 @@ export class MessageChannelUpdateOnePreQueryHook } const hasCompletedConfiguration = - messageChannel.syncStage !== + messageChannelWorkspace.syncStage !== MessageChannelSyncStage.PENDING_CONFIGURATION; if (!hasCompletedConfiguration) { this.logger.log( - `MessageChannelId: ${messageChannel.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`, + `MessageChannelId: ${messageChannelWorkspace.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`, ); return payload; @@ -129,11 +123,12 @@ export class MessageChannelUpdateOnePreQueryHook const excludeGroupEmailsChanged = isDefined(payload.data.excludeGroupEmails) && - payload.data.excludeGroupEmails !== messageChannel.excludeGroupEmails; + payload.data.excludeGroupEmails !== + messageChannelWorkspace.excludeGroupEmails; if (excludeGroupEmailsChanged) { await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction( - messageChannel, + messageChannelWorkspace, workspace.id, payload.data.excludeGroupEmails ? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts index 7ff63adf4b0..3372d6b8d92 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common'; +import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; +import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module'; import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service'; import { MessageChannelUpdateOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook'; import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook'; @@ -7,7 +10,12 @@ import { MessageFindOnePostQueryHook } from 'src/modules/messaging/common/query- import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module'; @Module({ - imports: [MessagingImportManagerModule], + imports: [ + MessagingImportManagerModule, + ConnectedAccountDataAccessModule, + MessageChannelDataAccessModule, + MessageFolderDataAccessModule, + ], providers: [ ApplyMessagesVisibilityRestrictionsService, MessageFindOnePostQueryHook, diff --git a/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts b/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts index 91b6a4cdc80..49f8459baa0 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts @@ -7,10 +7,12 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; +import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-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 { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type'; import { MessageChannelPendingGroupEmailsAction, @@ -18,10 +20,8 @@ import { MessageChannelSyncStatus, type MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { - MessageFolderPendingSyncAction, - MessageFolderWorkspaceEntity, -} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class MessageChannelSyncStatusService { @@ -29,6 +29,9 @@ export class MessageChannelSyncStatusService { @InjectCacheStorage(CacheStorageNamespace.ModuleMessaging) private readonly cacheStorage: CacheStorageService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, + private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService, private readonly accountsToReconnectService: AccountsToReconnectService, private readonly metricsService: MetricsService, ) {} @@ -45,16 +48,14 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }, + ); }, authContext); } @@ -70,16 +71,14 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }, + ); }, authContext); } @@ -100,27 +99,20 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncCursor: '', + syncStageStartedAt: null, + throttleFailureCount: 0, + throttleRetryAfter: null, + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, + }, + ); - const messageFolderRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageFolder', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncCursor: '', - syncStageStartedAt: null, - throttleFailureCount: 0, - throttleRetryAfter: null, - pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, - }); - - await messageFolderRepository.update( + await this.messageFolderDataAccessService.update( + workspaceId, { messageChannelId: In(messageChannelIds) }, { syncCursor: '', @@ -143,15 +135,11 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStageStartedAt: null, - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { syncStageStartedAt: null }, + ); }, authContext); } @@ -166,17 +154,15 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, - syncStatus: MessageChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, + syncStatus: MessageChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }, + ); }, authContext); } @@ -191,17 +177,15 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, - syncStatus: MessageChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, + syncStatus: MessageChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }, + ); }, authContext); } @@ -216,20 +200,18 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStatus: MessageChannelSyncStatus.ACTIVE, - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - throttleFailureCount: 0, - throttleRetryAfter: null, - syncStageStartedAt: null, - syncedAt: new Date().toISOString(), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStatus: MessageChannelSyncStatus.ACTIVE, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + throttleFailureCount: 0, + throttleRetryAfter: null, + syncStageStartedAt: null, + syncedAt: new Date().toISOString(), + }, + ); }, authContext); await this.metricsService.batchIncrementCounter({ @@ -249,15 +231,13 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, + }, + ); }, authContext); } @@ -272,17 +252,15 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, - syncStatus: MessageChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, + syncStatus: MessageChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }, + ); }, authContext); } @@ -300,17 +278,15 @@ export class MessageChannelSyncStatusService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.FAILED, - syncStatus: syncStatus, - throttleRetryAfter: null, - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: In(messageChannelIds) }, + { + syncStage: MessageChannelSyncStage.FAILED, + syncStatus: syncStatus, + throttleRetryAfter: null, + }, + ); const metricsKey = syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS @@ -325,22 +301,17 @@ export class MessageChannelSyncStatusService { if ( syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS ) { - const connectedAccountRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'connectedAccount', - ); - - const messageChannels = await messageChannelRepository.find({ - select: ['id', 'connectedAccountId'], - where: { id: Any(messageChannelIds) }, - }); + const messageChannels = await this.messageChannelDataAccessService.find( + workspaceId, + { id: In(messageChannelIds) }, + ); const connectedAccountIds = messageChannels.map( (messageChannel) => messageChannel.connectedAccountId, ); - await connectedAccountRepository.update( + await this.connectedAccountDataAccessService.update( + workspaceId, { id: Any(connectedAccountIds) }, { authFailedAt: new Date(), @@ -363,30 +334,43 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( + const messageChannels = await this.messageChannelDataAccessService.findMany( + workspaceId, + { + where: { id: In(messageChannelIds) }, + }, + ); + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( workspaceId, - 'messageChannel', + 'workspaceMember', + { shouldBypassPermissionChecks: true }, ); - const messageChannels = await messageChannelRepository.find({ - where: { id: Any(messageChannelIds) }, - relations: { - connectedAccount: { - accountOwner: true, - }, - }, - }); - for (const messageChannel of messageChannels) { - const userId = messageChannel.connectedAccount.accountOwner.userId; - const connectedAccountId = messageChannel.connectedAccount.id; + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { id: messageChannel.connectedAccountId }, + }); + + if (!connectedAccount) { + continue; + } + + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { id: connectedAccount.accountOwnerId }, + }); + + if (!workspaceMember) { + continue; + } await this.accountsToReconnectService.addAccountToReconnectByKey( AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, - userId, + workspaceMember.userId, workspaceId, - connectedAccountId, + connectedAccount.id, ); } } diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts index 3c1ab9babbf..49b0ca838cf 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts @@ -1,6 +1,15 @@ import { registerEnumType } from '@nestjs/graphql'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + FieldMetadataType, + MessageChannelContactAutoCreationPolicy, + MessageChannelPendingGroupEmailsAction, + MessageChannelSyncStage, + MessageChannelSyncStatus, + MessageChannelType, + MessageChannelVisibility, + MessageFolderImportPolicy, +} from 'twenty-shared/types'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util'; @@ -9,52 +18,15 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; -export enum MessageChannelSyncStatus { - NOT_SYNCED = 'NOT_SYNCED', - ONGOING = 'ONGOING', - ACTIVE = 'ACTIVE', - FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', - FAILED_UNKNOWN = 'FAILED_UNKNOWN', -} - -export enum MessageChannelSyncStage { - PENDING_CONFIGURATION = 'PENDING_CONFIGURATION', - MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING', - MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED', - MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING', - MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING', - MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED', - MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING', - FAILED = 'FAILED', -} - -export enum MessageChannelVisibility { - METADATA = 'METADATA', - SUBJECT = 'SUBJECT', - SHARE_EVERYTHING = 'SHARE_EVERYTHING', -} - -export enum MessageChannelType { - EMAIL = 'EMAIL', - SMS = 'SMS', -} - -export enum MessageChannelContactAutoCreationPolicy { - SENT_AND_RECEIVED = 'SENT_AND_RECEIVED', - SENT = 'SENT', - NONE = 'NONE', -} - -export enum MessageFolderImportPolicy { - ALL_FOLDERS = 'ALL_FOLDERS', - SELECTED_FOLDERS = 'SELECTED_FOLDERS', -} - -export enum MessageChannelPendingGroupEmailsAction { - GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION', - GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT', - NONE = 'NONE', -} +export { + MessageChannelContactAutoCreationPolicy, + MessageChannelPendingGroupEmailsAction, + MessageChannelSyncStage, + MessageChannelSyncStatus, + MessageChannelType, + MessageChannelVisibility, + MessageFolderImportPolicy, +}; registerEnumType(MessageChannelVisibility, { name: 'MessageChannelVisibility', diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts index e00e21e3f2f..d49af4721bb 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts @@ -1,6 +1,9 @@ import { registerEnumType } from '@nestjs/graphql'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + FieldMetadataType, + MessageFolderPendingSyncAction, +} from 'twenty-shared/types'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util'; @@ -8,10 +11,7 @@ import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migr import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -export enum MessageFolderPendingSyncAction { - FOLDER_DELETION = 'FOLDER_DELETION', - NONE = 'NONE', -} +export { MessageFolderPendingSyncAction }; registerEnumType(MessageFolderPendingSyncAction, { name: 'MessageFolderPendingSyncAction', diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts index 152a827828e..298b06285c9 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts @@ -3,10 +3,10 @@ import { Logger } from '@nestjs/common'; import { Command, CommandRunner, Option } from 'nest-commander'; import { isDefined } from 'twenty-shared/utils'; +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 { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; -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'; type MessagingResetChannelCommandOptions = { @@ -24,6 +24,7 @@ export class MessagingResetChannelCommand extends CommandRunner { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService, private readonly messagingMessageCleanerService: MessagingMessageCleanerService, ) { @@ -39,21 +40,14 @@ export class MessagingResetChannelCommand extends CommandRunner { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - this.logger.log( `No message channel ID provided, resetting all message channels in workspace ${workspaceId}`, ); - const messageChannels = await messageChannelRepository.find({ - where: { - ...(isDefined(messageChannelId) ? { id: messageChannelId } : {}), - }, - }); + const messageChannels = await this.messageChannelDataAccessService.find( + workspaceId, + isDefined(messageChannelId) ? { id: messageChannelId } : {}, + ); if (messageChannels.length === 0) { this.logger.log( diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/messaging-message-cleaner.module.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/messaging-message-cleaner.module.ts index 523c51f8db8..b0312a014e1 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/messaging-message-cleaner.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/messaging-message-cleaner.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; import { MessagingMessageCleanerRemoveOrphansCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command'; import { MessagingResetChannelCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command'; @@ -15,6 +16,7 @@ import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cl TypeOrmModule.forFeature([WorkspaceEntity]), DataSourceModule, MessagingCommonModule, + MessageChannelDataAccessModule, ], providers: [ MessagingConnectedAccountDeletionCleanupJob, diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts index 0955aadd206..ba02fff116c 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service'; @@ -20,6 +21,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im FeatureFlagModule, WorkspaceDataSourceModule, DataSourceModule, + MessageFolderDataAccessModule, TypeOrmModule.forFeature([WorkspaceEntity]), OAuth2ClientManagerModule, MessagingGmailDriverModule, diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.spec.ts index 131d9cc0b23..8da989fd207 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.spec.ts @@ -1,9 +1,11 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { In } from 'typeorm'; import { type DiscoveredMessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { MessageChannelContactAutoCreationPolicy, @@ -83,39 +85,73 @@ const createMockExistingFolder = ( ...overrides, }); +const getInValuesFromWhere = (externalIdClause: unknown): string[] => { + if ( + externalIdClause && + typeof externalIdClause === 'object' && + '_value' in externalIdClause + ) { + return (externalIdClause as { _value: string[] })._value; + } + + if ( + externalIdClause && + typeof externalIdClause === 'object' && + 'value' in externalIdClause + ) { + const value = (externalIdClause as { value: unknown }).value; + + return Array.isArray(value) ? value : []; + } + + return []; +}; + describe('SyncMessageFoldersService', () => { let service: SyncMessageFoldersService; let gmailGetAllFoldersService: jest.Mocked; - let mockRepository: { + let mockMessageFolderDataAccessService: { delete: jest.Mock; update: jest.Mock; - updateMany: jest.Mock; save: jest.Mock; + find: jest.Mock; }; - let mockTransactionManager: object; + + let createdFolderRecords: Array< + Partial & { + id: string; + externalId: string; + } + >; beforeEach(async () => { - mockRepository = { + createdFolderRecords = []; + + mockMessageFolderDataAccessService = { delete: jest.fn(), - update: jest.fn(), - updateMany: jest.fn(), - save: jest.fn().mockImplementation((folders) => - folders.map((folder: Partial) => ({ + update: jest.fn().mockResolvedValue(undefined), + save: jest.fn().mockImplementation(async (_workspaceId, folder) => { + createdFolderRecords.push({ ...folder, - id: `new-folder-${Math.random().toString(36).substring(7)}`, + id: `new-folder-${createdFolderRecords.length}-${Math.random().toString(36).substring(7)}`, isSynced: false, syncCursor: null, - })), - ), - }; + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + externalId: folder.externalId as string, + }); + }), + find: jest.fn().mockImplementation(async (_workspaceId, where) => { + if (!where?.externalId) { + return []; + } - mockTransactionManager = {}; + const externalIds = getInValuesFromWhere(where.externalId); - const mockDataSource = { - transaction: jest - .fn() - .mockImplementation((callback) => callback(mockTransactionManager)), + return createdFolderRecords.filter((folder) => + externalIds.includes(folder.externalId as string), + ); + }), }; const module: TestingModule = await Test.createTestingModule({ @@ -129,15 +165,15 @@ describe('SyncMessageFoldersService', () => { .mockImplementation((callback: () => any, _authContext?: any) => callback(), ), - getRepository: jest.fn().mockResolvedValue(mockRepository), - getDataSourceForWorkspace: jest - .fn() - .mockResolvedValue(mockDataSource), - getGlobalWorkspaceDataSource: jest - .fn() - .mockResolvedValue(mockDataSource), + getRepository: jest.fn(), + getDataSourceForWorkspace: jest.fn(), + getGlobalWorkspaceDataSource: jest.fn(), }, }, + { + provide: MessageFolderDataAccessService, + useValue: mockMessageFolderDataAccessService, + }, { provide: GmailGetAllFoldersService, useValue: { @@ -192,23 +228,23 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.save).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - name: 'INBOX', - externalId: 'inbox-ext', - messageChannelId: 'channel-123', - isSentFolder: false, - }), - expect.objectContaining({ - name: 'Sent', - externalId: 'sent-ext', - messageChannelId: 'channel-123', - isSentFolder: true, - }), - ]), - {}, - mockTransactionManager, + expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith( + workspaceId, + expect.objectContaining({ + name: 'INBOX', + externalId: 'inbox-ext', + messageChannelId: 'channel-123', + isSentFolder: false, + }), + ); + expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith( + workspaceId, + expect.objectContaining({ + name: 'Sent', + externalId: 'sent-ext', + messageChannelId: 'channel-123', + isSentFolder: true, + }), ); expect(result).toHaveLength(2); }); @@ -239,15 +275,12 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.save).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - name: 'Projects', - parentFolderId: 'parent-folder-id', - }), - ]), - {}, - mockTransactionManager, + expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith( + workspaceId, + expect.objectContaining({ + name: 'Projects', + parentFolderId: 'parent-folder-id', + }), ); }); }); @@ -278,14 +311,10 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.updateMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - criteria: 'folder-1', - partialEntity: expect.objectContaining({ name: 'Primary Inbox' }), - }), - ]), - mockTransactionManager, + expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith( + workspaceId, + { id: 'folder-1' }, + expect.objectContaining({ name: 'Primary Inbox' }), ); expect(result).toContainEqual( expect.objectContaining({ @@ -322,16 +351,12 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.updateMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - criteria: 'folder-1', - partialEntity: expect.objectContaining({ - parentFolderId: 'new-parent-id', - }), - }), - ]), - mockTransactionManager, + expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith( + workspaceId, + { id: 'folder-1' }, + expect.objectContaining({ + parentFolderId: 'new-parent-id', + }), ); }); @@ -364,7 +389,9 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.updateMany).not.toHaveBeenCalled(); + expect( + mockMessageFolderDataAccessService.update, + ).not.toHaveBeenCalled(); }); }); @@ -401,16 +428,12 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.updateMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - criteria: 'folder-2', - partialEntity: expect.objectContaining({ - pendingSyncAction: 'FOLDER_DELETION', - }), - }), - ]), - mockTransactionManager, + expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith( + workspaceId, + { id: In(['folder-2']) }, + expect.objectContaining({ + pendingSyncAction: 'FOLDER_DELETION', + }), ); expect(result).toContainEqual( expect.objectContaining({ @@ -468,32 +491,21 @@ describe('SyncMessageFoldersService', () => { workspaceId, }); - expect(mockRepository.updateMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - criteria: 'folder-to-delete', - partialEntity: expect.objectContaining({ - pendingSyncAction: 'FOLDER_DELETION', - }), - }), - ]), - mockTransactionManager, + expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith( + workspaceId, + { id: In(['folder-to-delete']) }, + expect.objectContaining({ + pendingSyncAction: 'FOLDER_DELETION', + }), ); - expect(mockRepository.updateMany).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - criteria: 'folder-to-update', - partialEntity: expect.objectContaining({ name: 'New Name' }), - }), - ]), - mockTransactionManager, + expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith( + workspaceId, + { id: 'folder-to-update' }, + expect.objectContaining({ name: 'New Name' }), ); - expect(mockRepository.save).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ externalId: 'new-ext' }), - ]), - {}, - mockTransactionManager, + expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith( + workspaceId, + expect.objectContaining({ externalId: 'new-ext' }), ); expect(result).toHaveLength(4); expect(result).toContainEqual( diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts index 1dd442bd7ff..59c319dfa12 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts @@ -1,21 +1,20 @@ import { Injectable } from '@nestjs/common'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { In } from 'typeorm'; import { DiscoveredMessageFolder, MessageFolder, } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface'; -import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-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 { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { - MessageFolderPendingSyncAction, - MessageFolderWorkspaceEntity, -} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service'; import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service'; import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service'; @@ -28,6 +27,7 @@ import { computeUpdatedFolders } from 'src/modules/messaging/message-folder-mana export class SyncMessageFoldersService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, private readonly gmailGetAllFoldersService: GmailGetAllFoldersService, private readonly microsoftGetAllFoldersService: MicrosoftGetAllFoldersService, private readonly imapGetAllFoldersService: ImapGetAllFoldersService, @@ -132,60 +132,63 @@ export class SyncMessageFoldersService { const authContext = buildSystemAuthContext(workspaceId); + // TODO: Restore transaction wrapper once migration is complete — folder + // sync operations (create/update/delete) are no longer atomic since + // the data access layer routes writes across workspace and core schemas. + // Acceptable during transition as sync is idempotent and self-corrects. return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const messageFolderRepository = - await this.globalWorkspaceOrmManager.getRepository( + if (folderIdsToDelete.length > 0) { + await this.messageFolderDataAccessService.update( workspaceId, - 'messageFolder', + { id: In(folderIdsToDelete) }, + { + pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION, + }, ); + } - const workspaceDataSource = - await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource(); + if (foldersToUpdate.size > 0) { + for (const [id, data] of foldersToUpdate.entries()) { + await this.messageFolderDataAccessService.update( + workspaceId, + { id }, + data, + ); + } + } - return workspaceDataSource.transaction( - async (transactionManager: WorkspaceEntityManager) => { - if (folderIdsToDelete.length > 0) { - await messageFolderRepository.updateMany( - folderIdsToDelete.map((id) => ({ - criteria: id, - partialEntity: { - pendingSyncAction: - MessageFolderPendingSyncAction.FOLDER_DELETION, - }, - })), - transactionManager, - ); - } + if (foldersToCreate.length > 0) { + for (const folderToCreate of foldersToCreate) { + await this.messageFolderDataAccessService.save( + workspaceId, + folderToCreate, + ); + } + } - if (foldersToUpdate.size > 0) { - await messageFolderRepository.updateMany( - Array.from(foldersToUpdate.entries()).map(([id, data]) => ({ - criteria: id, - partialEntity: data, - })), - transactionManager, - ); - } + const createdFolders = + foldersToCreate.length > 0 + ? await this.messageFolderDataAccessService.find(workspaceId, { + messageChannelId, + externalId: In( + foldersToCreate + .map((folder) => folder.externalId) + .filter(isDefined), + ), + }) + : []; - const createdFolders = - foldersToCreate.length > 0 - ? await messageFolderRepository.save( - foldersToCreate, - {}, - transactionManager, - ) - : []; + const updatedExistingFolders = computeUpdatedFolders({ + existingFolders, + foldersToUpdate, + folderIdsToDelete, + }); - const updatedExistingFolders = computeUpdatedFolders({ - existingFolders, - foldersToUpdate, - folderIdsToDelete, - }); - - return [...updatedExistingFolders, ...createdFolders]; - }, - ); + return [ + ...updatedExistingFolders, + ...(createdFolders as MessageFolder[]), + ]; }, authContext, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-trigger-message-list-fetch.command.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-trigger-message-list-fetch.command.ts index 74aa8cbee62..f71b6e307bb 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-trigger-message-list-fetch.command.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-trigger-message-list-fetch.command.ts @@ -5,16 +5,14 @@ import { Command, CommandRunner, Option } from 'nest-commander'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +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 { MessagingMessageListFetchJob, type MessagingMessageListFetchJobData, } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; -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'; type MessagingTriggerMessageListFetchCommandOptions = { workspaceId: string; @@ -33,6 +31,7 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, ) { @@ -52,23 +51,14 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const whereCondition: Record = { - isSyncEnabled: true, - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - }; - - if (messageChannelId) { - whereCondition.id = messageChannelId; - } - - const messageChannels = - await messageChannelRepository.find(whereCondition); + const messageChannels = await this.messageChannelDataAccessService.find( + workspaceId, + { + isSyncEnabled: true, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + ...(messageChannelId ? { id: messageChannelId } : {}), + }, + ); if (messageChannels.length === 0) { this.logger.warn( @@ -83,10 +73,14 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner { ); for (const messageChannel of messageChannels) { - await messageChannelRepository.update(messageChannel.id, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, - syncStageStartedAt: new Date().toISOString(), - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: messageChannel.id }, + { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, + syncStageStartedAt: new Date().toISOString(), + }, + ); await this.messageQueueService.add( MessagingMessageListFetchJob.name, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts index 0c797a28b2b..a00fb4b8b8c 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts @@ -3,6 +3,7 @@ import { Scope } from '@nestjs/common'; 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 { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; @@ -18,6 +19,16 @@ import { import { MessagingMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; +const toIsoStringOrNull = ( + value: string | Date | null | undefined, +): string | null => { + if (value == null) { + return null; + } + + return value instanceof Date ? value.toISOString() : value; +}; + export type MessagingMessageListFetchJobData = { messageChannelId: string; workspaceId: string; @@ -32,6 +43,7 @@ export class MessagingMessageListFetchJob { private readonly messagingMessageListFetchService: MessagingMessageListFetchService, private readonly messagingMonitoringService: MessagingMonitoringService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, ) {} @@ -49,18 +61,15 @@ export class MessagingMessageListFetchJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const messageChannel = await messageChannelRepository.findOne({ - where: { - id: messageChannelId, + const messageChannel = await this.messageChannelDataAccessService.findOne( + workspaceId, + { + where: { + id: messageChannelId, + }, + relations: ['connectedAccount', 'messageFolders'], }, - relations: ['connectedAccount', 'messageFolders'], - }); + ); if (!messageChannel) { await this.messagingMonitoringService.track({ @@ -82,9 +91,9 @@ export class MessagingMessageListFetchJob { try { if ( isThrottled( - messageChannel.syncStageStartedAt, + toIsoStringOrNull(messageChannel.syncStageStartedAt), messageChannel.throttleFailureCount, - messageChannel.throttleRetryAfter, + toIsoStringOrNull(messageChannel.throttleRetryAfter), ) ) { await this.messageChannelSyncStatusService.markAsMessagesListFetchPending( @@ -104,7 +113,7 @@ export class MessagingMessageListFetchJob { }); await this.messagingMessageListFetchService.processMessageListFetch( - messageChannel, + messageChannel as unknown as MessageChannelWorkspaceEntity, workspaceId, ); @@ -118,7 +127,7 @@ export class MessagingMessageListFetchJob { await this.messageImportErrorHandlerService.handleDriverException( error, MessageImportSyncStep.MESSAGE_LIST_FETCH, - messageChannel, + messageChannel as unknown as MessageChannelWorkspaceEntity, workspaceId, ); } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts index a0ac85a9c93..5e6987fb31d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts @@ -3,6 +3,7 @@ import { Scope } from '@nestjs/common'; 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 { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; @@ -14,6 +15,16 @@ import { import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; +const toIsoStringOrNull = ( + value: string | Date | null | undefined, +): string | null => { + if (value == null) { + return null; + } + + return value instanceof Date ? value.toISOString() : value; +}; + export type MessagingMessagesImportJobData = { messageChannelId: string; workspaceId: string; @@ -29,6 +40,7 @@ export class MessagingMessagesImportJob { private readonly messagingMonitoringService: MessagingMonitoringService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, ) {} @Process(MessagingMessagesImportJob.name) @@ -44,18 +56,15 @@ export class MessagingMessagesImportJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const messageChannel = await messageChannelRepository.findOne({ - where: { - id: messageChannelId, + const messageChannel = await this.messageChannelDataAccessService.findOne( + workspaceId, + { + where: { + id: messageChannelId, + }, + relations: ['connectedAccount', 'messageFolders'], }, - relations: ['connectedAccount', 'messageFolders'], - }); + ); if (!messageChannel) { await this.messagingMonitoringService.track({ @@ -80,9 +89,9 @@ export class MessagingMessagesImportJob { if ( isThrottled( - messageChannel.syncStageStartedAt, + toIsoStringOrNull(messageChannel.syncStageStartedAt), messageChannel.throttleFailureCount, - messageChannel.throttleRetryAfter, + toIsoStringOrNull(messageChannel.throttleRetryAfter), ) ) { await this.messageChannelSyncStatusService.markAsMessagesImportPending( @@ -94,9 +103,12 @@ export class MessagingMessagesImportJob { return; } + const messageChannelWorkspace = + messageChannel as unknown as MessageChannelWorkspaceEntity; + await this.messagingMessagesImportService.processMessageBatchImport( - messageChannel, - messageChannel.connectedAccount, + messageChannelWorkspace, + messageChannelWorkspace.connectedAccount, workspaceId, ); }, authContext); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts index 561850742cb..25aa1ccdfdd 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts @@ -5,15 +5,23 @@ import { In } from 'typeorm'; 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 { 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'; import { isSyncStale } from 'src/modules/messaging/message-import-manager/utils/is-sync-stale.util'; +const toIsoStringOrNull = ( + value: string | Date | null | undefined, +): string | null => { + if (value == null) { + return null; + } + + return value instanceof Date ? value.toISOString() : value; +}; + export type MessagingOngoingStaleJobData = { workspaceId: string; }; @@ -26,6 +34,7 @@ export class MessagingOngoingStaleJob { private readonly logger = new Logger(MessagingOngoingStaleJob.name); constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, ) {} @@ -36,14 +45,9 @@ export class MessagingOngoingStaleJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - const messageChannels = await messageChannelRepository.find({ - where: { + const messageChannels = await this.messageChannelDataAccessService.find( + workspaceId, + { syncStage: In([ MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, @@ -51,10 +55,10 @@ export class MessagingOngoingStaleJob { MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, ]), }, - }); + ); for (const messageChannel of messageChannels) { - if (isSyncStale(messageChannel.syncStageStartedAt)) { + if (isSyncStale(toIsoStringOrNull(messageChannel.syncStageStartedAt))) { await this.messageChannelSyncStatusService.resetSyncStageStartedAt( [messageChannel.id], workspaceId, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts index d2a03bc7f6b..8166fdbcff5 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts @@ -3,12 +3,12 @@ import { Scope } from '@nestjs/common'; 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 { MessageChannelSyncStage, MessageChannelSyncStatus, - MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; export type MessagingRelaunchFailedMessageChannelJobData = { @@ -23,6 +23,7 @@ export type MessagingRelaunchFailedMessageChannelJobData = { export class MessagingRelaunchFailedMessageChannelJob { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, ) {} @Process(MessagingRelaunchFailedMessageChannelJob.name) @@ -32,23 +33,14 @@ export class MessagingRelaunchFailedMessageChannelJob { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - { shouldBypassPermissionChecks: true }, - ); - - const messageChannel = await messageChannelRepository.findOne({ - where: { - id: messageChannelId, - }, - relations: { - connectedAccount: { - accountOwner: true, + const messageChannel = await this.messageChannelDataAccessService.findOne( + workspaceId, + { + where: { + id: messageChannelId, }, }, - }); + ); if ( !messageChannel || @@ -58,10 +50,14 @@ export class MessagingRelaunchFailedMessageChannelJob { return; } - await messageChannelRepository.update(messageChannelId, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - syncStatus: MessageChannelSyncStatus.ACTIVE, - }); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: messageChannelId }, + { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + syncStatus: MessageChannelSyncStatus.ACTIVE, + }, + ); }, authContext); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index 5f28d291ac9..56839daa8e3 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -4,6 +4,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; +import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; @@ -61,6 +63,8 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess MessagingIMAPDriverModule, MessagingSmtpDriverModule, MessagingCommonModule, + MessageChannelDataAccessModule, + MessageFolderDataAccessModule, TypeOrmModule.forFeature([ WorkspaceEntity, DataSourceEntity, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts index c09c46bdee5..5a9ce48249c 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts @@ -24,6 +24,7 @@ import { MessagingMessageListFetchService } from 'src/modules/messaging/message- import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service'; import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; describe('MessagingMessageListFetchService', () => { let messagingMessageListFetchService: MessagingMessageListFetchService; @@ -229,6 +230,12 @@ describe('MessagingMessageListFetchService', () => { }), }, }, + { + provide: MessageChannelDataAccessService, + useValue: { + findOne: jest.fn().mockResolvedValue(undefined), + }, + }, { provide: MessagingCursorService, useValue: { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts index b338cebc11f..3f02c404e8b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts @@ -22,6 +22,7 @@ import { MessagingGetMessagesService } from 'src/modules/messaging/message-impor import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service'; import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; describe('MessagingMessagesImportService', () => { @@ -127,6 +128,12 @@ describe('MessagingMessagesImportService', () => { .mockImplementation((fn: () => any, _authContext?: any) => fn()), }, }, + { + provide: MessageChannelDataAccessService, + useValue: { + update: jest.fn().mockResolvedValue(undefined), + }, + }, { provide: MessagingGetMessagesService, useValue: { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts index fc43c3e1439..b085d209154 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts @@ -1,14 +1,17 @@ import { Injectable } from '@nestjs/common'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-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 MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; @Injectable() export class MessagingCursorService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, ) {} public async updateCursor( @@ -20,19 +23,9 @@ export class MessagingCursorService { const authContext = buildSystemAuthContext(workspaceId); await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - const folderRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageFolder', - ); - if (!folderId) { - await messageChannelRepository.update( + await this.messageChannelDataAccessService.update( + workspaceId, { id: messageChannel.id, }, @@ -48,7 +41,8 @@ export class MessagingCursorService { }, ); } else { - await folderRepository.update( + await this.messageFolderDataAccessService.update( + workspaceId, { id: folderId, }, @@ -56,7 +50,8 @@ export class MessagingCursorService { syncCursor: nextSyncCursor, }, ); - await messageChannelRepository.update( + await this.messageChannelDataAccessService.update( + workspaceId, { id: messageChannel.id, }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts index dc3fa1e16f9..3775afe1104 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts @@ -7,8 +7,7 @@ import { type TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; -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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { MessageChannelSyncStatus, @@ -30,7 +29,7 @@ export enum MessageImportSyncStep { @Injectable() export class MessageImportExceptionHandlerService { constructor( - private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -152,37 +151,27 @@ export class MessageImportExceptionHandlerService { return; } - const authContext = buildSystemAuthContext(workspaceId); + await this.messageChannelDataAccessService.increment( + workspaceId, + { id: messageChannel.id }, + 'throttleFailureCount', + 1, + ); - await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); + const throttleRetryAfter = + exception instanceof MessageImportDriverException + ? exception.throttleRetryAfter + : undefined; - await messageChannelRepository.increment( - { id: messageChannel.id }, - 'throttleFailureCount', - 1, - undefined, - ['throttleFailureCount', 'id'], - ); - - const throttleRetryAfter = - exception instanceof MessageImportDriverException - ? exception.throttleRetryAfter - : undefined; - - await messageChannelRepository.update( - { id: messageChannel.id }, - { - throttleRetryAfter: isDefined(throttleRetryAfter) - ? throttleRetryAfter.toISOString() - : null, - }, - ); - }, authContext); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: messageChannel.id }, + { + throttleRetryAfter: isDefined(throttleRetryAfter) + ? throttleRetryAfter.toISOString() + : null, + }, + ); switch (syncStep) { case MessageImportSyncStep.MESSAGE_LIST_FETCH: diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts index 77be3c02072..f351f761661 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts @@ -8,6 +8,7 @@ import { In, MoreThanOrEqual } from 'typeorm'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +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 { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; @@ -41,6 +42,7 @@ export class MessagingMessageListFetchService { private readonly cacheStorage: CacheStorageService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messagingGetMessageListService: MessagingGetMessageListService, private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, private readonly messagingMessageCleanerService: MessagingMessageCleanerService, @@ -78,15 +80,9 @@ export class MessagingMessageListFetchService { `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Processing message list fetch`, ); - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - const freshMessageChannel = pendingGroupEmailActionsProcessed || pendingFolderActionsProcessed - ? await messageChannelRepository.findOne({ + ? await this.messageChannelDataAccessService.findOne(workspaceId, { where: { id: messageChannel.id, }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts index 8265adc5bcd..dc36a753097 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts @@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; 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'; @@ -46,6 +47,7 @@ export class MessagingMessagesImportService { private readonly blocklistRepository: BlocklistRepository, private readonly emailAliasManagerService: EmailAliasManagerService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly messagingGetMessagesService: MessagingGetMessagesService, private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService, @@ -196,13 +198,8 @@ export class MessagingMessagesImportService { ); } - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update( + await this.messageChannelDataAccessService.update( + workspaceId, { id: messageChannel.id, }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts index 6e955e43cf6..10b3891a2e1 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; -import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-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 MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -21,6 +21,7 @@ export class MessagingProcessFolderActionsService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, private readonly messagingDeleteFolderMessagesService: MessagingDeleteFolderMessagesService, ) {} @@ -91,41 +92,27 @@ export class MessagingProcessFolderActionsService { await this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const workspaceDataSource = - await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource(); + if (processedFolderIds.length > 0) { + await this.messageFolderDataAccessService.update( + workspaceId, + { id: In(processedFolderIds) }, + { pendingSyncAction: MessageFolderPendingSyncAction.NONE }, + ); - await workspaceDataSource?.transaction( - async (transactionManager: WorkspaceEntityManager) => { - const messageFolderRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageFolder', - ); + this.logger.debug( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Reset pendingSyncAction to NONE for ${processedFolderIds.length} folders`, + ); + } - if (processedFolderIds.length > 0) { - await messageFolderRepository.update( - { id: In(processedFolderIds) }, - { pendingSyncAction: MessageFolderPendingSyncAction.NONE }, - transactionManager, - ); + if (folderIdsToDelete.length > 0) { + await this.messageFolderDataAccessService.delete(workspaceId, { + id: In(folderIdsToDelete), + }); - this.logger.debug( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Reset pendingSyncAction to NONE for ${processedFolderIds.length} folders`, - ); - } - - if (folderIdsToDelete.length > 0) { - await messageFolderRepository.delete( - { id: In(folderIdsToDelete) }, - transactionManager, - ); - - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Deleted ${folderIdsToDelete.length} folders`, - ); - } - }, - ); + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Deleted ${folderIdsToDelete.length} folders`, + ); + } }, authContext, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts index f3f36c07d86..3ea315938e8 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts @@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; +import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; 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'; @@ -9,7 +11,6 @@ import { MessageChannelPendingGroupEmailsAction, MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { MessagingDeleteGroupEmailMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service'; @Injectable() @@ -20,6 +21,8 @@ export class MessagingProcessGroupEmailActionsService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, private readonly messagingDeleteGroupEmailMessagesService: MessagingDeleteGroupEmailMessagesService, ) {} @@ -28,24 +31,15 @@ export class MessagingProcessGroupEmailActionsService { workspaceId: string, pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction, ): Promise { - const authContext = buildSystemAuthContext(workspaceId); + await this.messageChannelDataAccessService.update( + workspaceId, + { id: messageChannel.id }, + { pendingGroupEmailsAction }, + ); - await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update( - { id: messageChannel.id }, - { pendingGroupEmailsAction }, - ); - - this.logger.debug( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Marked message channel as pending group emails action: ${pendingGroupEmailsAction}`, - ); - }, authContext); + this.logger.debug( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Marked message channel as pending group emails action: ${pendingGroupEmailsAction}`, + ); } async processGroupEmailActions( @@ -74,12 +68,6 @@ export class MessagingProcessGroupEmailActionsService { await workspaceDataSource?.transaction( async (transactionManager: WorkspaceEntityManager) => { try { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - switch (pendingGroupEmailsAction) { case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION: await this.handleGroupEmailsDeletion( @@ -97,7 +85,8 @@ export class MessagingProcessGroupEmailActionsService { break; } - await messageChannelRepository.update( + await this.messageChannelDataAccessService.update( + workspaceId, { id: messageChannel.id }, { pendingGroupEmailsAction: @@ -167,27 +156,17 @@ export class MessagingProcessGroupEmailActionsService { messageChannelId: string; transactionManager: WorkspaceEntityManager; }) { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageChannel', - ); - - await messageChannelRepository.update( - messageChannelId, + await this.messageChannelDataAccessService.update( + workspaceId, + { id: messageChannelId }, { syncCursor: '', }, transactionManager, ); - const messageFolderRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'messageFolder', - ); - - await messageFolderRepository.update( + await this.messageFolderDataAccessService.update( + workspaceId, { messageChannelId }, { syncCursor: '' }, transactionManager, diff --git a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts index aeed0f0cfc4..0f86f646cf2 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts @@ -10,9 +10,9 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces 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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +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 MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; export const MESSAGING_MESSAGE_CHANNEL_SYNC_STATUS_MONITORING_CRON_PATTERN = @@ -25,6 +25,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { private readonly workspaceRepository: Repository, private readonly messagingMonitoringService: MessagingMonitoringService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + private readonly messageChannelDataAccessService: MessageChannelDataAccessService, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -51,14 +52,13 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { await this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { - const messageChannelRepository = - await this.globalWorkspaceOrmManager.getRepository( + const messageChannels = + await this.messageChannelDataAccessService.findMany( activeWorkspace.id, - 'messageChannel', + { + select: ['id', 'syncStatus', 'connectedAccountId'], + }, ); - const messageChannels = await messageChannelRepository.find({ - select: ['id', 'syncStatus', 'connectedAccountId'], - }); for (const messageChannel of messageChannels) { if (!messageChannel.syncStatus) { diff --git a/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts b/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts index f4d7d98f99b..95d5360c1b0 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command'; import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job'; @@ -12,6 +13,7 @@ import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/ser @Module({ imports: [ AuditModule, + MessageChannelDataAccessModule, MessagingCommonModule, TypeOrmModule.forFeature([WorkspaceEntity]), TypeOrmModule.forFeature([DataSourceEntity]), diff --git a/packages/twenty-shared/src/types/CalendarChannelContactAutoCreationPolicy.ts b/packages/twenty-shared/src/types/CalendarChannelContactAutoCreationPolicy.ts new file mode 100644 index 00000000000..d75065ebe0f --- /dev/null +++ b/packages/twenty-shared/src/types/CalendarChannelContactAutoCreationPolicy.ts @@ -0,0 +1,6 @@ +export enum CalendarChannelContactAutoCreationPolicy { + AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER', + AS_PARTICIPANT = 'AS_PARTICIPANT', + AS_ORGANIZER = 'AS_ORGANIZER', + NONE = 'NONE', +} diff --git a/packages/twenty-shared/src/types/CalendarChannelSyncStage.ts b/packages/twenty-shared/src/types/CalendarChannelSyncStage.ts new file mode 100644 index 00000000000..d8713a08eb3 --- /dev/null +++ b/packages/twenty-shared/src/types/CalendarChannelSyncStage.ts @@ -0,0 +1,10 @@ +export enum CalendarChannelSyncStage { + PENDING_CONFIGURATION = 'PENDING_CONFIGURATION', + CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING', + CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED', + CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING', + CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING', + CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED', + CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING', + FAILED = 'FAILED', +} diff --git a/packages/twenty-shared/src/types/CalendarChannelSyncStatus.ts b/packages/twenty-shared/src/types/CalendarChannelSyncStatus.ts new file mode 100644 index 00000000000..1eb42a24893 --- /dev/null +++ b/packages/twenty-shared/src/types/CalendarChannelSyncStatus.ts @@ -0,0 +1,7 @@ +export enum CalendarChannelSyncStatus { + NOT_SYNCED = 'NOT_SYNCED', + ONGOING = 'ONGOING', + ACTIVE = 'ACTIVE', + FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', + FAILED_UNKNOWN = 'FAILED_UNKNOWN', +} diff --git a/packages/twenty-shared/src/types/CalendarChannelVisibility.ts b/packages/twenty-shared/src/types/CalendarChannelVisibility.ts new file mode 100644 index 00000000000..151a43ae16e --- /dev/null +++ b/packages/twenty-shared/src/types/CalendarChannelVisibility.ts @@ -0,0 +1,4 @@ +export enum CalendarChannelVisibility { + METADATA = 'METADATA', + SHARE_EVERYTHING = 'SHARE_EVERYTHING', +} diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index 9a21153eb28..d7bd4e01eb2 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -20,4 +20,5 @@ export enum FeatureFlagKey { IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED', IS_RICH_TEXT_V1_MIGRATED = 'IS_RICH_TEXT_V1_MIGRATED', IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED', + IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED', } diff --git a/packages/twenty-shared/src/types/MessageChannelContactAutoCreationPolicy.ts b/packages/twenty-shared/src/types/MessageChannelContactAutoCreationPolicy.ts new file mode 100644 index 00000000000..007546e5963 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageChannelContactAutoCreationPolicy.ts @@ -0,0 +1,5 @@ +export enum MessageChannelContactAutoCreationPolicy { + SENT_AND_RECEIVED = 'SENT_AND_RECEIVED', + SENT = 'SENT', + NONE = 'NONE', +} diff --git a/packages/twenty-shared/src/types/MessageChannelPendingGroupEmailsAction.ts b/packages/twenty-shared/src/types/MessageChannelPendingGroupEmailsAction.ts new file mode 100644 index 00000000000..533ab8f9807 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageChannelPendingGroupEmailsAction.ts @@ -0,0 +1,5 @@ +export enum MessageChannelPendingGroupEmailsAction { + GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION', + GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT', + NONE = 'NONE', +} diff --git a/packages/twenty-shared/src/types/MessageChannelSyncStage.ts b/packages/twenty-shared/src/types/MessageChannelSyncStage.ts new file mode 100644 index 00000000000..bcf0daa2fa1 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageChannelSyncStage.ts @@ -0,0 +1,10 @@ +export enum MessageChannelSyncStage { + PENDING_CONFIGURATION = 'PENDING_CONFIGURATION', + MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING', + MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED', + MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING', + MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING', + MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED', + MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING', + FAILED = 'FAILED', +} diff --git a/packages/twenty-shared/src/types/MessageChannelSyncStatus.ts b/packages/twenty-shared/src/types/MessageChannelSyncStatus.ts new file mode 100644 index 00000000000..1375715c104 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageChannelSyncStatus.ts @@ -0,0 +1,7 @@ +export enum MessageChannelSyncStatus { + NOT_SYNCED = 'NOT_SYNCED', + ONGOING = 'ONGOING', + ACTIVE = 'ACTIVE', + FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', + FAILED_UNKNOWN = 'FAILED_UNKNOWN', +} diff --git a/packages/twenty-shared/src/types/MessageChannelType.ts b/packages/twenty-shared/src/types/MessageChannelType.ts new file mode 100644 index 00000000000..8b091f68291 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageChannelType.ts @@ -0,0 +1,4 @@ +export enum MessageChannelType { + EMAIL = 'EMAIL', + SMS = 'SMS', +} diff --git a/packages/twenty-shared/src/types/MessageChannelVisibility.ts b/packages/twenty-shared/src/types/MessageChannelVisibility.ts new file mode 100644 index 00000000000..d36491e1ad1 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageChannelVisibility.ts @@ -0,0 +1,5 @@ +export enum MessageChannelVisibility { + METADATA = 'METADATA', + SUBJECT = 'SUBJECT', + SHARE_EVERYTHING = 'SHARE_EVERYTHING', +} diff --git a/packages/twenty-shared/src/types/MessageFolderImportPolicy.ts b/packages/twenty-shared/src/types/MessageFolderImportPolicy.ts new file mode 100644 index 00000000000..120d3c32612 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageFolderImportPolicy.ts @@ -0,0 +1,4 @@ +export enum MessageFolderImportPolicy { + ALL_FOLDERS = 'ALL_FOLDERS', + SELECTED_FOLDERS = 'SELECTED_FOLDERS', +} diff --git a/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts b/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts new file mode 100644 index 00000000000..9e24eeba8e7 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts @@ -0,0 +1,4 @@ +export enum MessageFolderPendingSyncAction { + FOLDER_DELETION = 'FOLDER_DELETION', + NONE = 'NONE', +} diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index 147993ff8ae..bb5d483b9a0 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -14,6 +14,10 @@ export { AppBasePath } from './AppBasePath'; export { AppPath } from './AppPath'; export type { Arrayable } from './Arrayable'; export type { ArraySortDirection } from './ArraySortDirection'; +export { CalendarChannelContactAutoCreationPolicy } from './CalendarChannelContactAutoCreationPolicy'; +export { CalendarChannelSyncStage } from './CalendarChannelSyncStage'; +export { CalendarChannelSyncStatus } from './CalendarChannelSyncStatus'; +export { CalendarChannelVisibility } from './CalendarChannelVisibility'; export type { CommandMenuContextApi } from './CommandMenuContextApi'; export { CommandMenuContextApiPageType } from './CommandMenuContextApiPageType'; export { CommandMenuItemViewType } from './CommandMenuItemViewType'; @@ -125,6 +129,14 @@ export type { IsExactly } from './IsExactly'; export type { IsNever } from './IsNever.type'; export type { IsSerializedRelation } from './IsSerializedRelation.type'; export type { LogicFunctionEvent } from './LogicFunctionEvent'; +export { MessageChannelContactAutoCreationPolicy } from './MessageChannelContactAutoCreationPolicy'; +export { MessageChannelPendingGroupEmailsAction } from './MessageChannelPendingGroupEmailsAction'; +export { MessageChannelSyncStage } from './MessageChannelSyncStage'; +export { MessageChannelSyncStatus } from './MessageChannelSyncStatus'; +export { MessageChannelType } from './MessageChannelType'; +export { MessageChannelVisibility } from './MessageChannelVisibility'; +export { MessageFolderImportPolicy } from './MessageFolderImportPolicy'; +export { MessageFolderPendingSyncAction } from './MessageFolderPendingSyncAction'; export { MessageParticipantRole } from './MessageParticipantRole'; export type { MetadataGqlOperationSignature } from './MetadataGqlOperationSignature'; export type { ModifiedProperties } from './ModifiedProperties';