diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 3a77339837..e3372ecbc4 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -770,7 +770,6 @@ export type CalendarChannel = { id: Scalars['UUID']; isContactAutoCreationEnabled: Scalars['Boolean']; isSyncEnabled: Scalars['Boolean']; - syncCursor?: Maybe; syncStage: CalendarChannelSyncStage; syncStageStartedAt?: Maybe; syncStatus: CalendarChannelSyncStatus; @@ -979,18 +978,14 @@ export type ConfigVariablesGroupData = { 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']; @@ -1077,17 +1072,6 @@ 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; @@ -1103,16 +1087,6 @@ 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; @@ -1159,33 +1133,6 @@ 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; @@ -2421,7 +2368,6 @@ export type MessageChannel = { isSyncEnabled: Scalars['Boolean']; messageFolderImportPolicy: MessageFolderImportPolicy; pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction; - syncCursor?: Maybe; syncStage: MessageChannelSyncStage; syncStageStartedAt?: Maybe; syncStatus: MessageChannelSyncStatus; @@ -2486,7 +2432,6 @@ export type MessageFolder = { name?: Maybe; parentFolderId?: Maybe; pendingSyncAction: MessageFolderPendingSyncAction; - syncCursor?: Maybe; updatedAt: Scalars['DateTime']; }; @@ -2572,10 +2517,8 @@ export type Mutation = { createApplicationRegistration: CreateApplicationRegistration; createApplicationRegistrationVariable: ApplicationRegistrationVariable; createApprovedAccessDomain: ApprovedAccessDomain; - createCalendarChannel: CalendarChannel; createChatThread: AgentChatThread; createCommandMenuItem: CommandMenuItem; - createConnectedAccount: ConnectedAccountDto; createDatabaseConfigVariable: Scalars['Boolean']; createDevelopmentApplication: DevelopmentApplication; createEmailingDomain: EmailingDomain; @@ -2583,8 +2526,6 @@ export type Mutation = { createManyViewFieldGroups: Array; createManyViewFields: Array; createManyViewGroups: Array; - createMessageChannel: MessageChannel; - createMessageFolder: MessageFolder; createNavigationMenuItem: NavigationMenuItem; createOIDCIdentityProvider: SetupSso; createObjectEvent: Analytics; @@ -2612,7 +2553,6 @@ export type Mutation = { deleteApplicationRegistration: Scalars['Boolean']; deleteApplicationRegistrationVariable: Scalars['Boolean']; deleteApprovedAccessDomain: Scalars['Boolean']; - deleteCalendarChannel: CalendarChannel; deleteCommandMenuItem: CommandMenuItem; deleteConnectedAccount: ConnectedAccountDto; deleteCurrentWorkspace: Workspace; @@ -2620,8 +2560,6 @@ export type Mutation = { deleteEmailingDomain: Scalars['Boolean']; deleteFrontComponent: FrontComponent; deleteJobs: DeleteJobsResponse; - deleteMessageChannel: MessageChannel; - deleteMessageFolder: MessageFolder; deleteNavigationMenuItem: NavigationMenuItem; deleteOneAgent: Agent; deleteOneField: Field; @@ -2708,12 +2646,12 @@ export type Mutation = { updateApplicationRegistrationVariable: ApplicationRegistrationVariable; updateCalendarChannel: CalendarChannel; updateCommandMenuItem: CommandMenuItem; - updateConnectedAccount: ConnectedAccountDto; updateDatabaseConfigVariable: Scalars['Boolean']; updateFrontComponent: FrontComponent; updateLabPublicFeatureFlag: FeatureFlag; updateMessageChannel: MessageChannel; updateMessageFolder: MessageFolder; + updateMessageFolders: Array; updateNavigationMenuItem: NavigationMenuItem; updateOneAgent: Agent; updateOneApplicationVariable: Scalars['Boolean']; @@ -2831,21 +2769,11 @@ 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']; @@ -2884,16 +2812,6 @@ export type MutationCreateManyViewGroupsArgs = { }; -export type MutationCreateMessageChannelArgs = { - input: CreateMessageChannelInput; -}; - - -export type MutationCreateMessageFolderArgs = { - input: CreateMessageFolderInput; -}; - - export type MutationCreateNavigationMenuItemArgs = { input: CreateNavigationMenuItemInput; }; @@ -3032,11 +2950,6 @@ export type MutationDeleteApprovedAccessDomainArgs = { }; -export type MutationDeleteCalendarChannelArgs = { - id: Scalars['UUID']; -}; - - export type MutationDeleteCommandMenuItemArgs = { id: Scalars['UUID']; }; @@ -3068,16 +2981,6 @@ export type MutationDeleteJobsArgs = { }; -export type MutationDeleteMessageChannelArgs = { - id: Scalars['UUID']; -}; - - -export type MutationDeleteMessageFolderArgs = { - id: Scalars['UUID']; -}; - - export type MutationDeleteNavigationMenuItemArgs = { id: Scalars['UUID']; }; @@ -3490,11 +3393,6 @@ export type MutationUpdateCommandMenuItemArgs = { }; -export type MutationUpdateConnectedAccountArgs = { - input: UpdateConnectedAccountInput; -}; - - export type MutationUpdateDatabaseConfigVariableArgs = { key: Scalars['String']; value: Scalars['JSON']; @@ -3521,6 +3419,11 @@ export type MutationUpdateMessageFolderArgs = { }; +export type MutationUpdateMessageFoldersArgs = { + input: UpdateMessageFoldersInput; +}; + + export type MutationUpdateNavigationMenuItemArgs = { input: UpdateOneNavigationMenuItemInput; }; @@ -4222,8 +4125,6 @@ export type Query = { applicationRegistrationTarballUrl?: Maybe; barChartData: BarChartData; billingPortalSession: BillingSession; - calendarChannel?: Maybe; - calendarChannels: Array; chatMessages: Array; chatThread: AgentChatThread; chatThreads: AgentChatThreadConnection; @@ -4231,7 +4132,6 @@ export type Query = { checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; commandMenuItem?: Maybe; commandMenuItems: Array; - connectedAccount?: Maybe; connectedAccounts: Array; currentUser: User; currentWorkspace: Workspace; @@ -4307,11 +4207,11 @@ export type Query = { indexMetadatas: IndexConnection; lineChartData: LineChartData; listPlans: Array; - messageChannel?: Maybe; - messageChannels: Array; - messageFolder?: Maybe; - messageFolders: Array; minimalMetadata: MinimalMetadata; + myCalendarChannels: Array; + myConnectedAccounts: Array; + myMessageChannels: Array; + myMessageFolders: Array; navigationMenuItem?: Maybe; navigationMenuItems: Array; object: Object; @@ -4352,16 +4252,6 @@ export type QueryBillingPortalSessionArgs = { }; -export type QueryCalendarChannelArgs = { - id: Scalars['UUID']; -}; - - -export type QueryCalendarChannelsArgs = { - connectedAccountId?: InputMaybe; -}; - - export type QueryChatMessagesArgs = { threadId: Scalars['UUID']; }; @@ -4395,11 +4285,6 @@ export type QueryCommandMenuItemArgs = { }; -export type QueryConnectedAccountArgs = { - id: Scalars['UUID']; -}; - - export type QueryEnterpriseCheckoutSessionArgs = { billingInterval?: InputMaybe; }; @@ -4663,22 +4548,17 @@ export type QueryLineChartDataArgs = { }; -export type QueryMessageChannelArgs = { - id: Scalars['UUID']; -}; - - -export type QueryMessageChannelsArgs = { +export type QueryMyCalendarChannelsArgs = { connectedAccountId?: InputMaybe; }; -export type QueryMessageFolderArgs = { - id: Scalars['UUID']; +export type QueryMyMessageChannelsArgs = { + connectedAccountId?: InputMaybe; }; -export type QueryMessageFoldersArgs = { +export type QueryMyMessageFoldersArgs = { messageChannelId?: InputMaybe; }; @@ -5222,18 +5102,6 @@ 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; @@ -5312,9 +5180,11 @@ export type UpdateMessageFolderInput = { export type UpdateMessageFolderInputUpdates = { isSynced?: InputMaybe; - name?: InputMaybe; - pendingSyncAction?: InputMaybe; - syncCursor?: InputMaybe; +}; + +export type UpdateMessageFoldersInput = { + ids: Array; + update: UpdateMessageFolderInputUpdates; }; export type UpdateNavigationMenuItemInput = { @@ -6954,6 +6824,13 @@ export type PieChartDataQueryVariables = Exact<{ export type PieChartDataQuery = { __typename?: 'Query', pieChartData: { __typename?: 'PieChartData', showLegend: boolean, showDataLabels: boolean, showCenterMetric: boolean, hasTooManyGroups: boolean, formattedToRawLookup: any, data: Array<{ __typename?: 'PieChartDataItem', id: string, value: number }> } }; +export type DeleteConnectedAccountMutationVariables = Exact<{ + id: Scalars['UUID']; +}>; + + +export type DeleteConnectedAccountMutation = { __typename?: 'Mutation', deleteConnectedAccount: { __typename?: 'ConnectedAccountDTO', id: string } }; + export type SaveImapSmtpCaldavAccountMutationVariables = Exact<{ accountOwnerId: Scalars['UUID']; handle: Scalars['String']; @@ -6971,6 +6848,34 @@ export type StartChannelSyncMutationVariables = Exact<{ export type StartChannelSyncMutation = { __typename?: 'Mutation', startChannelSync: { __typename?: 'ChannelSyncSuccess', success: boolean } }; +export type UpdateCalendarChannelMutationVariables = Exact<{ + input: UpdateCalendarChannelInput; +}>; + + +export type UpdateCalendarChannelMutation = { __typename?: 'Mutation', updateCalendarChannel: { __typename?: 'CalendarChannel', id: string, visibility: CalendarChannelVisibility, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy } }; + +export type UpdateMessageChannelMutationVariables = Exact<{ + input: UpdateMessageChannelInput; +}>; + + +export type UpdateMessageChannelMutation = { __typename?: 'Mutation', updateMessageChannel: { __typename?: 'MessageChannel', id: string, visibility: MessageChannelVisibility, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, messageFolderImportPolicy: MessageFolderImportPolicy } }; + +export type UpdateMessageFolderMutationVariables = Exact<{ + input: UpdateMessageFolderInput; +}>; + + +export type UpdateMessageFolderMutation = { __typename?: 'Mutation', updateMessageFolder: { __typename?: 'MessageFolder', id: string, isSynced: boolean } }; + +export type UpdateMessageFoldersMutationVariables = Exact<{ + input: UpdateMessageFoldersInput; +}>; + + +export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean }> }; + export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ id: Scalars['UUID']; }>; @@ -6978,6 +6883,32 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } }; +export type MyCalendarChannelsQueryVariables = Exact<{ + connectedAccountId?: InputMaybe; +}>; + + +export type MyCalendarChannelsQuery = { __typename?: 'Query', myCalendarChannels: Array<{ __typename?: 'CalendarChannel', id: string, handle: string, visibility: CalendarChannelVisibility, syncStatus: CalendarChannelSyncStatus, syncStage: CalendarChannelSyncStage, syncStageStartedAt?: string | null, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy, isSyncEnabled: boolean, connectedAccountId: string, createdAt: string, updatedAt: string }> }; + +export type MyConnectedAccountsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type MyConnectedAccountsQuery = { __typename?: 'Query', myConnectedAccounts: Array<{ __typename?: 'ConnectedAccountDTO', id: string, handle: string, provider: string, authFailedAt?: string | null, scopes?: Array | null, handleAliases?: Array | null, lastSignedInAt?: string | null, userWorkspaceId: string, createdAt: string, updatedAt: string }> }; + +export type MyMessageChannelsQueryVariables = Exact<{ + connectedAccountId?: InputMaybe; +}>; + + +export type MyMessageChannelsQuery = { __typename?: 'Query', myMessageChannels: Array<{ __typename?: 'MessageChannel', id: string, handle: string, visibility: MessageChannelVisibility, type: MessageChannelType, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, messageFolderImportPolicy: MessageFolderImportPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, isSyncEnabled: boolean, syncStatus: MessageChannelSyncStatus, syncStage: MessageChannelSyncStage, syncStageStartedAt?: string | null, connectedAccountId: string, createdAt: string, updatedAt: string }> }; + +export type MyMessageFoldersQueryVariables = Exact<{ + messageChannelId?: InputMaybe; +}>; + + +export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, messageChannelId: string, createdAt: string, updatedAt: string }> }; + export type SetAdminAiModelEnabledMutationVariables = Exact<{ modelId: Scalars['String']; enabled: Scalars['Boolean']; @@ -8079,9 +8010,18 @@ export const FindAllRecordPageLayoutsDocument = {"kind":"Document","definitions" export const BarChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BarChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BarChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"barChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"indexBy"}},{"kind":"Field","name":{"kind":"Name","value":"keys"}},{"kind":"Field","name":{"kind":"Name","value":"series"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"yAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"groupMode"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode; export const LineChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LineChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LineChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lineChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"series"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"xAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"yAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode; export const PieChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PieChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PieChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pieChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"showCenterMetric"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteConnectedAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteConnectedAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteConnectedAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SaveImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SaveImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accountOwnerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"handle"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmailAccountConnectionParameters"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"saveImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accountOwnerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accountOwnerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"handle"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handle"}}},{"kind":"Argument","name":{"kind":"Name","value":"connectionParameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}}]}}]}}]} as unknown as DocumentNode; export const StartChannelSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StartChannelSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startChannelSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCalendarChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCalendarChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCalendarChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode; export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"accountOwnerId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"secure"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"secure"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; +export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; +export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; +export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const SetAdminAiModelEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelEnabled"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}}]}]}}]} as unknown as DocumentNode; export const GetAdminAiModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"autoEnableNewModels"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"modelFamily"}},{"kind":"Field","name":{"kind":"Name","value":"inferenceProvider"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isAdminEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"deprecated"}},{"kind":"Field","name":{"kind":"Name","value":"isRecommended"}}]}}]}}]}}]} as unknown as DocumentNode; export const FindAllApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx index 23152a8ed5..be0e4d1095 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx @@ -1,6 +1,9 @@ import { type CalendarChannel } from '@/accounts/types/CalendarChannel'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { UPDATE_CALENDAR_CHANNEL } from '@/settings/accounts/graphql/mutations/updateCalendarChannel'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useMutation } from '@apollo/client/react'; import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard'; import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; import { styled } from '@linaria/react'; @@ -27,26 +30,33 @@ type SettingsAccountsCalendarChannelDetailsProps = { export const SettingsAccountsCalendarChannelDetails = ({ calendarChannel, }: SettingsAccountsCalendarChannelDetailsProps) => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + const { updateOneRecord } = useUpdateOneRecord(); + const [updateMetadataChannel] = useMutation(UPDATE_CALENDAR_CHANNEL); + + const updateChannel = (update: Record) => { + if (isMigrated) { + updateMetadataChannel({ + variables: { input: { id: calendarChannel.id, update } }, + }); + } else { + updateOneRecord({ + objectNameSingular: CoreObjectNameSingular.CalendarChannel, + idToUpdate: calendarChannel.id, + updateOneRecordInput: update, + }); + } + }; const handleVisibilityChange = (value: CalendarChannelVisibility) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.CalendarChannel, - idToUpdate: calendarChannel.id, - updateOneRecordInput: { - visibility: value, - }, - }); + updateChannel({ visibility: value }); }; const handleContactAutoCreationToggle = (value: boolean) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.CalendarChannel, - idToUpdate: calendarChannel.id, - updateOneRecordInput: { - isContactAutoCreationEnabled: value, - }, - }); + updateChannel({ isContactAutoCreationEnabled: value }); }; return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx index 729a9c0bee..dc496b74fb 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx @@ -1,17 +1,9 @@ import { styled } from '@linaria/react'; -import { - type CalendarChannel, - CalendarChannelSyncStage, -} from '@/accounts/types/CalendarChannel'; -import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails'; import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId'; +import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; @@ -27,33 +19,8 @@ export const SettingsAccountsCalendarChannelsContainer = () => { activeTabIdComponentState, SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID, ); - const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); - const { records: accounts } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ConnectedAccount, - filter: { - accountOwnerId: { - eq: currentWorkspaceMember?.id, - }, - }, - }); - - const { records: calendarChannels } = useFindManyRecords< - CalendarChannel & { - connectedAccount: ConnectedAccount; - } - >({ - objectNameSingular: CoreObjectNameSingular.CalendarChannel, - filter: { - connectedAccountId: { - in: accounts.map((account) => account.id), - }, - syncStage: { - neq: CalendarChannelSyncStage.PENDING_CONFIGURATION, - }, - }, - skip: !accounts.length, - }); + const { channels: calendarChannels } = useMyCalendarChannels(); const tabs = [ ...calendarChannels.map((calendarChannel) => ({ diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx index ad82d49a69..2cfafb624b 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx @@ -5,8 +5,11 @@ import { type MessageChannelContactAutoCreationPolicy, type MessageFolderImportPolicy, } from '@/accounts/types/MessageChannel'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { UPDATE_MESSAGE_CHANNEL } from '@/settings/accounts/graphql/mutations/updateMessageChannel'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useMutation } from '@apollo/client/react'; import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard'; import { SettingsAccountsMessageFolderCard } from '@/settings/accounts/components/SettingsAccountsMessageFolderCard'; import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard'; @@ -40,58 +43,49 @@ const StyledDetailsContainer = styled.div` export const SettingsAccountsMessageChannelDetails = ({ messageChannel, }: SettingsAccountsMessageChannelDetailsProps) => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + const { updateOneRecord } = useUpdateOneRecord(); + const [updateMetadataChannel] = useMutation(UPDATE_MESSAGE_CHANNEL); + + const updateChannel = (update: Record) => { + if (isMigrated) { + updateMetadataChannel({ + variables: { input: { id: messageChannel.id, update } }, + }); + } else { + updateOneRecord({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + idToUpdate: messageChannel.id, + updateOneRecordInput: update, + }); + } + }; const handleVisibilityChange = (value: MessageChannelVisibility) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - idToUpdate: messageChannel.id, - updateOneRecordInput: { - visibility: value, - }, - }); + updateChannel({ visibility: value }); }; const handleContactAutoCreationChange = ( value: MessageChannelContactAutoCreationPolicy, ) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - idToUpdate: messageChannel.id, - updateOneRecordInput: { - contactAutoCreationPolicy: value, - }, - }); + updateChannel({ contactAutoCreationPolicy: value }); }; const handleIsGroupEmailExcludedToggle = (value: boolean) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - idToUpdate: messageChannel.id, - updateOneRecordInput: { - excludeGroupEmails: value, - }, - }); + updateChannel({ excludeGroupEmails: value }); }; const handleIsNonProfessionalEmailExcludedToggle = (value: boolean) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - idToUpdate: messageChannel.id, - updateOneRecordInput: { - excludeNonProfessionalEmails: value, - }, - }); + updateChannel({ excludeNonProfessionalEmails: value }); }; const handleMessageFolderImportPolicyChange = ( value: MessageFolderImportPolicy, ) => { - updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - idToUpdate: messageChannel.id, - updateOneRecordInput: { messageFolderImportPolicy: value }, - }); + updateChannel({ messageFolderImportPolicy: value }); }; return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx index 3de4cadf96..9a61d9b125 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx @@ -1,23 +1,15 @@ import { styled } from '@linaria/react'; -import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; -import { - type MessageChannel, - MessageChannelSyncStage, -} from '@/accounts/types/MessageChannel'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; +import { SettingsAccountsSelectedMessageChannelEffect } from '@/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect'; import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; +import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels'; import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import React, { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { themeCssVariables } from 'twenty-ui/theme-constants'; @@ -31,48 +23,11 @@ export const SettingsAccountsMessageChannelsContainer = () => { activeTabIdComponentState, SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID, ); - const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); const setSettingsAccountsSelectedMessageChannel = useSetAtomState( settingsAccountsSelectedMessageChannelState, ); - const { records: accounts } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ConnectedAccount, - filter: { - accountOwnerId: { - eq: currentWorkspaceMember?.id, - }, - }, - }); - - const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - depth: 1, - }); - - const { records: messageChannels } = useFindManyRecords< - MessageChannel & { - connectedAccount: ConnectedAccount; - } - >({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - filter: { - connectedAccountId: { - in: accounts.map((account) => account.id), - }, - isSyncEnabled: { - eq: true, - }, - syncStage: { - neq: MessageChannelSyncStage.PENDING_CONFIGURATION, - }, - }, - recordGqlFields, - onCompleted: (data) => { - setSettingsAccountsSelectedMessageChannel(data[0]); - }, - skip: !accounts.length, - }); + const { channels: messageChannels } = useMyMessageChannels(); const tabs = messageChannels.map((messageChannel) => ({ id: messageChannel.id, @@ -97,6 +52,9 @@ export const SettingsAccountsMessageChannelsContainer = () => { return ( <> + {tabs.length > 1 && ( { - await destroyOneRecord(account.id); + if (isMigrated) { + await deleteConnectedAccountMutation({ + variables: { id: account.id }, + }); + await apolloClient.refetchQueries({ include: 'active' }); + } else { + await destroyOneRecord(account.id); + } }; return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx new file mode 100644 index 0000000000..023b41169b --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx @@ -0,0 +1,40 @@ +import { type MessageChannel } from '@/accounts/types/MessageChannel'; +import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; +import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { useEffect } from 'react'; + +type SettingsAccountsSelectedMessageChannelEffectProps = { + messageChannels: MessageChannel[]; +}; + +export const SettingsAccountsSelectedMessageChannelEffect = ({ + messageChannels, +}: SettingsAccountsSelectedMessageChannelEffectProps) => { + const activeTabId = useAtomComponentStateValue( + activeTabIdComponentState, + SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID, + ); + + const setSettingsAccountsSelectedMessageChannel = useSetAtomState( + settingsAccountsSelectedMessageChannelState, + ); + + useEffect(() => { + if (messageChannels.length === 0) { + return; + } + + const currentSelectionStillExists = activeTabId + ? messageChannels.some((channel) => channel.id === activeTabId) + : false; + + if (!currentSelectionStillExists) { + setSettingsAccountsSelectedMessageChannel(messageChannels[0]); + } + }, [messageChannels, activeTabId, setSettingsAccountsSelectedMessageChannel]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx index 6b2d49042b..9a0274c189 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx @@ -1,13 +1,10 @@ -import { type MessageChannel } from '@/accounts/types/MessageChannel'; import { type MessageFolder } from '@/accounts/types/MessageFolder'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { SettingsMessageFoldersEmptyStateCard } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersEmptyStateCard'; import { SettingsMessageFoldersSkeletonLoader } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersSkeletonLoader'; import { SettingsMessageFoldersTreeItem } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem'; import { computeFolderIdsForSyncToggle } from '@/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle'; import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree'; +import { useMyMessageFolders } from '@/settings/accounts/hooks/useMyMessageFolders'; import { useUpdateMessageFoldersSyncStatus } from '@/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus'; import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -75,19 +72,9 @@ export const SettingsAccountsMessageFoldersCard = () => { const { updateMessageFoldersSyncStatus } = useUpdateMessageFoldersSyncStatus(); - const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - depth: 1, - shouldOnlyLoadRelationIdentifiers: false, - }); - - const { record: messageChannel, loading } = useFindOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - objectRecordId: settingsAccountsSelectedMessageChannel?.id, - recordGqlFields, - }); - - const { messageFolders = [] } = messageChannel ?? {}; + const { messageFolders, loading } = useMyMessageFolders( + settingsAccountsSelectedMessageChannel?.id, + ); const filteredMessageFolders = useMemo(() => { return messageFolders.filter((folder) => diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts index 58af3f81be..59666cfef2 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts @@ -146,4 +146,25 @@ describe('computeMessageFolderTree', () => { expect(result[0].children[0].folder.name).toBe('Clients'); expect(result[0].children[1].folder.name).toBe('Projects'); }); + + it('should resolve parent-child when parentFolderId references parent id instead of externalId', () => { + const parent = createFolder( + '20202020-aaaa-bbbb-cccc-000000000001', + 'custom folder', + null, + 'Label_5900090362003645629', + ); + const child = createFolder( + '20202020-aaaa-bbbb-cccc-000000000002', + 'child folder', + '20202020-aaaa-bbbb-cccc-000000000001', + 'Label_7713410187110265162', + ); + const result = computeMessageFolderTree([parent, child]); + + expect(result).toHaveLength(1); + expect(result[0].folder.name).toBe('custom folder'); + expect(result[0].children).toHaveLength(1); + expect(result[0].children[0].folder.name).toBe('child folder'); + }); }); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts index 89e6cef65f..dcbf9275bf 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts @@ -16,9 +16,11 @@ export const computeFolderIdsForSyncToggle = ({ const collectChildren = (id: string): string[] => { const folder = folderById.get(id); - const children = folder?.externalId + const children = folder ? allFolders.filter( - (childFolder) => childFolder.parentFolderId === folder.externalId, + (childFolder) => + childFolder.parentFolderId === folder.externalId || + childFolder.parentFolderId === folder.id, ) : []; @@ -38,7 +40,9 @@ export const computeFolderIdsForSyncToggle = ({ break; } - const parent = folderByExternalId.get(current.parentFolderId); + const parent = + folderByExternalId.get(current.parentFolderId) ?? + folderById.get(current.parentFolderId); if (!parent) { break; @@ -63,7 +67,9 @@ export const computeFolderIdsForSyncToggle = ({ for (const parent of collectParents(folderId)) { const children = allFolders.filter( - (folder) => folder.parentFolderId === parent.externalId, + (folder) => + folder.parentFolderId === parent.externalId || + folder.parentFolderId === parent.id, ); const hasOtherSyncedChild = children.some( (child) => child.isSynced && !idsToUnsync.has(child.id), diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts index 9d4d291d29..46847673e2 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts @@ -11,17 +11,22 @@ export const computeMessageFolderTree = ( folders: MessageFolder[], ): MessageFolderTreeNode[] => { const folderByExternalIdMap = new Map(); + const folderByIdMap = new Map(); const childrenMap = new Map(); folders.forEach((folder) => { if (isDefined(folder.externalId)) { folderByExternalIdMap.set(folder.externalId, folder); } + folderByIdMap.set(folder.id, folder); }); folders.forEach((folder) => { if (isDefined(folder.parentFolderId)) { - const parent = folderByExternalIdMap.get(folder.parentFolderId); + const parent = + folderByExternalIdMap.get(folder.parentFolderId) ?? + folderByIdMap.get(folder.parentFolderId); + if (isDefined(parent)) { const siblings = childrenMap.get(parent.id) || []; siblings.push(folder); @@ -46,7 +51,10 @@ export const computeMessageFolderTree = ( const rootFolders = folders.filter((folder) => { if (!folder.parentFolderId) return true; - return !folderByExternalIdMap.has(folder.parentFolderId); + return ( + !folderByExternalIdMap.has(folder.parentFolderId) && + !folderByIdMap.has(folder.parentFolderId) + ); }); rootFolders.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/deleteConnectedAccount.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/deleteConnectedAccount.ts new file mode 100644 index 0000000000..5bd8e95e38 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/deleteConnectedAccount.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const DELETE_CONNECTED_ACCOUNT = gql` + mutation DeleteConnectedAccount($id: UUID!) { + deleteConnectedAccount(id: $id) { + id + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateCalendarChannel.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateCalendarChannel.ts new file mode 100644 index 0000000000..62d1512046 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateCalendarChannel.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_CALENDAR_CHANNEL = gql` + mutation UpdateCalendarChannel($input: UpdateCalendarChannelInput!) { + updateCalendarChannel(input: $input) { + id + visibility + isContactAutoCreationEnabled + contactAutoCreationPolicy + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageChannel.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageChannel.ts new file mode 100644 index 0000000000..ddba6f4baa --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageChannel.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_MESSAGE_CHANNEL = gql` + mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) { + updateMessageChannel(input: $input) { + id + visibility + contactAutoCreationPolicy + excludeNonProfessionalEmails + excludeGroupEmails + messageFolderImportPolicy + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolder.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolder.ts new file mode 100644 index 0000000000..7f7027cfcd --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolder.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_MESSAGE_FOLDER = gql` + mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) { + updateMessageFolder(input: $input) { + id + isSynced + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts new file mode 100644 index 0000000000..f1327626bf --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_MESSAGE_FOLDERS = gql` + mutation UpdateMessageFolders($input: UpdateMessageFoldersInput!) { + updateMessageFolders(input: $input) { + id + isSynced + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyCalendarChannels.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyCalendarChannels.ts new file mode 100644 index 0000000000..590b69cf07 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyCalendarChannels.ts @@ -0,0 +1,20 @@ +import { gql } from '@apollo/client'; + +export const GET_MY_CALENDAR_CHANNELS = gql` + query MyCalendarChannels($connectedAccountId: UUID) { + myCalendarChannels(connectedAccountId: $connectedAccountId) { + id + handle + visibility + syncStatus + syncStage + syncStageStartedAt + isContactAutoCreationEnabled + contactAutoCreationPolicy + isSyncEnabled + connectedAccountId + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts new file mode 100644 index 0000000000..d530376d6a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts @@ -0,0 +1,18 @@ +import { gql } from '@apollo/client'; + +export const GET_MY_CONNECTED_ACCOUNTS = gql` + query MyConnectedAccounts { + myConnectedAccounts { + id + handle + provider + authFailedAt + scopes + handleAliases + lastSignedInAt + userWorkspaceId + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageChannels.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageChannels.ts new file mode 100644 index 0000000000..0bfeedaa2d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageChannels.ts @@ -0,0 +1,24 @@ +import { gql } from '@apollo/client'; + +export const GET_MY_MESSAGE_CHANNELS = gql` + query MyMessageChannels($connectedAccountId: UUID) { + myMessageChannels(connectedAccountId: $connectedAccountId) { + id + handle + visibility + type + isContactAutoCreationEnabled + contactAutoCreationPolicy + messageFolderImportPolicy + excludeNonProfessionalEmails + excludeGroupEmails + isSyncEnabled + syncStatus + syncStage + syncStageStartedAt + connectedAccountId + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts new file mode 100644 index 0000000000..029d0c1f2d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts @@ -0,0 +1,17 @@ +import { gql } from '@apollo/client'; + +export const GET_MY_MESSAGE_FOLDERS = gql` + query MyMessageFolders($messageChannelId: UUID) { + myMessageFolders(messageChannelId: $messageChannelId) { + id + name + isSynced + isSentFolder + parentFolderId + externalId + messageChannelId + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useMyCalendarChannels.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyCalendarChannels.ts new file mode 100644 index 0000000000..52608fd56c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyCalendarChannels.ts @@ -0,0 +1,111 @@ +import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { + type CalendarChannel, + CalendarChannelSyncStage, +} from '@/accounts/types/CalendarChannel'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { GET_MY_CALENDAR_CHANNELS } from '@/settings/accounts/graphql/queries/getMyCalendarChannels'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useApolloClient, useQuery } from '@apollo/client/react'; +import { useMemo } from 'react'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; + +type MetadataCalendarChannel = { + id: string; + handle: string; + visibility: string; + syncStatus: string; + syncStage: string; + syncStageStartedAt: string | null; + isContactAutoCreationEnabled: boolean; + contactAutoCreationPolicy: string; + isSyncEnabled: boolean; + connectedAccountId: string; + createdAt: string; + updatedAt: string; +}; + +export const useMyCalendarChannels = () => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + + const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); + const apolloClient = useApolloClient(); + + const { records: workspaceAccounts } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + filter: { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, + }, + skip: isMigrated, + }); + + const { records: workspaceChannels, loading: workspaceLoading } = + useFindManyRecords< + CalendarChannel & { connectedAccount: ConnectedAccount } + >({ + objectNameSingular: CoreObjectNameSingular.CalendarChannel, + filter: { + connectedAccountId: { + in: workspaceAccounts.map((account) => account.id), + }, + syncStage: { + neq: CalendarChannelSyncStage.PENDING_CONFIGURATION, + }, + }, + skip: isMigrated || !workspaceAccounts.length, + }); + + const { data: metadataData, loading: metadataLoading } = useQuery<{ + myCalendarChannels: MetadataCalendarChannel[]; + }>(GET_MY_CALENDAR_CHANNELS, { + client: apolloClient, + skip: !isMigrated, + }); + + const channels = useMemo(() => { + if (!isMigrated) { + return workspaceChannels; + } + + if (!metadataData?.myCalendarChannels) { + return []; + } + + return metadataData.myCalendarChannels + .filter( + (channel: MetadataCalendarChannel) => + channel.syncStage !== 'PENDING_CONFIGURATION', + ) + .map( + (channel: MetadataCalendarChannel) => + ({ + id: channel.id, + handle: channel.handle, + visibility: channel.visibility, + isContactAutoCreationEnabled: channel.isContactAutoCreationEnabled, + contactAutoCreationPolicy: channel.contactAutoCreationPolicy, + isSyncEnabled: channel.isSyncEnabled, + syncStatus: channel.syncStatus, + syncStage: channel.syncStage, + syncCursor: '', + syncStageStartedAt: channel.syncStageStartedAt + ? new Date(channel.syncStageStartedAt) + : null, + throttleFailureCount: 0, + connectedAccountId: channel.connectedAccountId, + __typename: 'CalendarChannel', + }) as CalendarChannel, + ); + }, [isMigrated, workspaceChannels, metadataData]); + + return { + channels, + loading: isMigrated ? metadataLoading : workspaceLoading, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useMyConnectedAccounts.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyConnectedAccounts.ts new file mode 100644 index 0000000000..481379b51e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyConnectedAccounts.ts @@ -0,0 +1,116 @@ +import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; +import { GET_MY_CONNECTED_ACCOUNTS } from '@/settings/accounts/graphql/queries/getMyConnectedAccounts'; +import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels'; +import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useApolloClient, useQuery } from '@apollo/client/react'; +import { useMemo } from 'react'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; + +type MetadataConnectedAccount = { + id: string; + handle: string; + provider: string; + authFailedAt: string | null; + scopes: string[] | null; + handleAliases: string[] | null; + lastSignedInAt: string | null; + userWorkspaceId: string; + createdAt: string; + updatedAt: string; +}; + +export const useMyConnectedAccounts = () => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + + const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); + + const apolloClient = useApolloClient(); + + const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + depth: 1, + shouldOnlyLoadRelationIdentifiers: false, + }); + + const { records: workspaceAccounts, loading: workspaceLoading } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + filter: { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, + }, + recordGqlFields, + skip: isMigrated, + }); + + const { data: metadataData, loading: metadataLoading } = useQuery<{ + myConnectedAccounts: MetadataConnectedAccount[]; + }>(GET_MY_CONNECTED_ACCOUNTS, { + client: apolloClient, + skip: !isMigrated, + }); + + const { channels: messageChannels, loading: messageChannelsLoading } = + useMyMessageChannels(); + const { channels: calendarChannels, loading: calendarChannelsLoading } = + useMyCalendarChannels(); + + const accounts = useMemo(() => { + if (!isMigrated) { + return workspaceAccounts; + } + + if (!metadataData?.myConnectedAccounts) { + return []; + } + + return metadataData.myConnectedAccounts.map( + (account: MetadataConnectedAccount) => + ({ + id: account.id, + handle: account.handle, + provider: account.provider, + accessToken: '', + refreshToken: '', + accountOwnerId: account.userWorkspaceId, + lastSyncHistoryId: '', + authFailedAt: account.authFailedAt + ? new Date(account.authFailedAt) + : null, + messageChannels: messageChannels.filter( + (channel) => + (channel as unknown as { connectedAccountId: string }) + .connectedAccountId === account.id, + ), + calendarChannels: calendarChannels.filter( + (channel) => + (channel as unknown as { connectedAccountId: string }) + .connectedAccountId === account.id, + ), + scopes: account.scopes, + __typename: 'ConnectedAccount', + }) as ConnectedAccount, + ); + }, [ + isMigrated, + workspaceAccounts, + metadataData, + messageChannels, + calendarChannels, + ]); + + return { + accounts, + loading: isMigrated + ? metadataLoading || messageChannelsLoading || calendarChannelsLoading + : workspaceLoading, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useMyMessageChannels.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyMessageChannels.ts new file mode 100644 index 0000000000..164955165f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyMessageChannels.ts @@ -0,0 +1,127 @@ +import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { + type MessageChannel, + MessageChannelSyncStage, +} from '@/accounts/types/MessageChannel'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; +import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useApolloClient, useQuery } from '@apollo/client/react'; +import { useMemo } from 'react'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; + +type MetadataMessageChannel = { + id: string; + handle: string; + visibility: string; + type: string; + isContactAutoCreationEnabled: boolean; + contactAutoCreationPolicy: string; + messageFolderImportPolicy: string; + excludeNonProfessionalEmails: boolean; + excludeGroupEmails: boolean; + isSyncEnabled: boolean; + syncStatus: string; + syncStage: string; + syncStageStartedAt: string | null; + connectedAccountId: string; + createdAt: string; + updatedAt: string; +}; + +export const useMyMessageChannels = () => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + + const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); + const apolloClient = useApolloClient(); + + const { records: workspaceAccounts } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + filter: { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, + }, + skip: isMigrated, + }); + + const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + depth: 1, + }); + + const { records: workspaceChannels, loading: workspaceLoading } = + useFindManyRecords( + { + objectNameSingular: CoreObjectNameSingular.MessageChannel, + filter: { + connectedAccountId: { + in: workspaceAccounts.map((account) => account.id), + }, + isSyncEnabled: { eq: true }, + syncStage: { + neq: MessageChannelSyncStage.PENDING_CONFIGURATION, + }, + }, + recordGqlFields, + skip: isMigrated || !workspaceAccounts.length, + }, + ); + + const { data: metadataData, loading: metadataLoading } = useQuery<{ + myMessageChannels: MetadataMessageChannel[]; + }>(GET_MY_MESSAGE_CHANNELS, { + client: apolloClient, + skip: !isMigrated, + }); + + const channels = useMemo(() => { + if (!isMigrated) { + return workspaceChannels; + } + + if (!metadataData?.myMessageChannels) { + return []; + } + + return metadataData.myMessageChannels + .filter( + (channel: MetadataMessageChannel) => + channel.isSyncEnabled && + channel.syncStage !== 'PENDING_CONFIGURATION', + ) + .map( + (channel: MetadataMessageChannel) => + ({ + id: channel.id, + handle: channel.handle, + visibility: channel.visibility, + contactAutoCreationPolicy: channel.contactAutoCreationPolicy, + excludeNonProfessionalEmails: channel.excludeNonProfessionalEmails, + excludeGroupEmails: channel.excludeGroupEmails, + isSyncEnabled: channel.isSyncEnabled, + messageFolders: [], + messageFolderImportPolicy: channel.messageFolderImportPolicy, + syncStatus: channel.syncStatus, + syncStage: channel.syncStage, + syncCursor: '', + syncStageStartedAt: channel.syncStageStartedAt + ? new Date(channel.syncStageStartedAt) + : null, + throttleFailureCount: 0, + connectedAccountId: channel.connectedAccountId, + __typename: 'MessageChannel', + }) as MessageChannel, + ); + }, [isMigrated, workspaceChannels, metadataData]); + + return { + channels, + loading: isMigrated ? metadataLoading : workspaceLoading, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useMyMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyMessageFolders.ts new file mode 100644 index 0000000000..120ba42815 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useMyMessageFolders.ts @@ -0,0 +1,80 @@ +import { type MessageChannel } from '@/accounts/types/MessageChannel'; +import { type MessageFolder } from '@/accounts/types/MessageFolder'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; +import { GET_MY_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/queries/getMyMessageFolders'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useApolloClient, useQuery } from '@apollo/client/react'; +import { useMemo } from 'react'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; + +type MetadataMessageFolder = { + id: string; + name: string | null; + isSynced: boolean; + isSentFolder: boolean; + parentFolderId: string | null; + externalId: string | null; + messageChannelId: string; + createdAt: string; + updatedAt: string; +}; + +export const useMyMessageFolders = (messageChannelId?: string) => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + + const apolloClient = useApolloClient(); + + const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + depth: 1, + shouldOnlyLoadRelationIdentifiers: false, + }); + + const { record: messageChannel, loading: workspaceLoading } = + useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + objectRecordId: messageChannelId, + recordGqlFields, + skip: isMigrated || !messageChannelId, + }); + + const { data: metadataData, loading: metadataLoading } = useQuery<{ + myMessageFolders: MetadataMessageFolder[]; + }>(GET_MY_MESSAGE_FOLDERS, { + client: apolloClient, + variables: messageChannelId ? { messageChannelId } : undefined, + skip: !isMigrated, + }); + + const messageFolders = useMemo(() => { + if (!isMigrated) { + return messageChannel?.messageFolders ?? []; + } + + if (!metadataData?.myMessageFolders) { + return []; + } + + return metadataData.myMessageFolders.map( + (folder: MetadataMessageFolder) => ({ + id: folder.id, + name: folder.name ?? '', + syncCursor: '', + isSynced: folder.isSynced, + isSentFolder: folder.isSentFolder, + parentFolderId: folder.parentFolderId, + messageChannelId: folder.messageChannelId, + externalId: folder.externalId, + __typename: 'MessageFolder' as const, + }), + ); + }, [isMigrated, messageChannel, metadataData]); + + return { + messageFolders, + loading: isMigrated ? metadataLoading : workspaceLoading, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts index db3fa017d7..bf831651c0 100644 --- a/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts @@ -1,7 +1,10 @@ import { useCallback } from 'react'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords'; +import { UPDATE_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/mutations/updateMessageFolders'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useApolloClient } from '@apollo/client/react'; +import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types'; type UpdateMessageFoldersSyncStatusArgs = { messageFolderIds: string[]; @@ -9,6 +12,12 @@ type UpdateMessageFoldersSyncStatusArgs = { }; export const useUpdateMessageFoldersSyncStatus = () => { + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + + const apolloClient = useApolloClient(); + const { updateManyRecords } = useUpdateManyRecords({ objectNameSingular: CoreObjectNameSingular.MessageFolder, recordGqlFields: { @@ -22,12 +31,37 @@ export const useUpdateMessageFoldersSyncStatus = () => { messageFolderIds, isSynced, }: UpdateMessageFoldersSyncStatusArgs) => { + if (isMigrated) { + if (messageFolderIds.length === 0) { + return; + } + + await apolloClient.mutate({ + mutation: UPDATE_MESSAGE_FOLDERS, + variables: { + input: { + ids: messageFolderIds, + update: { isSynced }, + }, + }, + optimisticResponse: { + updateMessageFolders: messageFolderIds.map((id) => ({ + __typename: 'MessageFolder', + id, + isSynced, + })), + }, + }); + + return; + } + await updateManyRecords({ recordIdsToUpdate: messageFolderIds, updateOneRecordInput: { isSynced }, }); }, - [updateManyRecords], + [isMigrated, apolloClient, updateManyRecords], ); return { updateMessageFoldersSyncStatus }; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx index 4692b2fa7d..539f270b7b 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx @@ -1,40 +1,20 @@ -import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CoreObjectNameSingular, SettingsPath } from 'twenty-shared/types'; -import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { SettingsPath } from 'twenty-shared/types'; import { SettingsAccountLoader } from '@/settings/accounts/components/SettingsAccountLoader'; import { SettingsAccountsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsBlocklistSection'; import { SettingsAccountsConnectedAccountsListCard } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsListCard'; import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/SettingsAccountsSettingsSection'; +import { useMyConnectedAccounts } from '@/settings/accounts/hooks/useMyConnectedAccounts'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { useLingui } from '@lingui/react/macro'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { getSettingsPath } from 'twenty-shared/utils'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; export const SettingsAccounts = () => { const { t } = useLingui(); - const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); - const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ - objectNameSingular: CoreObjectNameSingular.ConnectedAccount, - depth: 1, - shouldOnlyLoadRelationIdentifiers: false, - }); - - const { records: allAccounts, loading } = - useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ConnectedAccount, - filter: { - accountOwnerId: { - eq: currentWorkspaceMember?.id, - }, - }, - recordGqlFields, - }); + const { accounts: allAccounts, loading } = useMyConnectedAccounts(); return ( { SettingsAccountsConfigurationStep.Email, ); + const featureFlagsMap = useFeatureFlagsMap(); + const isMigrated = + featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false; + const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({ objectNameSingular: CoreObjectNameSingular.MessageChannel, depth: 1, shouldOnlyLoadRelationIdentifiers: false, }); - const { records: messageChannels } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - filter: { - connectedAccountId: { - eq: connectedAccountId, + const { records: workspaceMessageChannels } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + filter: { + connectedAccountId: { + eq: connectedAccountId, + }, }, - }, - recordGqlFields, - onCompleted: (data) => { - if (isDefined(data[0])) { - setSettingsAccountsSelectedMessageChannel(data[0]); - } - }, - skip: !connectedAccountId, + recordGqlFields, + onCompleted: (data) => { + if (isDefined(data[0])) { + setSettingsAccountsSelectedMessageChannel(data[0]); + } + }, + skip: !connectedAccountId || isMigrated, + }); + + const { records: workspaceCalendarChannels } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.CalendarChannel, + filter: { + connectedAccountId: { + eq: connectedAccountId, + }, + }, + skip: !connectedAccountId || isMigrated, + }); + + const { data: metadataMessageChannelData } = useQuery<{ + myMessageChannels: MessageChannel[]; + }>(GET_MY_MESSAGE_CHANNELS, { + variables: { connectedAccountId }, + skip: !isMigrated || !connectedAccountId, }); - const { records: calendarChannels } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.CalendarChannel, - filter: { - connectedAccountId: { - eq: connectedAccountId, - }, - }, - skip: !connectedAccountId, + const { data: metadataCalendarChannelData } = useQuery<{ + myCalendarChannels: CalendarChannel[]; + }>(GET_MY_CALENDAR_CHANNELS, { + variables: { connectedAccountId }, + skip: !isMigrated || !connectedAccountId, }); + const messageChannels = isMigrated + ? (metadataMessageChannelData?.myMessageChannels ?? []) + : workspaceMessageChannels; + + const calendarChannels = isMigrated + ? (metadataCalendarChannelData?.myCalendarChannels ?? []) + : workspaceCalendarChannels; + const messageChannel = messageChannels[0]; const calendarChannel = calendarChannels[0]; @@ -106,13 +142,20 @@ export const SettingsAccountsConfiguration = () => { if (showEmailStep) { return ( - + <> + {isMigrated && ( + + )} + + ); } diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfigurationSelectedMessageChannelEffect.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfigurationSelectedMessageChannelEffect.tsx new file mode 100644 index 0000000000..5d2e014d7e --- /dev/null +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfigurationSelectedMessageChannelEffect.tsx @@ -0,0 +1,25 @@ +import { type MessageChannel } from '@/accounts/types/MessageChannel'; +import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { useEffect } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type SettingsAccountsConfigurationSelectedMessageChannelEffectProps = { + messageChannel: MessageChannel | undefined; +}; + +export const SettingsAccountsConfigurationSelectedMessageChannelEffect = ({ + messageChannel, +}: SettingsAccountsConfigurationSelectedMessageChannelEffectProps) => { + const setSettingsAccountsSelectedMessageChannel = useSetAtomState( + settingsAccountsSelectedMessageChannelState, + ); + + useEffect(() => { + if (isDefined(messageChannel)) { + setSettingsAccountsSelectedMessageChannel(messageChannel); + } + }, [messageChannel, setSettingsAccountsSelectedMessageChannel]); + + return null; +}; diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql index ef35ea7186..7641bb2cc3 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql @@ -2215,7 +2215,6 @@ type CalendarChannel { isContactAutoCreationEnabled: Boolean! contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy! isSyncEnabled: Boolean! - syncCursor: String syncedAt: DateTime syncStageStartedAt: DateTime throttleFailureCount: Float! @@ -2259,15 +2258,11 @@ 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! @@ -2285,7 +2280,6 @@ type MessageChannel { excludeGroupEmails: Boolean! pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction! isSyncEnabled: Boolean! - syncCursor: String syncedAt: DateTime syncStatus: MessageChannelSyncStatus! syncStage: MessageChannelSyncStage! @@ -2347,7 +2341,6 @@ enum MessageChannelSyncStage { type MessageFolder { id: UUID! name: String - syncCursor: String isSentFolder: Boolean! isSynced: Boolean! parentFolderId: UUID @@ -3169,14 +3162,11 @@ 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 + myMessageFolders(messageChannelId: UUID): [MessageFolder!]! + myMessageChannels(connectedAccountId: UUID): [MessageChannel!]! + myConnectedAccounts: [ConnectedAccountDTO!]! connectedAccounts: [ConnectedAccountDTO!]! - connectedAccount(id: UUID!): ConnectedAccountDTO + myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]! webhooks: [Webhook!]! webhook(id: UUID!): Webhook minimalMetadata: MinimalMetadata! @@ -3474,18 +3464,11 @@ 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! + updateMessageFolders(input: UpdateMessageFoldersInput!): [MessageFolder!]! updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel! - deleteMessageChannel(id: UUID!): MessageChannel! - createConnectedAccount(input: CreateConnectedAccountInput!): ConnectedAccountDTO! - updateConnectedAccount(input: UpdateConnectedAccountInput!): ConnectedAccountDTO! deleteConnectedAccount(id: UUID!): ConnectedAccountDTO! + updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel! createWebhook(input: CreateWebhookInput!): Webhook! updateWebhook(input: UpdateWebhookInput!): Webhook! deleteWebhook(id: UUID!): Webhook! @@ -4382,66 +4365,18 @@ 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 UpdateMessageFoldersInput { + ids: [UUID!]! + update: UpdateMessageFolderInputUpdates! } input UpdateMessageChannelInput { @@ -4459,26 +4394,16 @@ input UpdateMessageChannelInputUpdates { excludeGroupEmails: Boolean } -input CreateConnectedAccountInput { - id: UUID - handle: String! - provider: String! - accessToken: String - refreshToken: String - scopes: [String!] - userWorkspaceId: UUID! -} - -input UpdateConnectedAccountInput { +input UpdateCalendarChannelInput { id: UUID! - update: UpdateConnectedAccountInputUpdates! + update: UpdateCalendarChannelInputUpdates! } -input UpdateConnectedAccountInputUpdates { - accessToken: String - refreshToken: String - handleAliases: [String!] - scopes: [String!] +input UpdateCalendarChannelInputUpdates { + visibility: CalendarChannelVisibility + isContactAutoCreationEnabled: Boolean + contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy + isSyncEnabled: Boolean } input CreateWebhookInput { diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts index 874bd5f296..17d0b9db2f 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts @@ -1886,7 +1886,6 @@ export interface CalendarChannel { isContactAutoCreationEnabled: Scalars['Boolean'] contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy isSyncEnabled: Scalars['Boolean'] - syncCursor?: Scalars['String'] syncedAt?: Scalars['DateTime'] syncStageStartedAt?: Scalars['DateTime'] throttleFailureCount: Scalars['Float'] @@ -1908,15 +1907,11 @@ 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'] @@ -1935,7 +1930,6 @@ export interface MessageChannel { excludeGroupEmails: Scalars['Boolean'] pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction isSyncEnabled: Scalars['Boolean'] - syncCursor?: Scalars['String'] syncedAt?: Scalars['DateTime'] syncStatus: MessageChannelSyncStatus syncStage: MessageChannelSyncStage @@ -1965,7 +1959,6 @@ export type MessageChannelSyncStage = 'PENDING_CONFIGURATION' | 'MESSAGE_LIST_FE export interface MessageFolder { id: Scalars['UUID'] name?: Scalars['String'] - syncCursor?: Scalars['String'] isSentFolder: Scalars['Boolean'] isSynced: Scalars['Boolean'] parentFolderId?: Scalars['UUID'] @@ -2724,14 +2717,11 @@ export interface Query { findWorkspaceFromInviteHash: Workspace validatePasswordResetToken: ValidatePasswordResetToken getSSOIdentityProviders: FindAvailableSSOIDP[] - messageFolders: MessageFolder[] - messageFolder?: MessageFolder - calendarChannels: CalendarChannel[] - calendarChannel?: CalendarChannel - messageChannels: MessageChannel[] - messageChannel?: MessageChannel + myMessageFolders: MessageFolder[] + myMessageChannels: MessageChannel[] + myConnectedAccounts: ConnectedAccountDTO[] connectedAccounts: ConnectedAccountDTO[] - connectedAccount?: ConnectedAccountDTO + myCalendarChannels: CalendarChannel[] webhooks: Webhook[] webhook?: Webhook minimalMetadata: MinimalMetadata @@ -2928,18 +2918,11 @@ export interface Mutation { createSAMLIdentityProvider: SetupSso deleteSSOIdentityProvider: DeleteSso editSSOIdentityProvider: EditSso - createMessageFolder: MessageFolder updateMessageFolder: MessageFolder - deleteMessageFolder: MessageFolder - createCalendarChannel: CalendarChannel - updateCalendarChannel: CalendarChannel - deleteCalendarChannel: CalendarChannel - createMessageChannel: MessageChannel + updateMessageFolders: MessageFolder[] updateMessageChannel: MessageChannel - deleteMessageChannel: MessageChannel - createConnectedAccount: ConnectedAccountDTO - updateConnectedAccount: ConnectedAccountDTO deleteConnectedAccount: ConnectedAccountDTO + updateCalendarChannel: CalendarChannel createWebhook: Webhook updateWebhook: Webhook deleteWebhook: Webhook @@ -4994,7 +4977,6 @@ export interface CalendarChannelGenqlSelection{ isContactAutoCreationEnabled?: boolean | number contactAutoCreationPolicy?: boolean | number isSyncEnabled?: boolean | number - syncCursor?: boolean | number syncedAt?: boolean | number syncStageStartedAt?: boolean | number throttleFailureCount?: boolean | number @@ -5009,15 +4991,11 @@ 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 @@ -5037,7 +5015,6 @@ export interface MessageChannelGenqlSelection{ excludeGroupEmails?: boolean | number pendingGroupEmailsAction?: boolean | number isSyncEnabled?: boolean | number - syncCursor?: boolean | number syncedAt?: boolean | number syncStatus?: boolean | number syncStage?: boolean | number @@ -5054,7 +5031,6 @@ export interface MessageChannelGenqlSelection{ export interface MessageFolderGenqlSelection{ id?: boolean | number name?: boolean | number - syncCursor?: boolean | number isSentFolder?: boolean | number isSynced?: boolean | number parentFolderId?: boolean | number @@ -5874,14 +5850,11 @@ 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']} }) + myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} }) + myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} }) + myConnectedAccounts?: ConnectedAccountDTOGenqlSelection connectedAccounts?: ConnectedAccountDTOGenqlSelection - connectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} }) + myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} }) webhooks?: WebhookGenqlSelection webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) minimalMetadata?: MinimalMetadataGenqlSelection @@ -6103,18 +6076,11 @@ 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} }) + updateMessageFolders?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFoldersInput} }) 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']} }) + updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} }) createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} }) updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} }) deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) @@ -6439,29 +6405,19 @@ 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 UpdateMessageFolderInputUpdates {isSynced?: (Scalars['Boolean'] | 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 UpdateMessageFoldersInput {ids: Scalars['UUID'][],update: UpdateMessageFolderInputUpdates} 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 UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateCalendarChannelInputUpdates} -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 UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)} export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} diff --git a/packages/twenty-sdk/src/clients/generated/metadata/types.ts b/packages/twenty-sdk/src/clients/generated/metadata/types.ts index 1ff22db52a..dca55a2ea8 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/types.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/types.ts @@ -87,8 +87,8 @@ export default { 352, 364, 365, - 483, - 484 + 478, + 479 ], "types": { "BillingProductDTO": { @@ -4346,9 +4346,6 @@ export default { "isSyncEnabled": [ 6 ], - "syncCursor": [ - 1 - ], "syncedAt": [ 4 ], @@ -4385,12 +4382,6 @@ export default { "provider": [ 1 ], - "accessToken": [ - 1 - ], - "refreshToken": [ - 1 - ], "lastCredentialsRefreshedAt": [ 4 ], @@ -4403,15 +4394,9 @@ export default { "scopes": [ 1 ], - "connectionParameters": [ - 15 - ], "lastSignedInAt": [ 4 ], - "oidcTokenClaims": [ - 15 - ], "userWorkspaceId": [ 3 ], @@ -4459,9 +4444,6 @@ export default { "isSyncEnabled": [ 6 ], - "syncCursor": [ - 1 - ], "syncedAt": [ 4 ], @@ -4507,9 +4489,6 @@ export default { "name": [ 1 ], - "syncCursor": [ - 1 - ], "isSentFolder": [ 6 ], @@ -6484,7 +6463,7 @@ export default { "getSSOIdentityProviders": [ 184 ], - "messageFolders": [ + "myMessageFolders": [ 249, { "messageChannelId": [ @@ -6492,33 +6471,7 @@ export default { ] } ], - "messageFolder": [ - 249, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "calendarChannels": [ - 235, - { - "connectedAccountId": [ - 3 - ] - } - ], - "calendarChannel": [ - 235, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "messageChannels": [ + "myMessageChannels": [ 241, { "connectedAccountId": [ @@ -6526,24 +6479,17 @@ export default { ] } ], - "messageChannel": [ - 241, - { - "id": [ - 3, - "UUID!" - ] - } + "myConnectedAccounts": [ + 240 ], "connectedAccounts": [ 240 ], - "connectedAccount": [ - 240, + "myCalendarChannels": [ + 235, { - "id": [ - 3, - "UUID!" + "connectedAccountId": [ + 3 ] } ], @@ -8446,66 +8392,21 @@ export default { ] } ], - "createMessageFolder": [ - 249, - { - "input": [ - 459, - "CreateMessageFolderInput!" - ] - } - ], "updateMessageFolder": [ 249, { "input": [ - 460, + 459, "UpdateMessageFolderInput!" ] } ], - "deleteMessageFolder": [ + "updateMessageFolders": [ 249, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "createCalendarChannel": [ - 235, { "input": [ - 462, - "CreateCalendarChannelInput!" - ] - } - ], - "updateCalendarChannel": [ - 235, - { - "input": [ - 463, - "UpdateCalendarChannelInput!" - ] - } - ], - "deleteCalendarChannel": [ - 235, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "createMessageChannel": [ - 241, - { - "input": [ - 465, - "CreateMessageChannelInput!" + 461, + "UpdateMessageFoldersInput!" ] } ], @@ -8513,38 +8414,11 @@ export default { 241, { "input": [ - 466, + 462, "UpdateMessageChannelInput!" ] } ], - "deleteMessageChannel": [ - 241, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "createConnectedAccount": [ - 240, - { - "input": [ - 468, - "CreateConnectedAccountInput!" - ] - } - ], - "updateConnectedAccount": [ - 240, - { - "input": [ - 469, - "UpdateConnectedAccountInput!" - ] - } - ], "deleteConnectedAccount": [ 240, { @@ -8554,11 +8428,20 @@ export default { ] } ], + "updateCalendarChannel": [ + 235, + { + "input": [ + 464, + "UpdateCalendarChannelInput!" + ] + } + ], "createWebhook": [ 256, { "input": [ - 471, + 466, "CreateWebhookInput!" ] } @@ -8567,7 +8450,7 @@ export default { 256, { "input": [ - 472, + 467, "UpdateWebhookInput!" ] } @@ -8588,7 +8471,7 @@ export default { 221, { "input": [ - 474, + 469, "CreateSkillInput!" ] } @@ -8597,7 +8480,7 @@ export default { 221, { "input": [ - 475, + 470, "UpdateSkillInput!" ] } @@ -8694,7 +8577,7 @@ export default { "String!" ], "connectionParameters": [ - 476, + 471, "EmailAccountConnectionParameters!" ], "id": [ @@ -8706,7 +8589,7 @@ export default { 154, { "input": [ - 478, + 473, "UpdateLabPublicFeatureFlagInput!" ] } @@ -8879,7 +8762,7 @@ export default { 68, { "input": [ - 479, + 474, "CreateOneAppTokenInput!" ] } @@ -8912,7 +8795,7 @@ export default { 6, { "workspaceMigration": [ - 481, + 476, "WorkspaceMigrationInput!" ] } @@ -8986,7 +8869,7 @@ export default { "String!" ], "fileFolder": [ - 484, + 479, "FileFolder!" ], "filePath": [ @@ -10983,159 +10866,31 @@ 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 + 460 ], "__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": [ + "UpdateMessageFoldersInput": { + "ids": [ 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 + 460 ], "__typename": [ 1 @@ -11146,7 +10901,7 @@ export default { 3 ], "update": [ - 467 + 463 ], "__typename": [ 1 @@ -11178,55 +10933,29 @@ export default { 1 ] }, - "CreateConnectedAccountInput": { - "id": [ - 3 - ], - "handle": [ - 1 - ], - "provider": [ - 1 - ], - "accessToken": [ - 1 - ], - "refreshToken": [ - 1 - ], - "scopes": [ - 1 - ], - "userWorkspaceId": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "UpdateConnectedAccountInput": { + "UpdateCalendarChannelInput": { "id": [ 3 ], "update": [ - 470 + 465 ], "__typename": [ 1 ] }, - "UpdateConnectedAccountInputUpdates": { - "accessToken": [ - 1 + "UpdateCalendarChannelInputUpdates": { + "visibility": [ + 238 ], - "refreshToken": [ - 1 + "isContactAutoCreationEnabled": [ + 6 ], - "handleAliases": [ - 1 + "contactAutoCreationPolicy": [ + 239 ], - "scopes": [ - 1 + "isSyncEnabled": [ + 6 ], "__typename": [ 1 @@ -11257,7 +10986,7 @@ export default { 3 ], "update": [ - 473 + 468 ], "__typename": [ 1 @@ -11331,13 +11060,13 @@ export default { }, "EmailAccountConnectionParameters": { "IMAP": [ - 477 + 472 ], "SMTP": [ - 477 + 472 ], "CALDAV": [ - 477 + 472 ], "__typename": [ 1 @@ -11376,7 +11105,7 @@ export default { }, "CreateOneAppTokenInput": { "appToken": [ - 480 + 475 ], "__typename": [ 1 @@ -11392,7 +11121,7 @@ export default { }, "WorkspaceMigrationInput": { "actions": [ - 482 + 477 ], "__typename": [ 1 @@ -11400,7 +11129,7 @@ export default { }, "WorkspaceMigrationDeleteActionInput": { "type": [ - 483 + 478 ], "metadataName": [ 252 @@ -11428,7 +11157,7 @@ export default { 218, { "input": [ - 486, + 481, "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 index 5f1f3082c2..0fcba085e8 100644 --- 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 @@ -309,26 +309,44 @@ export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSus } if (messageFolders.length > 0) { + const externalIdToFolderIdMap = new Map( + messageFolders.map((folder) => [folder.externalId, folder.id]), + ); + 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, - })); + .map((workspaceEntity) => { + let resolvedParentFolderId: string | null = null; + + if (isNonEmptyString(workspaceEntity.parentFolderId)) { + resolvedParentFolderId = + externalIdToFolderIdMap.get(workspaceEntity.parentFolderId) ?? + null; + + if (!resolvedParentFolderId) { + this.logger.warn( + `Message folder ${workspaceEntity.id}: could not resolve parentFolderId externalId "${workspaceEntity.parentFolderId}" to a UUID`, + ); + } + } + + return { + id: workspaceEntity.id, + name: workspaceEntity.name, + syncCursor: workspaceEntity.syncCursor, + isSentFolder: workspaceEntity.isSentFolder, + isSynced: workspaceEntity.isSynced, + parentFolderId: resolvedParentFolderId, + 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[], 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/1774005903909-add-messaging-infrastructure-metadata-entities.ts similarity index 79% rename from packages/twenty-server/src/database/typeorm/core/migrations/common/1773945207801-add-messaging-infrastructure-metadata-entities.ts rename to packages/twenty-server/src/database/typeorm/core/migrations/common/1774005903909-add-messaging-infrastructure-metadata-entities.ts index bbcd3e1eeb..3698e434f2 100644 --- 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/1774005903909-add-messaging-infrastructure-metadata-entities.ts @@ -1,9 +1,9 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddMessagingInfrastructureMetadataEntities1773945207801 +export class AddMessagingInfrastructureMetadataEntities1774005903909 implements MigrationInterface { - name = 'AddMessagingInfrastructureMetadataEntities1773945207801'; + name = 'AddMessagingInfrastructureMetadataEntities1774005903909'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -31,13 +31,13 @@ export class AddMessagingInfrastructureMetadataEntities1773945207801 `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"))`, + `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 DEFAULT true, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL DEFAULT 'SENT', "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL DEFAULT 'ALL_FOLDERS', "excludeNonProfessionalEmails" boolean NOT NULL DEFAULT true, "excludeGroupEmails" boolean NOT NULL DEFAULT true, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL DEFAULT true, "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"))`, + `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 DEFAULT 'NONE', "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')`, @@ -52,7 +52,7 @@ export class AddMessagingInfrastructureMetadataEntities1773945207801 `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"))`, + `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 DEFAULT true, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL DEFAULT 'AS_PARTICIPANT_AND_ORGANIZER', "isSyncEnabled" boolean NOT NULL DEFAULT true, "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`, diff --git a/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts b/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts index 5b1060dfc6..48602b3336 100644 --- a/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts +++ b/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts @@ -48,7 +48,8 @@ export class McpCoreController { @AuthWorkspace() workspace: WorkspaceEntity, @AuthApiKey() apiKey: ApiKeyEntity | undefined, @AuthUser({ allowUndefined: true }) user: UserEntity | undefined, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @Res({ passthrough: true }) res: Response, ) { const result = await this.mcpProtocolService.handleMCPCoreQuery(body, { diff --git a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts index 36871a5bc3..948a4fa66e 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts @@ -112,7 +112,7 @@ export class ApplicationDevelopmentResolver { @Args() { applicationId }: GenerateApplicationTokenInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, @AuthUser({ allowUndefined: true }) user?: { id: string }, - @AuthUserWorkspaceId() userWorkspaceId?: string, + @AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string, ): Promise { return this.applicationTokenService.generateApplicationTokenPair({ workspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts index c64780604e..72a544aa43 100644 --- a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts @@ -37,7 +37,8 @@ export class SearchResolver { @Query(() => SearchResultConnectionDTO) async search( @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, @Args() { diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index a4fde479ac..5e03e82a70 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -362,7 +362,8 @@ export class WorkspaceResolver { @ResolveField(() => [ViewDTO]) async views( @Parent() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, ): Promise { return this.viewService.findByWorkspaceId(workspace.id, userWorkspaceId); } diff --git a/packages/twenty-server/src/engine/decorators/auth/auth-user-workspace-id.decorator.ts b/packages/twenty-server/src/engine/decorators/auth/auth-user-workspace-id.decorator.ts index e0d625a2df..b0265c3e0c 100644 --- a/packages/twenty-server/src/engine/decorators/auth/auth-user-workspace-id.decorator.ts +++ b/packages/twenty-server/src/engine/decorators/auth/auth-user-workspace-id.decorator.ts @@ -1,11 +1,25 @@ -import { type ExecutionContext, createParamDecorator } from '@nestjs/common'; +import { + type ExecutionContext, + ForbiddenException, + createParamDecorator, +} from '@nestjs/common'; import { getRequest } from 'src/utils/extract-request'; +interface DecoratorOptions { + allowUndefined?: boolean; +} + export const AuthUserWorkspaceId = createParamDecorator( - (_data: unknown, ctx: ExecutionContext) => { + (options: DecoratorOptions | undefined, ctx: ExecutionContext) => { const request = getRequest(ctx); + if (!options?.allowUndefined && !request.userWorkspaceId) { + throw new ForbiddenException( + 'This endpoint requires a user context. API keys are not supported.', + ); + } + return request.userWorkspaceId; }, ); 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 index b1b68ec33f..47b3ac895f 100644 --- 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 @@ -7,6 +7,7 @@ import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/cale 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 { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; @Module({ @@ -15,6 +16,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi AuthModule, PermissionsModule, FeatureFlagModule, + ConnectedAccountMetadataModule, ], providers: [ CalendarChannelMetadataService, 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 index 61a2217c9d..2cb7d2136c 100644 --- 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 @@ -1,43 +1,144 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { CalendarChannelSyncStage, CalendarChannelVisibility, } from 'twenty-shared/types'; +import { + CalendarChannelException, + CalendarChannelExceptionCode, +} from 'src/engine/metadata-modules/calendar-channel/calendar-channel.exception'; 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'; +import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; @Injectable() export class CalendarChannelMetadataService { constructor( @InjectRepository(CalendarChannelEntity) private readonly repository: Repository, + private readonly connectedAccountMetadataService: ConnectedAccountMetadataService, ) {} async findAll(workspaceId: string): Promise { return this.repository.find({ where: { workspaceId } }); } - async findByConnectedAccountId( - connectedAccountId: string, - workspaceId: string, - ): Promise { + async findByUserWorkspaceId({ + userWorkspaceId, + workspaceId, + }: { + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const userAccountIds = + await this.connectedAccountMetadataService.getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }); + + return this.findByConnectedAccountIds({ + connectedAccountIds: userAccountIds, + workspaceId, + }); + } + + async findByConnectedAccountIdForUser({ + connectedAccountId, + userWorkspaceId, + workspaceId, + }: { + connectedAccountId: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + await this.connectedAccountMetadataService.verifyOwnership({ + id: connectedAccountId, + userWorkspaceId, + workspaceId, + }); + + return this.findByConnectedAccountId({ connectedAccountId, workspaceId }); + } + + async findByConnectedAccountId({ + connectedAccountId, + workspaceId, + }: { + connectedAccountId: string; + workspaceId: string; + }): Promise { return this.repository.find({ where: { connectedAccountId, workspaceId }, }); } - async findById( - id: string, - workspaceId: string, - ): Promise { + async findByConnectedAccountIds({ + connectedAccountIds, + workspaceId, + }: { + connectedAccountIds: string[]; + workspaceId: string; + }): Promise { + if (connectedAccountIds.length === 0) { + return []; + } + + return this.repository.find({ + where: { connectedAccountId: In(connectedAccountIds), workspaceId }, + }); + } + + async findById({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { return this.repository.findOne({ where: { id, workspaceId } }); } + async verifyOwnership({ + id, + userWorkspaceId, + workspaceId, + }: { + id: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const calendarChannel = await this.repository.findOne({ + where: { id, workspaceId }, + }); + + if (!calendarChannel) { + throw new CalendarChannelException( + `Calendar channel ${id} not found`, + CalendarChannelExceptionCode.CALENDAR_CHANNEL_NOT_FOUND, + ); + } + + const userAccountIds = + await this.connectedAccountMetadataService.getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }); + + if (!userAccountIds.includes(calendarChannel.connectedAccountId)) { + throw new CalendarChannelException( + `Calendar channel ${id} does not belong to user workspace ${userWorkspaceId}`, + CalendarChannelExceptionCode.CALENDAR_CHANNEL_OWNERSHIP_VIOLATION, + ); + } + + return calendarChannel; + } + async create( data: Partial & { workspaceId: string; @@ -52,11 +153,15 @@ export class CalendarChannelMetadataService { return this.repository.save(entity); } - async update( - id: string, - workspaceId: string, - data: Partial, - ): Promise { + async update({ + id, + workspaceId, + data, + }: { + id: string; + workspaceId: string; + data: Partial; + }): Promise { await this.repository.update( { id, workspaceId }, data as Record, @@ -65,13 +170,19 @@ export class CalendarChannelMetadataService { return this.repository.findOneOrFail({ where: { id, workspaceId } }); } - async delete(id: string, workspaceId: string): Promise { - const entity = await this.repository.findOneOrFail({ + async delete({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { + const calendarChannel = await this.repository.findOneOrFail({ where: { id, workspaceId }, }); await this.repository.delete({ id, workspaceId }); - return entity; + return calendarChannel; } } 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 index 004d1ff497..74516dae16 100644 --- 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 @@ -7,6 +7,7 @@ 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', + CALENDAR_CHANNEL_OWNERSHIP_VIOLATION = 'CALENDAR_CHANNEL_OWNERSHIP_VIOLATION', } const getCalendarChannelExceptionUserFriendlyMessage = ( @@ -17,6 +18,8 @@ const getCalendarChannelExceptionUserFriendlyMessage = ( return msg`Calendar channel not found.`; case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT: return msg`Invalid calendar channel input.`; + case CalendarChannelExceptionCode.CALENDAR_CHANNEL_OWNERSHIP_VIOLATION: + return msg`You do not have access to this calendar channel.`; default: assertUnreachable(code); } 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 index 0b15a0f654..502aa1b0be 100644 --- 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 @@ -96,10 +96,41 @@ export class CalendarChannelDataAccessService { ) : await this.toCoreWhere(workspaceId, where); - return this.coreRepository.findOne({ + const requestedRelations = + (options.relations as string[] | undefined)?.slice() ?? []; + + const needsConnectedAccount = + requestedRelations.includes('connectedAccount'); + + const coreRelations = requestedRelations.filter( + (r) => r !== 'connectedAccount', + ); + + const result = await this.coreRepository.findOne({ ...options, where: coreWhere, - } as FindOneOptions) as unknown as Promise; + relations: coreRelations, + } as FindOneOptions); + + if (!result) { + return null; + } + + const workspaceResult = + result as unknown as CalendarChannelWorkspaceEntity; + + if (needsConnectedAccount) { + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { id: result.connectedAccountId }, + }); + + if (connectedAccount) { + workspaceResult.connectedAccount = connectedAccount; + } + } + + return workspaceResult; } const workspaceRepository = await this.getWorkspaceRepository(workspaceId); 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 index 076db9816b..fee789078f 100644 --- 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 @@ -59,9 +59,7 @@ export class CalendarChannelDTO { @Field() isSyncEnabled: boolean; - @IsString() - @IsOptional() - @Field(() => String, { nullable: true }) + @HideField() syncCursor: string | null; @IsDateString() 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 index 8f4220dc33..8075d3c5a9 100644 --- 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 @@ -49,17 +49,19 @@ export class CalendarChannelEntity extends WorkspaceRelatedEntity { }) visibility: CalendarChannelVisibility; - @Column({ type: 'boolean', nullable: false }) + @Column({ type: 'boolean', nullable: false, default: true }) isContactAutoCreationEnabled: boolean; @Column({ type: 'enum', enum: CalendarChannelContactAutoCreationPolicy, nullable: false, + default: + CalendarChannelContactAutoCreationPolicy.AS_PARTICIPANT_AND_ORGANIZER, }) contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy; - @Column({ type: 'boolean', nullable: false }) + @Column({ type: 'boolean', nullable: false, default: true }) isSyncEnabled: boolean; @Column({ type: 'varchar', nullable: true }) 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 index 4f22f2cd18..5726c1e96e 100644 --- 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 @@ -1,22 +1,21 @@ 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 { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; 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 { NoPermissionGuard } from 'src/engine/guards/no-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'; @@ -29,10 +28,11 @@ export class CalendarChannelResolver { ) {} @Query(() => [CalendarChannelDTO]) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) - async calendarChannels( + async myCalendarChannels( @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, @Args('connectedAccountId', { type: () => UUIDScalarType, nullable: true, @@ -40,59 +40,39 @@ export class CalendarChannelResolver { connectedAccountId?: string, ): Promise { if (connectedAccountId) { - return this.calendarChannelMetadataService.findByConnectedAccountId( - connectedAccountId, - workspace.id, + return this.calendarChannelMetadataService.findByConnectedAccountIdForUser( + { + connectedAccountId, + userWorkspaceId, + workspaceId: 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, + return this.calendarChannelMetadataService.findByUserWorkspaceId({ + userWorkspaceId, workspaceId: workspace.id, }); } @Mutation(() => CalendarChannelDTO) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) async updateCalendarChannel( @Args('input') input: UpdateCalendarChannelInput, @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, ): Promise { - return this.calendarChannelMetadataService.update( - input.id, - workspace.id, - input.update, - ); - } + await this.calendarChannelMetadataService.verifyOwnership({ + id: input.id, + userWorkspaceId, + workspaceId: workspace.id, + }); - @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); + return this.calendarChannelMetadataService.update({ + id: input.id, + workspaceId: workspace.id, + data: input.update, + }); } } 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 index 7662580b6d..50e47d47a4 100644 --- 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 @@ -1,6 +1,7 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { + ForbiddenError, NotFoundError, UserInputError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; @@ -8,6 +9,10 @@ import { CalendarChannelException, CalendarChannelExceptionCode, } from 'src/engine/metadata-modules/calendar-channel/calendar-channel.exception'; +import { + ConnectedAccountException, + ConnectedAccountExceptionCode, +} from 'src/engine/metadata-modules/connected-account/connected-account.exception'; export const calendarChannelGraphqlApiExceptionHandler = (error: Error) => { if (error instanceof CalendarChannelException) { @@ -16,11 +21,23 @@ export const calendarChannelGraphqlApiExceptionHandler = (error: Error) => { throw new NotFoundError(error); case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT: throw new UserInputError(error); + case CalendarChannelExceptionCode.CALENDAR_CHANNEL_OWNERSHIP_VIOLATION: + throw new ForbiddenError(error); default: { return assertUnreachable(error.code); } } } + if (error instanceof ConnectedAccountException) { + switch (error.code) { + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION: + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND: + throw new ForbiddenError(error); + default: + break; + } + } + throw error; }; 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 index a6cf6e9b1c..e9f2e4845d 100644 --- 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 @@ -1,8 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; +import { + ConnectedAccountException, + ConnectedAccountExceptionCode, +} from 'src/engine/metadata-modules/connected-account/connected-account.exception'; 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'; @@ -17,13 +21,85 @@ export class ConnectedAccountMetadataService { return this.repository.find({ where: { workspaceId } }); } - async findById( - id: string, - workspaceId: string, - ): Promise { + async findByUserWorkspaceId({ + userWorkspaceId, + workspaceId, + }: { + userWorkspaceId: string; + workspaceId: string; + }): Promise { + return this.repository.find({ + where: { userWorkspaceId, workspaceId }, + }); + } + + async findById({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { return this.repository.findOne({ where: { id, workspaceId } }); } + async findByIds({ + ids, + workspaceId, + }: { + ids: string[]; + workspaceId: string; + }): Promise { + return this.repository.find({ + where: { id: In(ids), workspaceId }, + }); + } + + async verifyOwnership({ + id, + userWorkspaceId, + workspaceId, + }: { + id: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const connectedAccount = await this.repository.findOne({ + where: { id, workspaceId }, + }); + + if (!connectedAccount) { + throw new ConnectedAccountException( + `Connected account ${id} not found`, + ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + if (connectedAccount.userWorkspaceId !== userWorkspaceId) { + throw new ConnectedAccountException( + `Connected account ${id} does not belong to user workspace ${userWorkspaceId}`, + ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION, + ); + } + + return connectedAccount; + } + + async getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }: { + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const accounts = await this.repository.find({ + where: { userWorkspaceId, workspaceId }, + select: ['id'], + }); + + return accounts.map((account) => account.id); + } + async create( data: Partial & { workspaceId: string; @@ -37,11 +113,15 @@ export class ConnectedAccountMetadataService { return this.repository.save(entity); } - async update( - id: string, - workspaceId: string, - data: Partial, - ): Promise { + async update({ + id, + workspaceId, + data, + }: { + id: string; + workspaceId: string; + data: Partial; + }): Promise { await this.repository.update( { id, workspaceId }, data as Record, @@ -50,13 +130,19 @@ export class ConnectedAccountMetadataService { return this.repository.findOneOrFail({ where: { id, workspaceId } }); } - async delete(id: string, workspaceId: string): Promise { - const entity = await this.repository.findOneOrFail({ + async delete({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { + const connectedAccount = await this.repository.findOneOrFail({ where: { id, workspaceId }, }); await this.repository.delete({ id, workspaceId }); - return entity; + return connectedAccount; } } 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 index d026f6752f..912965c259 100644 --- 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 @@ -7,6 +7,7 @@ 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', + CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION = 'CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION', } const getConnectedAccountExceptionUserFriendlyMessage = ( @@ -17,6 +18,8 @@ const getConnectedAccountExceptionUserFriendlyMessage = ( return msg`Connected account not found.`; case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT: return msg`Invalid connected account input.`; + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION: + return msg`You do not have access to this connected account.`; default: assertUnreachable(code); } 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 index 336bec3014..c2764f6ff0 100644 --- 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 @@ -79,9 +79,15 @@ export class ConnectedAccountDataAccessService { const coreData: Record = { ...rest }; if (handleAliases !== undefined) { - coreData.handleAliases = isNonEmptyString(handleAliases) - ? handleAliases.split(',').map((alias: string) => alias.trim()) - : null; + if (Array.isArray(handleAliases)) { + coreData.handleAliases = handleAliases; + } else if (isNonEmptyString(handleAliases)) { + coreData.handleAliases = handleAliases + .split(',') + .map((alias: string) => alias.trim()); + } else { + coreData.handleAliases = null; + } } if (accountOwnerId !== undefined) { 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 index bf8a785ec0..1ea4fbbe3d 100644 --- 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 @@ -8,7 +8,6 @@ import { IsString, IsUUID, } from 'class-validator'; -import GraphQLJSON from 'graphql-type-json'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -29,14 +28,10 @@ export class ConnectedAccountDTO { @Field() provider: string; - @IsString() - @IsOptional() - @Field(() => String, { nullable: true }) + @HideField() accessToken: string | null; - @IsString() - @IsOptional() - @Field(() => String, { nullable: true }) + @HideField() refreshToken: string | null; @IsDateString() @@ -59,8 +54,7 @@ export class ConnectedAccountDTO { @Field(() => [String], { nullable: true }) scopes: string[] | null; - @IsOptional() - @Field(() => GraphQLJSON, { nullable: true }) + @HideField() connectionParameters: Record | null; @IsDateString() @@ -68,8 +62,7 @@ export class ConnectedAccountDTO { @Field(() => Date, { nullable: true }) lastSignedInAt: Date | null; - @IsOptional() - @Field(() => GraphQLJSON, { nullable: true }) + @HideField() oidcTokenClaims: Record | null; @IsUUID() 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 deleted file mode 100644 index 83c93e7045..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/update-connected-account.input.ts +++ /dev/null @@ -1,49 +0,0 @@ -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/resolvers/connected-account.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver.ts index 9a9637f7df..edb9c84f0c 100644 --- 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 @@ -7,17 +7,17 @@ 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 { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { FeatureFlagGuard, RequireFeatureFlag, } from 'src/engine/guards/feature-flag.guard'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.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) @@ -28,6 +28,19 @@ export class ConnectedAccountResolver { private readonly connectedAccountMetadataService: ConnectedAccountMetadataService, ) {} + @Query(() => [ConnectedAccountDTO]) + @UseGuards(NoPermissionGuard) + @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) + async myConnectedAccounts( + @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, + ): Promise { + return this.connectedAccountMetadataService.findByUserWorkspaceId({ + userWorkspaceId, + workspaceId: workspace.id, + }); + } + @Query(() => [ConnectedAccountDTO]) @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) @@ -37,50 +50,23 @@ export class ConnectedAccountResolver { 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)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) async deleteConnectedAccount( @Args('id', { type: () => UUIDScalarType }) id: string, @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, ): Promise { - return this.connectedAccountMetadataService.delete(id, workspace.id); + await this.connectedAccountMetadataService.verifyOwnership({ + id, + userWorkspaceId, + workspaceId: workspace.id, + }); + + return this.connectedAccountMetadataService.delete({ + id, + workspaceId: 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 index b181c92e3b..8aeb34eca6 100644 --- 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 @@ -1,6 +1,7 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { + ForbiddenError, NotFoundError, UserInputError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; @@ -16,6 +17,8 @@ export const connectedAccountGraphqlApiExceptionHandler = (error: Error) => { throw new NotFoundError(error); case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT: throw new UserInputError(error); + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION: + throw new ForbiddenError(error); default: { return assertUnreachable(error.code); } 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 index d3e1e82161..53481ac4f4 100644 --- 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 @@ -96,10 +96,55 @@ export class MessageChannelDataAccessService { ) : await this.toCoreWhere(workspaceId, where); - return this.coreRepository.findOne({ + const requestedRelations = + (options.relations as string[] | undefined)?.slice() ?? []; + + const needsConnectedAccount = + requestedRelations.includes('connectedAccount'); + + const needsMessageFolders = requestedRelations.includes('messageFolders'); + + const coreRelations = requestedRelations.filter( + (r) => r !== 'connectedAccount' && r !== 'messageFolders', + ); + + const result = await this.coreRepository.findOne({ ...options, where: coreWhere, - } as FindOneOptions) as unknown as Promise; + relations: coreRelations, + } as FindOneOptions); + + if (!result) { + return null; + } + + const workspaceResult = + result as unknown as MessageChannelWorkspaceEntity; + + if (needsConnectedAccount) { + const connectedAccount = + await this.connectedAccountDataAccessService.findOne(workspaceId, { + where: { id: result.connectedAccountId }, + }); + + if (connectedAccount) { + workspaceResult.connectedAccount = connectedAccount; + } + } + + if (needsMessageFolders) { + const workspaceRepository = + await this.getWorkspaceRepository(workspaceId); + + const workspaceChannel = await workspaceRepository.findOne({ + where: { id: result.id }, + relations: ['messageFolders'], + }); + + workspaceResult.messageFolders = workspaceChannel?.messageFolders ?? []; + } + + return workspaceResult; } const workspaceRepository = await this.getWorkspaceRepository(workspaceId); 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 index bfd5126391..b9aaf6ac3b 100644 --- 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 @@ -75,9 +75,7 @@ export class MessageChannelDTO { @Field() isSyncEnabled: boolean; - @IsString() - @IsOptional() - @Field(() => String, { nullable: true }) + @HideField() syncCursor: string | null; @IsDateString() 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 index 6782c57e25..50fb52e2ca 100644 --- 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 @@ -46,13 +46,14 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity { }) type: MessageChannelType; - @Column({ type: 'boolean', nullable: false }) + @Column({ type: 'boolean', nullable: false, default: true }) isContactAutoCreationEnabled: boolean; @Column({ type: 'enum', enum: MessageChannelContactAutoCreationPolicy, nullable: false, + default: MessageChannelContactAutoCreationPolicy.SENT, }) contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy; @@ -60,13 +61,14 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity { type: 'enum', enum: MessageFolderImportPolicy, nullable: false, + default: MessageFolderImportPolicy.ALL_FOLDERS, }) messageFolderImportPolicy: MessageFolderImportPolicy; - @Column({ type: 'boolean', nullable: false }) + @Column({ type: 'boolean', nullable: false, default: true }) excludeNonProfessionalEmails: boolean; - @Column({ type: 'boolean', nullable: false }) + @Column({ type: 'boolean', nullable: false, default: true }) excludeGroupEmails: boolean; @Column({ @@ -76,7 +78,7 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity { }) pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction; - @Column({ type: 'boolean', nullable: false }) + @Column({ type: 'boolean', nullable: false, default: true }) isSyncEnabled: boolean; @Column({ type: 'varchar', nullable: true }) 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 index 1defa96a44..2b58d14c8d 100644 --- 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 @@ -3,11 +3,14 @@ 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 { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.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 { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module'; @Module({ imports: [ @@ -15,6 +18,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi AuthModule, PermissionsModule, FeatureFlagModule, + ConnectedAccountMetadataModule, + MessageFolderDataAccessModule, + MessagingImportManagerModule, ], providers: [ MessageChannelMetadataService, 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 index da07a97392..f52d014db0 100644 --- 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 @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { MessageChannelSyncStage, @@ -9,36 +9,137 @@ import { MessageChannelVisibility, } from 'twenty-shared/types'; +import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; 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'; +import { + MessageChannelException, + MessageChannelExceptionCode, +} from 'src/engine/metadata-modules/message-channel/message-channel.exception'; @Injectable() export class MessageChannelMetadataService { constructor( @InjectRepository(MessageChannelEntity) private readonly repository: Repository, + private readonly connectedAccountMetadataService: ConnectedAccountMetadataService, ) {} async findAll(workspaceId: string): Promise { return this.repository.find({ where: { workspaceId } }); } - async findByConnectedAccountId( - connectedAccountId: string, - workspaceId: string, - ): Promise { + async findByUserWorkspaceId({ + userWorkspaceId, + workspaceId, + }: { + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const userAccountIds = + await this.connectedAccountMetadataService.getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }); + + return this.findByConnectedAccountIds({ + connectedAccountIds: userAccountIds, + workspaceId, + }); + } + + async findByConnectedAccountIdForUser({ + connectedAccountId, + userWorkspaceId, + workspaceId, + }: { + connectedAccountId: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + await this.connectedAccountMetadataService.verifyOwnership({ + id: connectedAccountId, + userWorkspaceId, + workspaceId, + }); + + return this.findByConnectedAccountId({ connectedAccountId, workspaceId }); + } + + async findByConnectedAccountId({ + connectedAccountId, + workspaceId, + }: { + connectedAccountId: string; + workspaceId: string; + }): Promise { return this.repository.find({ where: { connectedAccountId, workspaceId }, }); } - async findById( - id: string, - workspaceId: string, - ): Promise { + async findByConnectedAccountIds({ + connectedAccountIds, + workspaceId, + }: { + connectedAccountIds: string[]; + workspaceId: string; + }): Promise { + if (connectedAccountIds.length === 0) { + return []; + } + + return this.repository.find({ + where: { connectedAccountId: In(connectedAccountIds), workspaceId }, + }); + } + + async findById({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { return this.repository.findOne({ where: { id, workspaceId } }); } + async verifyOwnership({ + id, + userWorkspaceId, + workspaceId, + }: { + id: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const messageChannel = await this.repository.findOne({ + where: { id, workspaceId }, + }); + + if (!messageChannel) { + throw new MessageChannelException( + `Message channel ${id} not found`, + MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND, + ); + } + + const userAccountIds = + await this.connectedAccountMetadataService.getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }); + + if (!userAccountIds.includes(messageChannel.connectedAccountId)) { + throw new MessageChannelException( + `Message channel ${id} does not belong to user workspace ${userWorkspaceId}`, + MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION, + ); + } + + return messageChannel; + } + async create( data: Partial & { workspaceId: string; @@ -54,11 +155,15 @@ export class MessageChannelMetadataService { return this.repository.save(entity); } - async update( - id: string, - workspaceId: string, - data: Partial, - ): Promise { + async update({ + id, + workspaceId, + data, + }: { + id: string; + workspaceId: string; + data: Partial; + }): Promise { await this.repository.update( { id, workspaceId }, data as Record, @@ -67,13 +172,19 @@ export class MessageChannelMetadataService { return this.repository.findOneOrFail({ where: { id, workspaceId } }); } - async delete(id: string, workspaceId: string): Promise { - const entity = await this.repository.findOneOrFail({ + async delete({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { + const messageChannel = await this.repository.findOneOrFail({ where: { id, workspaceId }, }); await this.repository.delete({ id, workspaceId }); - return entity; + return messageChannel; } } 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 index 17d96d1743..bf07628b61 100644 --- 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 @@ -7,6 +7,7 @@ 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', + MESSAGE_CHANNEL_OWNERSHIP_VIOLATION = 'MESSAGE_CHANNEL_OWNERSHIP_VIOLATION', } const getMessageChannelExceptionUserFriendlyMessage = ( @@ -17,6 +18,8 @@ const getMessageChannelExceptionUserFriendlyMessage = ( return msg`Message channel not found.`; case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT: return msg`Invalid message channel input.`; + case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION: + return msg`You do not have access to this message channel.`; default: assertUnreachable(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 index e54273daf1..6cd491b409 100644 --- 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 @@ -1,24 +1,37 @@ import { UseGuards, UseInterceptors } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; -import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; 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 { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; 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 { NoPermissionGuard } from 'src/engine/guards/no-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 { + MessageChannelException, + MessageChannelExceptionCode, +} from 'src/engine/metadata-modules/message-channel/message-channel.exception'; 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 { + MessageChannelPendingGroupEmailsAction, + MessageChannelSyncStage, + type MessageChannelWorkspaceEntity, +} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service'; +import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service'; +import { Not } from 'typeorm'; @UseGuards(WorkspaceAuthGuard, FeatureFlagGuard) @UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor) @@ -26,13 +39,16 @@ import { MessageChannelMetadataService } from 'src/engine/metadata-modules/messa export class MessageChannelResolver { constructor( private readonly messageChannelMetadataService: MessageChannelMetadataService, + private readonly messageFolderDataAccessService: MessageFolderDataAccessService, + private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService, ) {} @Query(() => [MessageChannelDTO]) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) - async messageChannels( + async myMessageChannels( @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, @Args('connectedAccountId', { type: () => UUIDScalarType, nullable: true, @@ -40,59 +56,80 @@ export class MessageChannelResolver { connectedAccountId?: string, ): Promise { if (connectedAccountId) { - return this.messageChannelMetadataService.findByConnectedAccountId( - connectedAccountId, - workspace.id, + return this.messageChannelMetadataService.findByConnectedAccountIdForUser( + { + connectedAccountId, + userWorkspaceId, + workspaceId: 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, + return this.messageChannelMetadataService.findByUserWorkspaceId({ + userWorkspaceId, workspaceId: workspace.id, }); } @Mutation(() => MessageChannelDTO) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) async updateMessageChannel( @Args('input') input: UpdateMessageChannelInput, @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, ): Promise { - return this.messageChannelMetadataService.update( - input.id, - workspace.id, - input.update, - ); - } + const messageChannel = + await this.messageChannelMetadataService.verifyOwnership({ + id: input.id, + userWorkspaceId, + workspaceId: workspace.id, + }); - @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); + const isSyncOngoing = + messageChannel.syncStage === + MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING; + + const foldersWithPendingAction = + await this.messageFolderDataAccessService.find(workspace.id, { + messageChannelId: messageChannel.id, + pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE), + }); + + const hasPendingGroupEmailsAction = + messageChannel.pendingGroupEmailsAction !== + MessageChannelPendingGroupEmailsAction.NONE; + + if ( + isSyncOngoing && + (foldersWithPendingAction.length > 0 || hasPendingGroupEmailsAction) + ) { + throw new MessageChannelException( + 'Cannot update message channel while sync is ongoing with pending actions', + MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT, + ); + } + + if ( + messageChannel.syncStage !== + MessageChannelSyncStage.PENDING_CONFIGURATION && + isDefined(input.update.excludeGroupEmails) && + input.update.excludeGroupEmails !== messageChannel.excludeGroupEmails + ) { + // Service expects WorkspaceEntity type but only reads .id + await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction( + messageChannel as unknown as MessageChannelWorkspaceEntity, + workspace.id, + input.update.excludeGroupEmails + ? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION + : MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT, + ); + } + + return this.messageChannelMetadataService.update({ + id: input.id, + workspaceId: workspace.id, + data: input.update, + }); } } 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 index 5f50e2647c..c60e3c7ae9 100644 --- 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 @@ -1,9 +1,14 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { + ForbiddenError, 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'; import { MessageChannelException, MessageChannelExceptionCode, @@ -16,11 +21,23 @@ export const messageChannelGraphqlApiExceptionHandler = (error: Error) => { throw new NotFoundError(error); case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT: throw new UserInputError(error); + case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION: + throw new ForbiddenError(error); default: { return assertUnreachable(error.code); } } } + if (error instanceof ConnectedAccountException) { + switch (error.code) { + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION: + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND: + throw new ForbiddenError(error); + default: + break; + } + } + throw error; }; 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 index 83154b2dbd..b2d64563e7 100644 --- 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 @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FeatureFlagKey } from 'twenty-shared/types'; +import { validate as uuidValidate } from 'uuid'; import { type FindOneOptions, type FindOptionsWhere, @@ -32,6 +33,31 @@ export class MessageFolderDataAccessService { ); } + // Workspace stores parentFolderId as an externalId (text), + // core stores it as a uuid FK. Resolve during dual-write. + private async toCore( + workspaceId: string, + data: Partial, + ): Promise> { + const coreData: Record = { ...data, workspaceId }; + const parentFolderId = coreData.parentFolderId as string | null; + + if (parentFolderId && !uuidValidate(parentFolderId)) { + const parentFolder = await this.coreRepository.findOne({ + where: { + workspaceId, + messageChannelId: coreData.messageChannelId as string, + externalId: parentFolderId, + }, + select: ['id'], + }); + + coreData.parentFolderId = parentFolder?.id ?? null; + } + + return coreData; + } + async getWorkspaceRepository(workspaceId: string) { return this.globalWorkspaceOrmManager.getRepository( workspaceId, @@ -94,10 +120,11 @@ export class MessageFolderDataAccessService { if (await this.isMigrated(workspaceId)) { try { - await this.coreRepository.save({ - ...data, - workspaceId, - } as unknown as MessageFolderEntity); + const coreData = await this.toCore(workspaceId, data); + + await this.coreRepository.save( + coreData as unknown as MessageFolderEntity, + ); } catch (error) { this.logger.error( `Failed to dual-write messageFolder to core: ${error}`, 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 index c4137572e7..3cdf9b56ef 100644 --- 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 @@ -25,9 +25,7 @@ export class MessageFolderDTO { @Field(() => String, { nullable: true }) name: string | null; - @IsString() - @IsOptional() - @Field(() => String, { nullable: true }) + @HideField() syncCursor: string | null; @IsBoolean() 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 index eff88aa6ff..596806dfc5 100644 --- 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 @@ -3,38 +3,20 @@ 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() @@ -49,3 +31,16 @@ export class UpdateMessageFolderInput { @Field(() => UpdateMessageFolderInputUpdates) update: UpdateMessageFolderInputUpdates; } + +@InputType() +export class UpdateMessageFoldersInput { + @IsUUID('4', { each: true }) + @IsNotEmpty({ each: true }) + @Field(() => [UUIDScalarType]) + ids: 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 index 744220b219..879790afa7 100644 --- 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 @@ -41,6 +41,7 @@ export class MessageFolderEntity extends WorkspaceRelatedEntity { type: 'enum', enum: MessageFolderPendingSyncAction, nullable: false, + default: MessageFolderPendingSyncAction.NONE, }) pendingSyncAction: MessageFolderPendingSyncAction; 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 index 68a46df838..46369ac9c0 100644 --- 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 @@ -3,6 +3,8 @@ 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 { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module'; +import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.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'; @@ -15,6 +17,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi AuthModule, PermissionsModule, FeatureFlagModule, + ConnectedAccountMetadataModule, + MessageChannelMetadataModule, ], providers: [ MessageFolderMetadataService, 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 index c10350d3c2..1933b95925 100644 --- 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 @@ -1,40 +1,159 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; +import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; 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'; +import { + MessageFolderException, + MessageFolderExceptionCode, +} from 'src/engine/metadata-modules/message-folder/message-folder.exception'; +import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service'; @Injectable() export class MessageFolderMetadataService { constructor( @InjectRepository(MessageFolderEntity) private readonly repository: Repository, + private readonly messageChannelMetadataService: MessageChannelMetadataService, + private readonly connectedAccountMetadataService: ConnectedAccountMetadataService, ) {} async findAll(workspaceId: string): Promise { return this.repository.find({ where: { workspaceId } }); } - async findByMessageChannelId( - messageChannelId: string, - workspaceId: string, - ): Promise { + async findByUserWorkspaceId({ + userWorkspaceId, + workspaceId, + }: { + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const userAccountIds = + await this.connectedAccountMetadataService.getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }); + + const userChannels = + await this.messageChannelMetadataService.findByConnectedAccountIds({ + connectedAccountIds: userAccountIds, + workspaceId, + }); + + const userChannelIds = userChannels.map((channel) => channel.id); + + return this.findByMessageChannelIds({ + messageChannelIds: userChannelIds, + workspaceId, + }); + } + + async findByMessageChannelIdForUser({ + messageChannelId, + userWorkspaceId, + workspaceId, + }: { + messageChannelId: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + await this.messageChannelMetadataService.verifyOwnership({ + id: messageChannelId, + userWorkspaceId, + workspaceId, + }); + + return this.findByMessageChannelId({ messageChannelId, workspaceId }); + } + + async findByMessageChannelId({ + messageChannelId, + workspaceId, + }: { + messageChannelId: string; + workspaceId: string; + }): Promise { return this.repository.find({ where: { messageChannelId, workspaceId }, }); } - async findById( - id: string, - workspaceId: string, - ): Promise { + async findByMessageChannelIds({ + messageChannelIds, + workspaceId, + }: { + messageChannelIds: string[]; + workspaceId: string; + }): Promise { + if (messageChannelIds.length === 0) { + return []; + } + + return this.repository.find({ + where: { messageChannelId: In(messageChannelIds), workspaceId }, + }); + } + + async findById({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { return this.repository.findOne({ where: { id, workspaceId } }); } + async verifyOwnership({ + id, + userWorkspaceId, + workspaceId, + }: { + id: string; + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const messageFolder = await this.repository.findOne({ + where: { id, workspaceId }, + }); + + if (!messageFolder) { + throw new MessageFolderException( + `Message folder ${id} not found`, + MessageFolderExceptionCode.MESSAGE_FOLDER_NOT_FOUND, + ); + } + + const userAccountIds = + await this.connectedAccountMetadataService.getUserConnectedAccountIds({ + userWorkspaceId, + workspaceId, + }); + + const messageChannel = await this.messageChannelMetadataService.findById({ + id: messageFolder.messageChannelId, + workspaceId, + }); + + if ( + !messageChannel || + !userAccountIds.includes(messageChannel.connectedAccountId) + ) { + throw new MessageFolderException( + `Message folder ${id} does not belong to user workspace ${userWorkspaceId}`, + MessageFolderExceptionCode.MESSAGE_FOLDER_OWNERSHIP_VIOLATION, + ); + } + + return messageFolder; + } + async create( data: Partial & { workspaceId: string; @@ -47,11 +166,15 @@ export class MessageFolderMetadataService { return this.repository.save(entity); } - async update( - id: string, - workspaceId: string, - data: Partial, - ): Promise { + async update({ + id, + workspaceId, + data, + }: { + id: string; + workspaceId: string; + data: Partial; + }): Promise { await this.repository.update( { id, workspaceId }, data as Record, @@ -60,13 +183,36 @@ export class MessageFolderMetadataService { return this.repository.findOneOrFail({ where: { id, workspaceId } }); } - async delete(id: string, workspaceId: string): Promise { - const entity = await this.repository.findOneOrFail({ + async updateMany({ + ids, + workspaceId, + data, + }: { + ids: string[]; + workspaceId: string; + data: Partial; + }): Promise { + await this.repository.update( + { id: In(ids), workspaceId }, + data as Record, + ); + + return this.repository.find({ where: { id: In(ids), workspaceId } }); + } + + async delete({ + id, + workspaceId, + }: { + id: string; + workspaceId: string; + }): Promise { + const messageFolder = await this.repository.findOneOrFail({ where: { id, workspaceId }, }); await this.repository.delete({ id, workspaceId }); - return entity; + return messageFolder; } } 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 index 826152d519..21e0f27102 100644 --- 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 @@ -7,6 +7,7 @@ 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', + MESSAGE_FOLDER_OWNERSHIP_VIOLATION = 'MESSAGE_FOLDER_OWNERSHIP_VIOLATION', } const getMessageFolderExceptionUserFriendlyMessage = ( @@ -17,6 +18,8 @@ const getMessageFolderExceptionUserFriendlyMessage = ( return msg`Message folder not found.`; case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT: return msg`Invalid message folder input.`; + case MessageFolderExceptionCode.MESSAGE_FOLDER_OWNERSHIP_VIOLATION: + return msg`You do not have access to this message folder.`; default: assertUnreachable(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 index d495d0b1d0..9588bffbee 100644 --- 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 @@ -1,22 +1,24 @@ 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 { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; 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 { NoPermissionGuard } from 'src/engine/guards/no-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 { + UpdateMessageFolderInput, + UpdateMessageFoldersInput, +} 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'; @@ -29,10 +31,11 @@ export class MessageFolderResolver { ) {} @Query(() => [MessageFolderDTO]) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) - async messageFolders( + async myMessageFolders( @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, @Args('messageChannelId', { type: () => UUIDScalarType, nullable: true, @@ -40,59 +43,62 @@ export class MessageFolderResolver { messageChannelId?: string, ): Promise { if (messageChannelId) { - return this.messageFolderMetadataService.findByMessageChannelId( + return this.messageFolderMetadataService.findByMessageChannelIdForUser({ messageChannelId, - workspace.id, - ); + userWorkspaceId, + workspaceId: 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, + return this.messageFolderMetadataService.findByUserWorkspaceId({ + userWorkspaceId, workspaceId: workspace.id, }); } @Mutation(() => MessageFolderDTO) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) async updateMessageFolder( @Args('input') input: UpdateMessageFolderInput, @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, ): Promise { - return this.messageFolderMetadataService.update( - input.id, - workspace.id, - input.update, - ); + await this.messageFolderMetadataService.verifyOwnership({ + id: input.id, + userWorkspaceId, + workspaceId: workspace.id, + }); + + return this.messageFolderMetadataService.update({ + id: input.id, + workspaceId: workspace.id, + data: input.update, + }); } - @Mutation(() => MessageFolderDTO) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS)) + @Mutation(() => [MessageFolderDTO]) + @UseGuards(NoPermissionGuard) @RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED) - async deleteMessageFolder( - @Args('id', { type: () => UUIDScalarType }) id: string, + async updateMessageFolders( + @Args('input') input: UpdateMessageFoldersInput, @AuthWorkspace() workspace: WorkspaceEntity, - ): Promise { - return this.messageFolderMetadataService.delete(id, workspace.id); + @AuthUserWorkspaceId() userWorkspaceId: string, + ): Promise { + await Promise.all( + input.ids.map((id) => + this.messageFolderMetadataService.verifyOwnership({ + id, + userWorkspaceId, + workspaceId: workspace.id, + }), + ), + ); + + return this.messageFolderMetadataService.updateMany({ + ids: input.ids, + workspaceId: workspace.id, + data: input.update, + }); } } 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 index 7a4f3c0477..320942e3a2 100644 --- 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 @@ -1,9 +1,18 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { + ForbiddenError, 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'; +import { + MessageChannelException, + MessageChannelExceptionCode, +} from 'src/engine/metadata-modules/message-channel/message-channel.exception'; import { MessageFolderException, MessageFolderExceptionCode, @@ -16,11 +25,33 @@ export const messageFolderGraphqlApiExceptionHandler = (error: Error) => { throw new NotFoundError(error); case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT: throw new UserInputError(error); + case MessageFolderExceptionCode.MESSAGE_FOLDER_OWNERSHIP_VIOLATION: + throw new ForbiddenError(error); default: { return assertUnreachable(error.code); } } } + if (error instanceof MessageChannelException) { + switch (error.code) { + case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION: + case MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND: + throw new ForbiddenError(error); + default: + break; + } + } + + if (error instanceof ConnectedAccountException) { + switch (error.code) { + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION: + case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND: + throw new ForbiddenError(error); + default: + break; + } + } + throw error; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts index 0b8aab5a92..5f679fdbd6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts @@ -20,7 +20,8 @@ export class MinimalMetadataResolver { @Query(() => MinimalMetadataDTO) async minimalMetadata( @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, ): Promise { return this.minimalMetadataService.getMinimalMetadata( workspace.id, diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts index a5cd3a24ac..74ea1329ee 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts @@ -44,7 +44,8 @@ export class NavigationMenuItemResolver { @UseGuards(NoPermissionGuard) async navigationMenuItems( @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, ): Promise { return await this.navigationMenuItemService.findAll({ workspaceId: workspace.id, @@ -69,7 +70,8 @@ export class NavigationMenuItemResolver { async createNavigationMenuItem( @Args('input') input: CreateNavigationMenuItemInput, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, @Context() context: { req: { application?: ApplicationEntity } }, ): Promise { @@ -87,7 +89,8 @@ export class NavigationMenuItemResolver { async updateNavigationMenuItem( @Args('input') input: UpdateOneNavigationMenuItemInput, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, @Context() context: { req: { application?: ApplicationEntity } }, ): Promise { @@ -105,7 +108,8 @@ export class NavigationMenuItemResolver { async deleteNavigationMenuItem( @Args('id', { type: () => UUIDScalarType }) id: string, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, @Context() context: { req: { application?: ApplicationEntity } }, ): Promise { diff --git a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts index 2b08025c4b..1b638029b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts @@ -56,7 +56,8 @@ export class ViewController { async findMany( @RequestLocale() locale: keyof typeof APP_LOCALES | undefined, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @Query('objectMetadataId') objectMetadataId?: string, ): Promise { const views = objectMetadataId @@ -134,7 +135,8 @@ export class ViewController { @Body() input: UpdateViewInput, @RequestLocale() locale: keyof typeof APP_LOCALES | undefined, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, ): Promise { const updatedView = await this.viewService.updateOne({ updateViewInput: { diff --git a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts index 6ec54201d7..a5b86ffd18 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts @@ -96,7 +96,8 @@ export class ViewResolver { @UseGuards(CustomPermissionGuard) async getViews( @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @Args('objectMetadataId', { type: () => String, nullable: true }) objectMetadataId?: string, @Args('viewTypes', { type: () => [ViewType], nullable: true }) @@ -138,7 +139,8 @@ export class ViewResolver { async createView( @Args('input') input: CreateViewInput, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, ): Promise { const visibility = input.visibility ?? ViewVisibility.WORKSPACE; @@ -157,7 +159,8 @@ export class ViewResolver { @Args('id', { type: () => String }) id: string, @Args('input') input: UpdateViewInput, @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, ): Promise { return await this.viewService.updateOne({ updateViewInput: { ...input, id }, diff --git a/packages/twenty-server/src/engine/subscriptions/event-stream.resolver.ts b/packages/twenty-server/src/engine/subscriptions/event-stream.resolver.ts index 2c56b2c001..3bddb18cef 100644 --- a/packages/twenty-server/src/engine/subscriptions/event-stream.resolver.ts +++ b/packages/twenty-server/src/engine/subscriptions/event-stream.resolver.ts @@ -58,7 +58,8 @@ export class EventStreamResolver { @Args('eventStreamId') eventStreamId: string, @AuthWorkspace() workspace: WorkspaceEntity, @AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, ) { const eventStreamChannelId = eventStreamIdToChannelId(eventStreamId); @@ -124,7 +125,8 @@ export class EventStreamResolver { @Args('input') input: AddQuerySubscriptionInput, @AuthWorkspace() workspace: WorkspaceEntity, @AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, ): Promise { const eventStreamChannelId = eventStreamIdToChannelId(input.eventStreamId); @@ -168,7 +170,8 @@ export class EventStreamResolver { @Args('input') input: RemoveQueryFromEventStreamInput, @AuthWorkspace() workspace: WorkspaceEntity, @AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined, - @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, @AuthApiKey() apiKey: ApiKeyEntity | undefined, ): Promise { const eventStreamChannelId = eventStreamIdToChannelId(input.eventStreamId); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts index eb473e1ffa..8c05c8ae2b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts @@ -12,6 +12,7 @@ import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/s import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util'; import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util'; +import { seedMetadataEntities } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util'; import { seedServerId } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-server-id.util'; import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util'; import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util'; @@ -93,6 +94,8 @@ export const seedCoreSchema = async ({ await seedBillingSubscriptions({ queryRunner, schemaName, workspaceId }); } + await seedMetadataEntities({ queryRunner, schemaName, workspaceId }); + await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util.ts new file mode 100644 index 0000000000..8f45bb2b80 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util.ts @@ -0,0 +1,306 @@ +import { type QueryRunner } from 'typeorm'; + +import { + CalendarChannelVisibility, + MessageChannelSyncStage, + MessageChannelType, + MessageChannelVisibility, + MessageFolderPendingSyncAction, +} from 'twenty-shared/types'; + +import { + SEED_APPLE_WORKSPACE_ID, + SEED_YCOMBINATOR_WORKSPACE_ID, +} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant'; +import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util'; +import { CALENDAR_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant'; +import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; +import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant'; +import { MESSAGE_FOLDER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant'; + +type SeedMetadataEntitiesArgs = { + queryRunner: QueryRunner; + schemaName: string; + workspaceId: string; +}; + +export const seedMetadataEntities = async ({ + queryRunner, + schemaName, + workspaceId, +}: SeedMetadataEntitiesArgs) => { + if ( + workspaceId !== SEED_APPLE_WORKSPACE_ID && + workspaceId !== SEED_YCOMBINATOR_WORKSPACE_ID + ) { + return; + } + + if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) { + return; + } + + await seedConnectedAccounts({ queryRunner, schemaName, workspaceId }); + await seedMessageChannels({ queryRunner, schemaName, workspaceId }); + await seedCalendarChannels({ queryRunner, schemaName, workspaceId }); + await seedMessageFolders({ queryRunner, schemaName, workspaceId }); +}; + +const seedConnectedAccounts = async ({ + queryRunner, + schemaName, + workspaceId, +}: SeedMetadataEntitiesArgs) => { + const connectedAccounts = [ + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, + handle: 'tim@apple.dev', + provider: 'google', + userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.TIM, + workspaceId, + }, + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, + handle: 'jony.ive@apple.dev', + provider: 'google', + userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JONY, + workspaceId, + }, + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.PHIL, + handle: 'phil.schiler@apple.dev', + provider: 'google', + userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.PHIL, + workspaceId, + }, + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + handle: 'jane.austen@apple.dev', + provider: 'google', + userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JANE, + workspaceId, + }, + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE, + handle: 'jane-deletable@apple.dev', + provider: 'google', + userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JANE, + workspaceId, + }, + ]; + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(`${schemaName}.connectedAccount`, [ + 'id', + 'handle', + 'provider', + 'userWorkspaceId', + 'workspaceId', + ]) + .orIgnore() + .values(connectedAccounts) + .execute(); +}; + +const seedMessageChannels = async ({ + queryRunner, + schemaName, + workspaceId, +}: SeedMetadataEntitiesArgs) => { + const messageChannels = [ + { + id: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM, + handle: 'tim@apple.dev', + visibility: MessageChannelVisibility.SHARE_EVERYTHING, + type: MessageChannelType.EMAIL, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: 'SENT_AND_RECEIVED', + messageFolderImportPolicy: 'ALL_FOLDERS', + excludeNonProfessionalEmails: false, + excludeGroupEmails: false, + pendingGroupEmailsAction: 'NONE', + isSyncEnabled: true, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, + workspaceId, + }, + { + id: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY, + handle: 'jony.ive@apple.dev', + visibility: MessageChannelVisibility.SHARE_EVERYTHING, + type: MessageChannelType.EMAIL, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: 'SENT_AND_RECEIVED', + messageFolderImportPolicy: 'ALL_FOLDERS', + excludeNonProfessionalEmails: false, + excludeGroupEmails: false, + pendingGroupEmailsAction: 'NONE', + isSyncEnabled: true, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, + workspaceId, + }, + { + id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + handle: 'jane.austen@apple.dev', + visibility: MessageChannelVisibility.SHARE_EVERYTHING, + type: MessageChannelType.EMAIL, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: 'SENT_AND_RECEIVED', + messageFolderImportPolicy: 'ALL_FOLDERS', + excludeNonProfessionalEmails: false, + excludeGroupEmails: false, + pendingGroupEmailsAction: 'NONE', + isSyncEnabled: true, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + workspaceId, + }, + ]; + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(`${schemaName}.messageChannel`, [ + 'id', + 'handle', + 'visibility', + 'type', + 'syncStage', + 'isContactAutoCreationEnabled', + 'contactAutoCreationPolicy', + 'messageFolderImportPolicy', + 'excludeNonProfessionalEmails', + 'excludeGroupEmails', + 'pendingGroupEmailsAction', + 'isSyncEnabled', + 'connectedAccountId', + 'workspaceId', + ]) + .orIgnore() + .values(messageChannels) + .execute(); +}; + +const seedCalendarChannels = async ({ + queryRunner, + schemaName, + workspaceId, +}: SeedMetadataEntitiesArgs) => { + const calendarChannels = [ + { + id: CALENDAR_CHANNEL_DATA_SEED_IDS.TIM, + handle: 'tim@apple.dev', + visibility: CalendarChannelVisibility.METADATA, + syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING', + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: 'NONE', + isSyncEnabled: true, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, + workspaceId, + }, + { + id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY, + handle: 'jony@apple.dev', + visibility: CalendarChannelVisibility.SHARE_EVERYTHING, + syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING', + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: 'NONE', + isSyncEnabled: true, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, + workspaceId, + }, + { + id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE, + handle: 'jane.austen@apple.dev', + visibility: CalendarChannelVisibility.SHARE_EVERYTHING, + syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING', + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: 'NONE', + isSyncEnabled: true, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + workspaceId, + }, + ]; + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(`${schemaName}.calendarChannel`, [ + 'id', + 'handle', + 'visibility', + 'syncStage', + 'isContactAutoCreationEnabled', + 'contactAutoCreationPolicy', + 'isSyncEnabled', + 'connectedAccountId', + 'workspaceId', + ]) + .orIgnore() + .values(calendarChannels) + .execute(); +}; + +const seedMessageFolders = async ({ + queryRunner, + schemaName, + workspaceId, +}: SeedMetadataEntitiesArgs) => { + const messageFolders = [ + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_INBOX, + name: 'INBOX', + isSynced: true, + isSentFolder: false, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM, + workspaceId, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX, + name: 'INBOX', + isSynced: true, + isSentFolder: false, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY, + workspaceId, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX, + name: 'INBOX', + isSynced: true, + isSentFolder: false, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + workspaceId, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT, + name: 'Sent', + isSynced: true, + isSentFolder: true, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + workspaceId, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + ]; + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(`${schemaName}.messageFolder`, [ + 'id', + 'name', + 'isSynced', + 'isSentFolder', + 'messageChannelId', + 'workspaceId', + 'pendingSyncAction', + ]) + .orIgnore() + .values(messageFolders) + .execute(); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant.ts index c42d8f3e44..f8f6037647 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant.ts @@ -26,6 +26,7 @@ const GENERATE_CALENDAR_CHANNEL_IDS = (): Record => { CHANNEL_IDS['TIM'] = '20202020-a40f-4faf-bb9f-c6f9945b8203'; CHANNEL_IDS['JONY'] = '20202020-a40f-4faf-bb9f-c6f9945b8204'; CHANNEL_IDS['PHIL'] = '20202020-a40f-4faf-bb9f-c6f9945b8205'; + CHANNEL_IDS['JANE'] = '20202020-a40f-4faf-bb9f-c6f9945b8208'; CHANNEL_IDS['COMPANY_MAIN'] = '20202020-a40f-4faf-bb9f-c6f9945b8206'; CHANNEL_IDS['TEAM_CALENDAR'] = '20202020-a40f-4faf-bb9f-c6f9945b8207'; @@ -59,6 +60,14 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [ isContactAutoCreationEnabled: true, isSyncEnabled: true, }, + { + id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + handle: 'jane.austen@apple.dev', + visibility: CalendarChannelVisibility.SHARE_EVERYTHING, + isContactAutoCreationEnabled: true, + isSyncEnabled: true, + }, { id: CALENDAR_CHANNEL_DATA_SEED_IDS.COMPANY_MAIN, connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant.ts index 1218ff0a12..ad47f8e9a0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant.ts @@ -31,6 +31,8 @@ export const CONNECTED_ACCOUNT_DATA_SEED_IDS = { TIM: '20202020-9ac0-4390-9a1a-ab4d2c4e1bb7', JONY: '20202020-0cc8-4d60-a3a4-803245698908', PHIL: '20202020-cafc-4323-908d-e5b42ad69fdf', + JANE: '20202020-b5c7-46f0-bf5c-3f4e4b3f7c1a', + JANE_DELETABLE: '20202020-d1e5-4a8f-9c3b-7f6d5e4c3b2a', }; export const CONNECTED_ACCOUNT_DATA_SEEDS: ConnectedAccountDataSeed[] = [ @@ -70,4 +72,28 @@ export const CONNECTED_ACCOUNT_DATA_SEEDS: ConnectedAccountDataSeed[] = [ provider: 'google', handle: 'phil.schiler@apple.dev', }, + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + lastSyncHistoryId: 'exampleLastSyncHistory', + accountOwnerId: WORKSPACE_MEMBER_DATA_SEED_IDS.JANE, + refreshToken: 'exampleRefreshToken', + accessToken: 'exampleAccessToken', + provider: 'google', + handle: 'jane.austen@apple.dev', + }, + { + id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + lastSyncHistoryId: 'exampleLastSyncHistory', + accountOwnerId: WORKSPACE_MEMBER_DATA_SEED_IDS.JANE, + refreshToken: 'exampleRefreshToken', + accessToken: 'exampleAccessToken', + provider: 'google', + handle: 'jane-deletable@apple.dev', + }, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts index 3b7dd728a1..1c864c7bf7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts @@ -40,6 +40,7 @@ const GENERATE_MESSAGE_CHANNEL_IDS = (): Record => { CHANNEL_IDS['TIM'] = '20202020-9b80-4c2c-a597-383db48de1d6'; CHANNEL_IDS['JONY'] = '20202020-5ffe-4b32-814a-983d5e4911cd'; CHANNEL_IDS['PHIL'] = '20202020-e2f1-49b5-85d2-5d3a3386990c'; + CHANNEL_IDS['JANE'] = '20202020-8c4d-4e71-a672-2e6a8c9f1b3d'; CHANNEL_IDS['SUPPORT'] = '20202020-e2f1-49b5-85d2-5d3a3386990d'; CHANNEL_IDS['SALES'] = '20202020-e2f1-49b5-85d2-5d3a3386990e'; @@ -88,6 +89,19 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [ visibility: MessageChannelVisibility.SHARE_EVERYTHING, syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, }, + { + id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + isContactAutoCreationEnabled: true, + type: MessageChannelType.EMAIL, + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + handle: 'jane.austen@apple.dev', + isSyncEnabled: true, + visibility: MessageChannelVisibility.SHARE_EVERYTHING, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + }, { id: MESSAGE_CHANNEL_DATA_SEED_IDS.SUPPORT, createdAt: new Date(), diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant.ts new file mode 100644 index 0000000000..2a3ed2d35d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant.ts @@ -0,0 +1,94 @@ +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; + +import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant'; + +type MessageFolderDataSeed = { + id: string; + createdAt: Date; + updatedAt: Date; + deletedAt: Date | null; + name: string; + isSynced: boolean; + isSentFolder: boolean; + messageChannelId: string; + pendingSyncAction: MessageFolderPendingSyncAction; +}; + +export const MESSAGE_FOLDER_DATA_SEED_COLUMNS: (keyof MessageFolderDataSeed)[] = + [ + 'id', + 'createdAt', + 'updatedAt', + 'deletedAt', + 'name', + 'isSynced', + 'isSentFolder', + 'messageChannelId', + 'pendingSyncAction', + ]; + +export const MESSAGE_FOLDER_DATA_SEED_IDS = { + TIM_INBOX: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d0e', + TIM_SENT: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d1e', + JONY_INBOX: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d2e', + JANE_INBOX: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d3e', + JANE_SENT: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d4e', +}; + +export const MESSAGE_FOLDER_DATA_SEEDS: MessageFolderDataSeed[] = [ + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_INBOX, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + name: 'INBOX', + isSynced: true, + isSentFolder: false, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_SENT, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + name: 'Sent', + isSynced: true, + isSentFolder: true, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + name: 'INBOX', + isSynced: true, + isSentFolder: false, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + name: 'INBOX', + isSynced: true, + isSentFolder: false, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + name: 'Sent', + isSynced: true, + isSentFolder: true, + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts index 7b07b782c9..24687b2235 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts @@ -56,6 +56,10 @@ import { MESSAGE_CHANNEL_DATA_SEED_COLUMNS, MESSAGE_CHANNEL_DATA_SEEDS, } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant'; +import { + MESSAGE_FOLDER_DATA_SEED_COLUMNS, + MESSAGE_FOLDER_DATA_SEEDS, +} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant'; import { MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_DATA_SEED_COLUMNS, MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_DATA_SEEDS, @@ -194,8 +198,13 @@ const getRecordSeedsBatches = ( }, ]; - // Batch 4: Depends on person/company or independent + // Batch 4: Depends on person/company/messageChannel or independent const batch4: RecordSeedConfig[] = [ + { + tableName: 'messageFolder', + pgColumns: MESSAGE_FOLDER_DATA_SEED_COLUMNS, + recordSeeds: MESSAGE_FOLDER_DATA_SEEDS, + }, { tableName: 'opportunity', pgColumns: OPPORTUNITY_DATA_SEED_COLUMNS, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/__tests__/calendar-fetch-events.service.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/__tests__/calendar-fetch-events.service.spec.ts new file mode 100644 index 0000000000..0b166ebe42 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/__tests__/calendar-fetch-events.service.spec.ts @@ -0,0 +1,227 @@ +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 { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service'; + +const mockCalendarChannelSyncStatusService = { + markAsCalendarEventListFetchOngoing: jest.fn(), + markAsCalendarEventListFetchPending: jest.fn(), + markAsCalendarEventsImportPending: jest.fn(), +}; + +const mockGetCalendarEventsService = { + getCalendarEvents: jest.fn(), +}; + +const mockCalendarChannelDataAccessService = { + update: jest.fn(), +}; + +const mockCalendarAccountAuthenticationService = { + validateAndRefreshConnectedAccountAuthentication: jest + .fn() + .mockResolvedValue({ + accessToken: 'fresh-access-token', + refreshToken: 'fresh-refresh-token', + }), +}; + +const mockCalendarEventsImportService = { + processCalendarEventsImport: jest.fn(), +}; + +const mockCalendarEventImportErrorHandlerService = { + handleDriverException: jest.fn(), +}; + +const mockCacheStorage = { + setAdd: jest.fn(), +}; + +const mockGlobalWorkspaceOrmManager = { + executeInWorkspaceContext: jest.fn(async (callback: () => Promise) => { + await callback(); + }), +}; + +const workspaceId = 'workspace-123'; + +const baseConnectedAccount = { + id: 'account-123', + provider: 'google', + refreshToken: 'refresh-token', + accessToken: 'access-token', + handle: 'test@example.com', +} as unknown as ConnectedAccountWorkspaceEntity; + +const createCalendarChannel = ( + syncCursor: string | null, +): CalendarChannelWorkspaceEntity => + ({ + id: 'channel-123', + syncCursor, + connectedAccountId: 'account-123', + }) as unknown as CalendarChannelWorkspaceEntity; + +describe('CalendarFetchEventsService', () => { + let service: CalendarFetchEventsService; + + beforeEach(() => { + jest.clearAllMocks(); + + mockGetCalendarEventsService.getCalendarEvents.mockResolvedValue({ + fullEvents: true, + calendarEvents: [{ id: 'event-1' }], + nextSyncCursor: 'new-cursor-abc', + }); + + service = new CalendarFetchEventsService( + mockCacheStorage as any, + mockGlobalWorkspaceOrmManager as any, + mockCalendarChannelDataAccessService as any, + mockCalendarChannelSyncStatusService as any, + mockGetCalendarEventsService as any, + mockCalendarEventImportErrorHandlerService as any, + mockCalendarEventsImportService as any, + mockCalendarAccountAuthenticationService as any, + ); + }); + + describe('syncCursor handling (backwards compatibility)', () => { + it('should perform full sync when syncCursor is null (core schema)', async () => { + const calendarChannel = createCalendarChannel(null); + + await service.fetchCalendarEvents( + calendarChannel, + baseConnectedAccount, + workspaceId, + ); + + expect( + mockGetCalendarEventsService.getCalendarEvents, + ).toHaveBeenCalledWith( + expect.objectContaining({ handle: 'test@example.com' }), + undefined, + ); + }); + + it('should perform full sync when syncCursor is empty string (workspace schema)', async () => { + const calendarChannel = createCalendarChannel(''); + + await service.fetchCalendarEvents( + calendarChannel, + baseConnectedAccount, + workspaceId, + ); + + expect( + mockGetCalendarEventsService.getCalendarEvents, + ).toHaveBeenCalledWith( + expect.objectContaining({ handle: 'test@example.com' }), + undefined, + ); + }); + + it('should use existing syncCursor for incremental sync', async () => { + const calendarChannel = createCalendarChannel('cursor-xyz'); + + await service.fetchCalendarEvents( + calendarChannel, + baseConnectedAccount, + workspaceId, + ); + + expect( + mockGetCalendarEventsService.getCalendarEvents, + ).toHaveBeenCalledWith( + expect.objectContaining({ handle: 'test@example.com' }), + 'cursor-xyz', + ); + }); + + it('should update syncCursor after successful fetch regardless of initial cursor state', async () => { + const calendarChannel = createCalendarChannel(null); + + await service.fetchCalendarEvents( + calendarChannel, + baseConnectedAccount, + workspaceId, + ); + + expect(mockCalendarChannelDataAccessService.update).toHaveBeenCalledWith( + workspaceId, + { id: 'channel-123' }, + { syncCursor: 'new-cursor-abc' }, + ); + }); + + it('should not throw when feature flag switches from off (workspace empty string) to on (core null)', async () => { + // Simulate flag OFF: workspace returns empty string + const workspaceChannel = createCalendarChannel(''); + + await expect( + service.fetchCalendarEvents( + workspaceChannel, + baseConnectedAccount, + workspaceId, + ), + ).resolves.not.toThrow(); + + jest.clearAllMocks(); + mockGetCalendarEventsService.getCalendarEvents.mockResolvedValue({ + fullEvents: true, + calendarEvents: [{ id: 'event-2' }], + nextSyncCursor: 'new-cursor-def', + }); + + // Simulate flag ON: core returns null + const coreChannel = createCalendarChannel(null); + + await expect( + service.fetchCalendarEvents( + coreChannel, + baseConnectedAccount, + workspaceId, + ), + ).resolves.not.toThrow(); + + expect( + mockGetCalendarEventsService.getCalendarEvents, + ).toHaveBeenCalledWith(expect.anything(), undefined); + }); + + it('should preserve incremental sync behavior when toggling feature flag with existing cursor', async () => { + // Flag OFF: workspace has cursor + const workspaceChannel = createCalendarChannel('cursor-from-workspace'); + + await service.fetchCalendarEvents( + workspaceChannel, + baseConnectedAccount, + workspaceId, + ); + + expect( + mockGetCalendarEventsService.getCalendarEvents, + ).toHaveBeenCalledWith(expect.anything(), 'cursor-from-workspace'); + + jest.clearAllMocks(); + mockGetCalendarEventsService.getCalendarEvents.mockResolvedValue({ + fullEvents: true, + calendarEvents: [{ id: 'event-3' }], + nextSyncCursor: 'newer-cursor', + }); + + // Flag ON: core has same cursor (dual-written) + const coreChannel = createCalendarChannel('cursor-from-workspace'); + + await service.fetchCalendarEvents( + coreChannel, + baseConnectedAccount, + workspaceId, + ); + + expect( + mockGetCalendarEventsService.getCalendarEvents, + ).toHaveBeenCalledWith(expect.anything(), 'cursor-from-workspace'); + }); + }); +}); 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 a441eedc14..c29a33ed08 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 @@ -1,7 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -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'; @@ -71,17 +69,10 @@ export class CalendarFetchEventsService { refreshToken, }; - if (!isDefined(calendarChannel.syncCursor)) { - throw new CalendarEventImportDriverException( - 'Sync cursor is required', - CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR, - ); - } - const getCalendarEventsResponse = await this.getCalendarEventsService.getCalendarEvents( connectedAccountWithFreshTokens, - calendarChannel.syncCursor, + calendarChannel.syncCursor || undefined, ); const hasFullEvents = getCalendarEventsResponse.fullEvents; diff --git a/packages/twenty-server/test/integration/metadata/suites/calendar-channel/calendar-channel-resolver.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/calendar-channel/calendar-channel-resolver.integration-spec.ts new file mode 100644 index 0000000000..da7a487e7e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/calendar-channel/calendar-channel-resolver.integration-spec.ts @@ -0,0 +1,126 @@ +import { gql } from 'graphql-tag'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { FeatureFlagKey } from 'twenty-shared/types'; + +import { CALENDAR_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant'; +import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; + +describe('calendarChannelResolver (e2e)', () => { + beforeAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: true, + expectToFail: false, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: false, + expectToFail: false, + }); + }); + + describe('myCalendarChannels', () => { + it('should return only the current user calendar channels', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyCalendarChannels { + myCalendarChannels { + id + handle + visibility + syncStatus + syncStage + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const channels = response.body.data.myCalendarChannels; + const channelIds = channels.map( + (channel: { id: string }) => channel.id, + ); + + expect(channelIds).toContain(CALENDAR_CHANNEL_DATA_SEED_IDS.JANE); + expect(channelIds).not.toContain( + CALENDAR_CHANNEL_DATA_SEED_IDS.JONY, + ); + }); + + it('should deny filtering by another user connectedAccountId', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyCalendarChannels($connectedAccountId: UUID) { + myCalendarChannels(connectedAccountId: $connectedAccountId) { + id + } + } + `, + variables: { + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + }); + + describe('updateCalendarChannel', () => { + it('should allow updating own channel', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation UpdateCalendarChannel( + $input: UpdateCalendarChannelInput! + ) { + updateCalendarChannel(input: $input) { + id + visibility + } + } + `, + variables: { + input: { + id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE, + update: { visibility: 'METADATA' }, + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.updateCalendarChannel.visibility).toBe( + 'METADATA', + ); + }); + + it('should deny updating another user channel', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation UpdateCalendarChannel( + $input: UpdateCalendarChannelInput! + ) { + updateCalendarChannel(input: $input) { + id + } + } + `, + variables: { + input: { + id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY, + update: { visibility: 'METADATA' }, + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/connected-account/connected-account-resolver.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/connected-account/connected-account-resolver.integration-spec.ts new file mode 100644 index 0000000000..63541cb01a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/connected-account/connected-account-resolver.integration-spec.ts @@ -0,0 +1,188 @@ +import { gql } from 'graphql-tag'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { makeMetadataAPIRequestWithMemberRole } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-member-role.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { FeatureFlagKey } from 'twenty-shared/types'; + +import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; + +describe('connectedAccountResolver (e2e)', () => { + beforeAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: true, + expectToFail: false, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: false, + expectToFail: false, + }); + }); + + describe('myConnectedAccounts', () => { + it('should return only the current user connected accounts', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyConnectedAccounts { + myConnectedAccounts { + id + handle + provider + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const accounts = response.body.data.myConnectedAccounts; + const accountIds = accounts.map( + (account: { id: string }) => account.id, + ); + + expect(accountIds).toContain(CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE); + expect(accountIds).not.toContain( + CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, + ); + }); + + it('should not return sensitive fields', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyConnectedAccounts { + myConnectedAccounts { + id + handle + provider + authFailedAt + scopes + handleAliases + lastSignedInAt + userWorkspaceId + createdAt + updatedAt + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const account = response.body.data.myConnectedAccounts.find( + (connectedAccount: { id: string }) => + connectedAccount.id === CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + ); + + expect(account).toBeDefined(); + expect(account.handle).toBe('jane.austen@apple.dev'); + expect(account).not.toHaveProperty('accessToken'); + expect(account).not.toHaveProperty('refreshToken'); + expect(account).not.toHaveProperty('connectionParameters'); + expect(account).not.toHaveProperty('oidcTokenClaims'); + }); + + it('should reject requesting hidden fields via GraphQL', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyConnectedAccounts { + myConnectedAccounts { + id + accessToken + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeDefined(); + expect(response.body.errors.length).toBeGreaterThan(0); + }); + }); + + describe('connectedAccounts (admin listing)', () => { + it('should return all workspace accounts for admin', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query ConnectedAccounts { + connectedAccounts { + id + handle + provider + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const accounts = response.body.data.connectedAccounts; + const accountIds = accounts.map( + (account: { id: string }) => account.id, + ); + + expect(accountIds).toContain(CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE); + expect(accountIds).toContain(CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY); + }); + + it('should also be accessible for member role (tool permission)', async () => { + const response = await makeMetadataAPIRequestWithMemberRole({ + query: gql` + query ConnectedAccounts { + connectedAccounts { + id + handle + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.connectedAccounts).toBeDefined(); + }); + }); + + describe('deleteConnectedAccount', () => { + it('should allow deleting own account', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation DeleteConnectedAccount($id: UUID!) { + deleteConnectedAccount(id: $id) { + id + } + } + `, + variables: { id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.deleteConnectedAccount.id).toBe( + CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE, + ); + }); + + it('should deny deleting another user account', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation DeleteConnectedAccount($id: UUID!) { + deleteConnectedAccount(id: $id) { + id + } + } + `, + variables: { id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/message-channel/message-channel-resolver.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/message-channel/message-channel-resolver.integration-spec.ts new file mode 100644 index 0000000000..92f51ee5ff --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/message-channel/message-channel-resolver.integration-spec.ts @@ -0,0 +1,163 @@ +import { gql } from 'graphql-tag'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { FeatureFlagKey } from 'twenty-shared/types'; + +import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; +import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant'; + +describe('messageChannelResolver (e2e)', () => { + beforeAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: true, + expectToFail: false, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: false, + expectToFail: false, + }); + }); + + describe('myMessageChannels', () => { + it('should return only the current user message channels', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageChannels { + myMessageChannels { + id + handle + visibility + syncStatus + syncStage + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const channels = response.body.data.myMessageChannels; + const channelIds = channels.map( + (channel: { id: string }) => channel.id, + ); + + expect(channelIds).toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JANE); + expect(channelIds).not.toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JONY); + }); + + it('should filter by connectedAccountId', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageChannels($connectedAccountId: UUID) { + myMessageChannels(connectedAccountId: $connectedAccountId) { + id + handle + } + } + `, + variables: { + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const channels = response.body.data.myMessageChannels; + const channelIds = channels.map( + (channel: { id: string }) => channel.id, + ); + + expect(channelIds).toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JANE); + expect(channelIds).not.toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JONY); + }); + + it('should deny filtering by another user connectedAccountId', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageChannels($connectedAccountId: UUID) { + myMessageChannels(connectedAccountId: $connectedAccountId) { + id + } + } + `, + variables: { + connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + + it('should not return syncCursor', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageChannels { + myMessageChannels { + id + syncCursor + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeDefined(); + }); + }); + + describe('updateMessageChannel', () => { + it('should allow updating own channel settings', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) { + updateMessageChannel(input: $input) { + id + visibility + } + } + `, + variables: { + input: { + id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + update: { visibility: 'METADATA' }, + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.updateMessageChannel.visibility).toBe( + 'METADATA', + ); + }); + + it('should deny updating another user channel', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) { + updateMessageChannel(input: $input) { + id + } + } + `, + variables: { + input: { + id: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY, + update: { visibility: 'METADATA' }, + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/message-folder/message-folder-resolver.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/message-folder/message-folder-resolver.integration-spec.ts new file mode 100644 index 0000000000..64b80be752 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/message-folder/message-folder-resolver.integration-spec.ts @@ -0,0 +1,161 @@ +import { gql } from 'graphql-tag'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { FeatureFlagKey } from 'twenty-shared/types'; + +import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant'; +import { MESSAGE_FOLDER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant'; + +describe('messageFolderResolver (e2e)', () => { + beforeAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: true, + expectToFail: false, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED, + value: false, + expectToFail: false, + }); + }); + + describe('myMessageFolders', () => { + it('should return only the current user message folders', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageFolders { + myMessageFolders { + id + name + isSynced + messageChannelId + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const folders = response.body.data.myMessageFolders; + const folderIds = folders.map((folder: { id: string }) => folder.id); + + expect(folderIds).toContain(MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX); + expect(folderIds).toContain(MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT); + expect(folderIds).not.toContain( + MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX, + ); + }); + + it('should filter by messageChannelId', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageFolders($messageChannelId: UUID) { + myMessageFolders(messageChannelId: $messageChannelId) { + id + name + } + } + `, + variables: { + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + + const folders = response.body.data.myMessageFolders; + const folderIds = folders.map((folder: { id: string }) => folder.id); + + expect(folderIds).toContain(MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX); + expect(folderIds).not.toContain( + MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX, + ); + }); + + it('should deny filtering by another user messageChannelId', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageFolders($messageChannelId: UUID) { + myMessageFolders(messageChannelId: $messageChannelId) { + id + } + } + `, + variables: { + messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + + it('should not expose hidden fields', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + query MyMessageFolders { + myMessageFolders { + id + syncCursor + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeDefined(); + }); + }); + + describe('updateMessageFolder', () => { + it('should allow updating own folder isSynced', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) { + updateMessageFolder(input: $input) { + id + isSynced + } + } + `, + variables: { + input: { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX, + update: { isSynced: false }, + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.updateMessageFolder.isSynced).toBe(false); + }); + + it('should deny updating another user folder', async () => { + const response = await makeMetadataAPIRequest({ + query: gql` + mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) { + updateMessageFolder(input: $input) { + id + } + } + `, + variables: { + input: { + id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX, + update: { isSynced: false }, + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN'); + }); + }); +});