Message channel change 1 (#14942)

This commit is contained in:
neo773
2025-10-11 21:48:44 +05:30
committed by GitHub
parent 560e24fa4b
commit 6f49fd1911
33 changed files with 1124 additions and 90 deletions
@@ -483,6 +483,11 @@ export enum CaptchaDriverType {
TURNSTILE = 'TURNSTILE'
}
export type ChannelSyncSuccess = {
__typename?: 'ChannelSyncSuccess';
success: Scalars['Boolean'];
};
export type CheckUserExistOutput = {
__typename?: 'CheckUserExistOutput';
availableWorkspacesCount: Scalars['Float'];
@@ -1483,6 +1488,7 @@ export type ImapSmtpCaldavConnectionParameters = {
export type ImapSmtpCaldavConnectionSuccess = {
__typename?: 'ImapSmtpCaldavConnectionSuccess';
connectedAccountId: Scalars['String'];
success: Scalars['Boolean'];
};
@@ -1787,6 +1793,7 @@ export type Mutation = {
signUpInWorkspace: SignUpOutput;
skipBookOnboardingStep: OnboardingStepSuccess;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
startChannelSync: ChannelSyncSuccess;
submitFormStep: Scalars['Boolean'];
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
@@ -2454,6 +2461,11 @@ export type MutationSignUpInWorkspaceArgs = {
};
export type MutationStartChannelSyncArgs = {
connectedAccountId: Scalars['UUID'];
};
export type MutationSubmitFormStepArgs = {
input: SubmitFormStepInput;
};
@@ -5156,7 +5168,14 @@ export type SaveImapSmtpCaldavAccountMutationVariables = Exact<{
}>;
export type SaveImapSmtpCaldavAccountMutation = { __typename?: 'Mutation', saveImapSmtpCaldavAccount: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean } };
export type SaveImapSmtpCaldavAccountMutation = { __typename?: 'Mutation', saveImapSmtpCaldavAccount: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean, connectedAccountId: string } };
export type StartChannelSyncMutationVariables = Exact<{
connectedAccountId: Scalars['UUID'];
}>;
export type StartChannelSyncMutation = { __typename?: 'Mutation', startChannelSync: { __typename?: 'ChannelSyncSuccess', success: boolean } };
export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
id: Scalars['UUID'];
@@ -9442,6 +9461,7 @@ export const SaveImapSmtpCaldavAccountDocument = gql`
id: $id
) {
success
connectedAccountId
}
}
`;
@@ -9474,6 +9494,39 @@ export function useSaveImapSmtpCaldavAccountMutation(baseOptions?: Apollo.Mutati
export type SaveImapSmtpCaldavAccountMutationHookResult = ReturnType<typeof useSaveImapSmtpCaldavAccountMutation>;
export type SaveImapSmtpCaldavAccountMutationResult = Apollo.MutationResult<SaveImapSmtpCaldavAccountMutation>;
export type SaveImapSmtpCaldavAccountMutationOptions = Apollo.BaseMutationOptions<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>;
export const StartChannelSyncDocument = gql`
mutation StartChannelSync($connectedAccountId: UUID!) {
startChannelSync(connectedAccountId: $connectedAccountId) {
success
}
}
`;
export type StartChannelSyncMutationFn = Apollo.MutationFunction<StartChannelSyncMutation, StartChannelSyncMutationVariables>;
/**
* __useStartChannelSyncMutation__
*
* To run a mutation, you first call `useStartChannelSyncMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useStartChannelSyncMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [startChannelSyncMutation, { data, loading, error }] = useStartChannelSyncMutation({
* variables: {
* connectedAccountId: // value for 'connectedAccountId'
* },
* });
*/
export function useStartChannelSyncMutation(baseOptions?: Apollo.MutationHookOptions<StartChannelSyncMutation, StartChannelSyncMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<StartChannelSyncMutation, StartChannelSyncMutationVariables>(StartChannelSyncDocument, options);
}
export type StartChannelSyncMutationHookResult = ReturnType<typeof useStartChannelSyncMutation>;
export type StartChannelSyncMutationResult = Apollo.MutationResult<StartChannelSyncMutation>;
export type StartChannelSyncMutationOptions = Apollo.BaseMutationOptions<StartChannelSyncMutation, StartChannelSyncMutationVariables>;
export const GetConnectedImapSmtpCaldavAccountDocument = gql`
query GetConnectedImapSmtpCaldavAccount($id: UUID!) {
getConnectedImapSmtpCaldavAccount(id: $id) {
@@ -483,6 +483,11 @@ export enum CaptchaDriverType {
TURNSTILE = 'TURNSTILE'
}
export type ChannelSyncSuccess = {
__typename?: 'ChannelSyncSuccess';
success: Scalars['Boolean'];
};
export type CheckUserExistOutput = {
__typename?: 'CheckUserExistOutput';
availableWorkspacesCount: Scalars['Float'];
@@ -1440,6 +1445,7 @@ export type ImapSmtpCaldavConnectionParameters = {
export type ImapSmtpCaldavConnectionSuccess = {
__typename?: 'ImapSmtpCaldavConnectionSuccess';
connectedAccountId: Scalars['String'];
success: Scalars['Boolean'];
};
@@ -1742,6 +1748,7 @@ export type Mutation = {
signUpInWorkspace: SignUpOutput;
skipBookOnboardingStep: OnboardingStepSuccess;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
startChannelSync: ChannelSyncSuccess;
submitFormStep: Scalars['Boolean'];
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
@@ -2385,6 +2392,11 @@ export type MutationSignUpInWorkspaceArgs = {
};
export type MutationStartChannelSyncArgs = {
connectedAccountId: Scalars['UUID'];
};
export type MutationSubmitFormStepArgs = {
input: SubmitFormStepInput;
};
@@ -9,6 +9,7 @@ export enum CalendarChannelSyncStatus {
}
export enum CalendarChannelSyncStage {
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION',
FULL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', // WILL BE DEPRECATED
PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING', // DEPRECATED
CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING',
@@ -23,6 +23,7 @@ export enum MessageChannelSyncStatus {
}
export enum MessageChannelSyncStage {
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION',
FULL_MESSAGE_LIST_FETCH_PENDING = 'FULL_MESSAGE_LIST_FETCH_PENDING', // WILL BE DEPRECATED
PARTIAL_MESSAGE_LIST_FETCH_PENDING = 'PARTIAL_MESSAGE_LIST_FETCH_PENDING', // DEPRECATED
MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING',
@@ -37,6 +37,14 @@ const SettingsAccountsEmails = lazy(() =>
})),
);
const SettingsAccountsConfiguration = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsConfiguration').then(
(module) => ({
default: module.SettingsAccountsConfiguration,
}),
),
);
const SettingsNewAccount = lazy(() =>
import('~/pages/settings/accounts/SettingsNewAccount').then((module) => ({
default: module.SettingsNewAccount,
@@ -405,6 +413,10 @@ export const SettingsRoutes = ({
<Route path={SettingsPath.Experience} element={<SettingsExperience />} />
<Route path={SettingsPath.Accounts} element={<SettingsAccounts />} />
<Route path={SettingsPath.NewAccount} element={<SettingsNewAccount />} />
<Route
path={SettingsPath.AccountsConfiguration}
element={<SettingsAccountsConfiguration />}
/>
<Route
path={SettingsPath.AccountsCalendars}
element={<SettingsAccountsCalendars />}
@@ -1,7 +1,10 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
import {
type CalendarChannel,
CalendarChannelSyncStage,
} from '@/accounts/types/CalendarChannel';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -44,6 +47,9 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
connectedAccountId: {
in: accounts.map((account) => account.id),
},
syncStage: {
neq: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
},
skip: !accounts.length,
});
@@ -2,7 +2,10 @@ import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import {
type MessageChannel,
MessageChannelSyncStage,
} from '@/accounts/types/MessageChannel';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
@@ -58,6 +61,9 @@ export const SettingsAccountsMessageChannelsContainer = () => {
isSyncEnabled: {
eq: true,
},
syncStage: {
neq: MessageChannelSyncStage.PENDING_CONFIGURATION,
},
},
recordGqlFields,
onCompleted: (data) => {
@@ -14,6 +14,7 @@ export const SAVE_IMAP_SMTP_CALDAV_ACCOUNT = gql`
id: $id
) {
success
connectedAccountId
}
}
`;
@@ -0,0 +1,9 @@
import gql from 'graphql-tag';
export const START_CHANNEL_SYNC = gql`
mutation StartChannelSync($connectedAccountId: UUID!) {
startChannelSync(connectedAccountId: $connectedAccountId) {
success
}
}
`;
@@ -132,7 +132,7 @@ export const useImapSmtpCaldavConnectionForm = ({
});
try {
await saveConnection({
const { data } = await saveConnection({
variables: {
...(isEditing && connectedAccountId
? { id: connectedAccountId }
@@ -142,13 +142,20 @@ export const useImapSmtpCaldavConnectionForm = ({
connectionParameters,
},
});
if (!isDefined(data)) return;
const successMessage = isEditing
? t`Connection successfully updated`
: t`Connection successfully created`;
enqueueSuccessSnackBar({ message: successMessage });
navigate(SettingsPath.Accounts);
const { connectedAccountId: returnedConnectedAccountId } =
data?.saveImapSmtpCaldavAccount || {};
navigate(SettingsPath.AccountsConfiguration, {
connectedAccountId: returnedConnectedAccountId,
});
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
@@ -1,4 +1,5 @@
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { MessageChannelSyncStage } from '@/accounts/types/MessageChannel';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
@@ -26,14 +27,25 @@ export const SettingsAccounts = () => {
shouldOnlyLoadRelationIdentifiers: false,
});
const { records: accounts, loading } = useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
const { records: allAccounts, loading } =
useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
},
recordGqlFields,
recordGqlFields,
});
const accountsToShow = allAccounts.filter((account) => {
return (
account.messageChannels.length === 0 ||
account.messageChannels.some(
(channel) =>
channel.syncStage !== MessageChannelSyncStage.PENDING_CONFIGURATION,
)
);
});
return (
@@ -57,7 +69,9 @@ export const SettingsAccounts = () => {
title={t`Connected accounts`}
description={t`Manage your internet accounts.`}
/>
<SettingsAccountsConnectedAccountsListCard accounts={accounts} />
<SettingsAccountsConnectedAccountsListCard
accounts={accountsToShow}
/>
</Section>
<SettingsAccountsBlocklistSection />
<SettingsAccountsSettingsSection />
@@ -0,0 +1,111 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useStartChannelSyncMutation } from '~/generated-metadata/graphql';
import { SettingsAccountsConfigurationStepCalendar } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar';
import { SettingsAccountsConfigurationStepEmail } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepEmail';
enum SettingsAccountsConfigurationStep {
Email = 'email',
Calendar = 'calendar',
}
export const SettingsAccountsConfiguration = () => {
const { t } = useLingui();
const { connectedAccountId } = useParams<{
connectedAccountId: string;
}>();
const navigate = useNavigate();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [startChannelSyncMutation, { loading: isSubmitting }] =
useStartChannelSyncMutation();
const [currentStep, setCurrentStep] =
useState<SettingsAccountsConfigurationStep>(
SettingsAccountsConfigurationStep.Email,
);
const { records: messageChannels } = useFindManyRecords<MessageChannel>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
filter: {
connectedAccountId: {
eq: connectedAccountId,
},
},
skip: !connectedAccountId,
});
const { records: calendarChannels } = useFindManyRecords<CalendarChannel>({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
filter: {
connectedAccountId: {
eq: connectedAccountId,
},
},
skip: !connectedAccountId,
});
const messageChannel = messageChannels[0];
const calendarChannel = calendarChannels[0];
const handleNext = () => {
setCurrentStep(SettingsAccountsConfigurationStep.Calendar);
};
const handleAddAccount = async () => {
if (!connectedAccountId) return;
await startChannelSyncMutation({
variables: {
connectedAccountId,
},
onCompleted: () => {
enqueueSuccessSnackBar({
message: t`Account added successfully. Sync started.`,
});
navigate(getSettingsPath(SettingsPath.Accounts));
},
onError: (error) => {
enqueueErrorSnackBar({
apolloError: error,
});
},
});
};
switch (currentStep) {
case SettingsAccountsConfigurationStep.Email:
if (!isDefined(messageChannel)) {
return null;
}
return (
<SettingsAccountsConfigurationStepEmail
messageChannel={messageChannel}
hasNextStep={isDefined(calendarChannel)}
isSubmitting={isSubmitting}
onNext={handleNext}
onAddAccount={handleAddAccount}
/>
);
case SettingsAccountsConfigurationStep.Calendar:
if (!isDefined(calendarChannel)) {
return null;
}
return (
<SettingsAccountsConfigurationStepCalendar
calendarChannel={calendarChannel}
messageChannel={messageChannel}
isSubmitting={isSubmitting}
onAddAccount={handleAddAccount}
/>
);
}
};
@@ -0,0 +1,66 @@
import { Trans, useLingui } from '@lingui/react/macro';
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
type SettingsAccountsConfigurationStepCalendarProps = {
calendarChannel: CalendarChannel;
messageChannel?: MessageChannel;
isSubmitting: boolean;
onAddAccount: () => void;
};
export const SettingsAccountsConfigurationStepCalendar = ({
calendarChannel,
messageChannel,
isSubmitting,
onAddAccount,
}: SettingsAccountsConfigurationStepCalendarProps) => {
const { t } = useLingui();
const stepNumber = isDefined(messageChannel) ? 2 : 1;
const stepTitle = t`${stepNumber}. Calendar`;
return (
<SubMenuTopBarContainer
title={stepTitle}
links={[
{
children: <Trans>User</Trans>,
href: getSettingsPath(SettingsPath.ProfilePage),
},
{
children: <Trans>Account</Trans>,
href: getSettingsPath(SettingsPath.Accounts),
},
{
children: stepTitle,
},
]}
actionButton={
<Button
Icon={IconPlus}
title={t`Add account`}
accent="blue"
size="small"
variant="primary"
onClick={onAddAccount}
disabled={isSubmitting}
/>
}
>
<SettingsPageContainer>
<SettingsAccountsCalendarChannelDetails
calendarChannel={calendarChannel}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,76 @@
import { Trans, useLingui } from '@lingui/react/macro';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconChevronRight, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
type SettingsAccountsConfigurationStepEmailProps = {
messageChannel: MessageChannel;
hasNextStep: boolean;
isSubmitting: boolean;
onNext: () => void;
onAddAccount: () => void;
};
export const SettingsAccountsConfigurationStepEmail = ({
messageChannel,
hasNextStep,
isSubmitting,
onNext,
onAddAccount,
}: SettingsAccountsConfigurationStepEmailProps) => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`1. Email`}
links={[
{
children: <Trans>User</Trans>,
href: getSettingsPath(SettingsPath.ProfilePage),
},
{
children: <Trans>Account</Trans>,
href: getSettingsPath(SettingsPath.Accounts),
},
{
children: t`1. Email`,
},
]}
actionButton={
hasNextStep ? (
<Button
Icon={IconChevronRight}
title={t`Next`}
accent="blue"
size="small"
variant="secondary"
onClick={onNext}
disabled={isSubmitting}
/>
) : (
<Button
Icon={IconPlus}
title={t`Add account`}
accent="blue"
size="small"
variant="primary"
onClick={onAddAccount}
disabled={isSubmitting}
/>
)
}
>
<SettingsPageContainer>
<SettingsAccountsMessageChannelDetails
messageChannel={messageChannel}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,423 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
CALENDAR_CHANNEL_STANDARD_FIELD_IDS,
MESSAGE_CHANNEL_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@Command({
name: 'upgrade:1-10:migrate-channel-sync-stages',
description:
'Migrate message and calendar channel sync stages: add PENDING_CONFIGURATION, add MESSAGE_LIST_FETCH_SCHEDULED, migrate deprecated stages',
})
export class MigrateChannelSyncStagesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Migrating channel sync stages for workspace ${workspaceId}`,
);
await this.updateMessageChannelSyncStageFieldMetadata(workspaceId, options);
await this.updateCalendarChannelSyncStageFieldMetadata(
workspaceId,
options,
);
const schemaName = getWorkspaceSchemaName(workspaceId);
await this.migrateMessageChannelSyncStages(
workspaceId,
schemaName,
options,
);
await this.migrateCalendarChannelSyncStages(
workspaceId,
schemaName,
options,
);
this.logger.log(
`Successfully migrated channel sync stages for workspace ${workspaceId}`,
);
}
private async migrateMessageChannelSyncStages(
workspaceId: string,
schemaName: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
const messageChannelObject = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.messageChannel,
workspaceId,
},
});
if (!messageChannelObject) {
this.logger.log(
`MessageChannel object not found for workspace ${workspaceId}, skipping schema migration`,
);
return;
}
const tableName = computeObjectTargetTable(messageChannelObject);
// Add new enum values for MessageChannelSyncStage
if (options.dryRun) {
this.logger.log(
`Would try to add PENDING_CONFIGURATION to messageChannel_syncStage_enum for workspace ${workspaceId}`,
);
} else {
try {
await this.coreDataSource.query(
`ALTER TYPE ${schemaName}."messageChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'PENDING_CONFIGURATION'`,
);
this.logger.log(
`Added PENDING_CONFIGURATION to messageChannel_syncStage_enum for workspace ${workspaceId}`,
);
} catch (error) {
this.logger.error(
`Error adding PENDING_CONFIGURATION to messageChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
);
}
}
if (options.dryRun) {
this.logger.log(
`Would try to add MESSAGE_LIST_FETCH_SCHEDULED to messageChannel_syncStage_enum for workspace ${workspaceId}`,
);
} else {
try {
await this.coreDataSource.query(
`ALTER TYPE ${schemaName}."messageChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'MESSAGE_LIST_FETCH_SCHEDULED'`,
);
this.logger.log(
`Added MESSAGE_LIST_FETCH_SCHEDULED to messageChannel_syncStage_enum for workspace ${workspaceId}`,
);
} catch (error) {
this.logger.error(
`Error adding MESSAGE_LIST_FETCH_SCHEDULED to messageChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
);
}
}
// Migrate deprecated MessageChannel sync stages
if (options.dryRun) {
this.logger.log(
`Would migrate deprecated messageChannel sync stages for workspace ${workspaceId}`,
);
} else {
const messageChannelUpdateResult = await this.coreDataSource.query(
`UPDATE "${schemaName}"."${tableName}"
SET "syncStage" = 'MESSAGE_LIST_FETCH_PENDING'
WHERE "syncStage" IN ('FULL_MESSAGE_LIST_FETCH_PENDING', 'PARTIAL_MESSAGE_LIST_FETCH_PENDING')`,
);
const messageChannelRowsUpdated = messageChannelUpdateResult[1] || 0;
this.logger.log(
`Migrated ${messageChannelRowsUpdated} messageChannel records from deprecated sync stages in workspace ${workspaceId}`,
);
}
}
private async migrateCalendarChannelSyncStages(
workspaceId: string,
schemaName: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
const calendarChannelObject = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.calendarChannel,
workspaceId,
},
});
if (!calendarChannelObject) {
this.logger.log(
`CalendarChannel object not found for workspace ${workspaceId}, skipping schema migration`,
);
return;
}
const tableName = computeObjectTargetTable(calendarChannelObject);
// Add new enum values for CalendarChannelSyncStage
if (options.dryRun) {
this.logger.log(
`Would try to add PENDING_CONFIGURATION to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
);
} else {
try {
await this.coreDataSource.query(
`ALTER TYPE ${schemaName}."calendarChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'PENDING_CONFIGURATION'`,
);
this.logger.log(
`Added PENDING_CONFIGURATION to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
);
} catch (error) {
this.logger.error(
`Error adding PENDING_CONFIGURATION to calendarChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
);
}
}
if (options.dryRun) {
this.logger.log(
`Would try to add CALENDAR_EVENT_LIST_FETCH_SCHEDULED to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
);
} else {
try {
await this.coreDataSource.query(
`ALTER TYPE ${schemaName}."calendarChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED'`,
);
this.logger.log(
`Added CALENDAR_EVENT_LIST_FETCH_SCHEDULED to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
);
} catch (error) {
this.logger.error(
`Error adding CALENDAR_EVENT_LIST_FETCH_SCHEDULED to calendarChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
);
}
}
// Migrate deprecated CalendarChannel sync stages
if (options.dryRun) {
this.logger.log(
`Would migrate deprecated calendarChannel sync stages for workspace ${workspaceId}`,
);
} else {
const calendarChannelUpdateResult = await this.coreDataSource.query(
`UPDATE "${schemaName}"."${tableName}"
SET "syncStage" = 'CALENDAR_EVENT_LIST_FETCH_PENDING'
WHERE "syncStage" IN ('FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING')`,
);
const calendarChannelRowsUpdated = calendarChannelUpdateResult[1] || 0;
this.logger.log(
`Migrated ${calendarChannelRowsUpdated} calendarChannel records from deprecated sync stages in workspace ${workspaceId}`,
);
}
}
private async updateMessageChannelSyncStageFieldMetadata(
workspaceId: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
const messageChannelObject = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.messageChannel,
workspaceId,
},
});
if (!messageChannelObject) {
this.logger.log(
`MessageChannel object not found for workspace ${workspaceId}, skipping field metadata update`,
);
return;
}
const syncStageField = await this.fieldMetadataRepository.findOne({
where: {
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStage,
objectMetadataId: messageChannelObject.id,
workspaceId,
},
});
if (!syncStageField) {
this.logger.log(
`MessageChannel syncStage field not found for workspace ${workspaceId}, skipping field metadata update`,
);
return;
}
const fieldOptions = (syncStageField.options ||
[]) as FieldMetadataComplexOption[];
const hasMessageListFetchScheduled = fieldOptions.some(
(option) =>
option.value === MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
);
const hasPendingConfiguration = fieldOptions.some(
(option) =>
option.value === MessageChannelSyncStage.PENDING_CONFIGURATION,
);
if (hasMessageListFetchScheduled && hasPendingConfiguration) {
this.logger.log(
`MessageChannel syncStage field metadata already migrated for workspace ${workspaceId}`,
);
return;
}
if (options.dryRun) {
this.logger.log(
`Would update MessageChannel syncStage field metadata for workspace ${workspaceId}`,
);
return;
}
if (!hasMessageListFetchScheduled) {
fieldOptions.push({
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
label: 'Messages list fetch scheduled',
position: 1,
color: 'green',
});
}
if (!hasPendingConfiguration) {
fieldOptions.push({
value: MessageChannelSyncStage.PENDING_CONFIGURATION,
label: 'Pending configuration',
position: 9,
color: 'gray',
});
}
syncStageField.options = fieldOptions;
await this.fieldMetadataRepository.save(syncStageField);
this.logger.log(
`Updated MessageChannel syncStage field metadata for workspace ${workspaceId}`,
);
}
private async updateCalendarChannelSyncStageFieldMetadata(
workspaceId: string,
options: RunOnWorkspaceArgs['options'],
): Promise<void> {
const calendarChannelObject = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.calendarChannel,
workspaceId,
},
});
if (!calendarChannelObject) {
this.logger.log(
`CalendarChannel object not found for workspace ${workspaceId}, skipping field metadata update`,
);
return;
}
const syncStageField = await this.fieldMetadataRepository.findOne({
where: {
standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStage,
objectMetadataId: calendarChannelObject.id,
workspaceId,
},
});
if (!syncStageField) {
this.logger.log(
`CalendarChannel syncStage field not found for workspace ${workspaceId}, skipping field metadata update`,
);
return;
}
const fieldOptions = (syncStageField.options ||
[]) as FieldMetadataComplexOption[];
const hasCalendarEventListFetchScheduled = fieldOptions.some(
(option) =>
option.value ===
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
);
const hasPendingConfiguration = fieldOptions.some(
(option) =>
option.value === CalendarChannelSyncStage.PENDING_CONFIGURATION,
);
if (hasCalendarEventListFetchScheduled && hasPendingConfiguration) {
this.logger.log(
`CalendarChannel syncStage field metadata already migrated for workspace ${workspaceId}`,
);
return;
}
if (options.dryRun) {
this.logger.log(
`Would update CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
);
return;
}
if (!hasCalendarEventListFetchScheduled) {
fieldOptions.push({
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
label: 'Calendar event list fetch scheduled',
position: 1,
color: 'green',
});
}
if (!hasPendingConfiguration) {
fieldOptions.push({
value: CalendarChannelSyncStage.PENDING_CONFIGURATION,
label: 'Pending configuration',
position: 9,
color: 'gray',
});
}
syncStageField.options = fieldOptions;
await this.fieldMetadataRepository.save(syncStageField);
this.logger.log(
`Updated CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
);
}
}
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-deduplicate-unique-fields.command';
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-sync-stages.command';
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-workflow-step-filter-operand-value';
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-person-search-vector-with-phones.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -16,6 +18,7 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
imports: [
TypeOrmModule.forFeature([
Workspace,
FieldMetadataEntity,
ObjectMetadataEntity,
IndexMetadataEntity,
]),
@@ -26,10 +29,12 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
],
providers: [
MigrateWorkflowStepFilterOperandValueCommand,
MigrateChannelSyncStagesCommand,
DeduplicateUniqueFieldsCommand,
RegeneratePersonSearchVectorWithPhonesCommand,
],
exports: [
MigrateChannelSyncStagesCommand,
MigrateWorkflowStepFilterOperandValueCommand,
DeduplicateUniqueFieldsCommand,
RegeneratePersonSearchVectorWithPhonesCommand,
@@ -88,15 +88,16 @@ export class GoogleAPIsAuthController {
const handle = emails[0].value;
await this.googleAPIsService.refreshGoogleRefreshToken({
handle,
workspaceMemberId: workspaceMemberId,
workspaceId: workspaceId,
accessToken,
refreshToken,
calendarVisibility,
messageVisibility,
});
const connectedAccountId =
await this.googleAPIsService.refreshGoogleRefreshToken({
handle,
workspaceMemberId: workspaceMemberId,
workspaceId: workspaceId,
accessToken,
refreshToken,
calendarVisibility,
messageVisibility,
});
if (userId) {
await this.onboardingService.setOnboardingConnectAccountPending({
@@ -113,15 +114,18 @@ export class GoogleAPIsAuthController {
);
}
return res.redirect(
this.domainManagerService
.buildWorkspaceURL({
workspace,
pathname:
redirectLocation || getSettingsPath(SettingsPath.Accounts),
})
.toString(),
);
const pathname =
redirectLocation ||
getSettingsPath(SettingsPath.AccountsConfiguration, {
connectedAccountId,
});
const url = this.domainManagerService.buildWorkspaceURL({
workspace,
pathname,
});
return res.redirect(url.toString());
} catch (error) {
return res.redirect(
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
@@ -95,15 +95,16 @@ export class MicrosoftAPIsAuthController {
const handle = emails[0].value;
await this.microsoftAPIsService.refreshMicrosoftRefreshToken({
handle,
workspaceMemberId: workspaceMemberId,
workspaceId: workspaceId,
accessToken,
refreshToken,
calendarVisibility,
messageVisibility,
});
const connectedAccountId =
await this.microsoftAPIsService.refreshMicrosoftRefreshToken({
handle,
workspaceMemberId: workspaceMemberId,
workspaceId: workspaceId,
accessToken,
refreshToken,
calendarVisibility,
messageVisibility,
});
if (userId) {
await this.onboardingService.setOnboardingConnectAccountPending({
@@ -120,15 +121,18 @@ export class MicrosoftAPIsAuthController {
);
}
return res.redirect(
this.domainManagerService
.buildWorkspaceURL({
workspace,
pathname:
redirectLocation || getSettingsPath(SettingsPath.Accounts),
})
.toString(),
);
const pathname =
redirectLocation ||
getSettingsPath(SettingsPath.AccountsConfiguration, {
connectedAccountId,
});
const url = this.domainManagerService.buildWorkspaceURL({
workspace,
pathname,
});
return res.redirect(url.toString());
} catch (error) {
return res.redirect(
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
@@ -5,6 +5,8 @@ import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
@@ -47,6 +49,8 @@ export class CreateCalendarChannelService {
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
syncStatus: CalendarChannelSyncStatus.NOT_SYNCED,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
{},
manager,
@@ -5,6 +5,7 @@ import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
MessageChannelVisibility,
@@ -50,7 +51,8 @@ export class CreateMessageChannelService {
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.ONGOING,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
},
{},
manager,
@@ -25,12 +25,14 @@ import {
type CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelSyncStage,
type CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelSyncStage,
type MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@@ -67,7 +69,7 @@ export class GoogleAPIsService {
refreshToken: string;
calendarVisibility: CalendarChannelVisibility | undefined;
messageVisibility: MessageChannelVisibility | undefined;
}) {
}): Promise<string> {
const {
handle,
workspaceId,
@@ -210,13 +212,18 @@ export class GoogleAPIsService {
});
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
}
@@ -228,14 +235,21 @@ export class GoogleAPIsService {
});
for (const calendarChannel of calendarChannels) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
}
}
@@ -22,12 +22,14 @@ import {
type CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelSyncStage,
type CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelSyncStage,
type MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@@ -64,7 +66,7 @@ export class MicrosoftAPIsService {
refreshToken: string;
calendarVisibility: CalendarChannelVisibility | undefined;
messageVisibility: MessageChannelVisibility | undefined;
}) {
}): Promise<string> {
const {
handle,
workspaceId,
@@ -201,13 +203,18 @@ export class MicrosoftAPIsService {
});
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
}
@@ -219,14 +226,21 @@ export class MicrosoftAPIsService {
});
for (const calendarChannel of calendarChannels) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
}
}
@@ -57,6 +57,7 @@ import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.mod
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { ChannelSyncModule } from 'src/modules/connected-account/channel-sync/channel-sync.module';
import { AuditModule } from './audit/audit.module';
import { ClientConfigModule } from './client-config/client-config.module';
@@ -100,6 +101,7 @@ import { FileModule } from './file/file.module';
GeoMapModule,
SubscriptionsModule,
ImapSmtpCaldavModule,
ChannelSyncModule,
FileStorageModule.forRoot(),
LoggerModule.forRootAsync({
useFactory: loggerModuleFactory,
@@ -4,4 +4,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
export class ImapSmtpCaldavConnectionSuccess {
@Field(() => Boolean)
success: boolean;
@Field(() => String)
connectedAccountId: string;
}
@@ -103,16 +103,18 @@ export class ImapSmtpCaldavResolver {
handle,
);
await this.imapSmtpCaldavApisService.processAccount({
handle,
workspaceMemberId: accountOwnerId,
workspaceId: workspace.id,
connectionParameters: validatedParams,
connectedAccountId: id,
});
const connectedAccountId =
await this.imapSmtpCaldavApisService.processAccount({
handle,
workspaceMemberId: accountOwnerId,
workspaceId: workspace.id,
connectionParameters: validatedParams,
connectedAccountId: id,
});
return {
success: true,
connectedAccountId,
};
}
@@ -35,6 +35,7 @@ export enum CalendarChannelSyncStatus {
}
export enum CalendarChannelSyncStage {
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION',
FULL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', // WILL BE DEPRECATED
PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING', // DEPRECATED
CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING',
@@ -194,8 +195,14 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
position: 8,
color: 'blue',
},
{
value: CalendarChannelSyncStage.PENDING_CONFIGURATION,
label: 'Pending configuration',
position: 9,
color: 'gray',
},
],
defaultValue: `'${CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING}'`,
defaultValue: `'${CalendarChannelSyncStage.PENDING_CONFIGURATION}'`,
})
syncStage: CalendarChannelSyncStage;
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { ChannelSyncResolver } from 'src/modules/connected-account/channel-sync/channel-sync.resolver';
import { ChannelSyncService } from 'src/modules/connected-account/channel-sync/services/channel-sync.service';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [ChannelSyncResolver, ChannelSyncService],
exports: [ChannelSyncService],
})
export class ChannelSyncModule {}
@@ -0,0 +1,33 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ChannelSyncSuccess } from 'src/modules/connected-account/channel-sync/dtos/channel-sync-success.dto';
import { ChannelSyncService } from 'src/modules/connected-account/channel-sync/services/channel-sync.service';
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(AuthGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ChannelSyncResolver {
constructor(private readonly channelSyncService: ChannelSyncService) {}
@Mutation(() => ChannelSyncSuccess)
async startChannelSync(
@Args('connectedAccountId', { type: () => UUIDScalarType })
connectedAccountId: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ChannelSyncSuccess> {
await this.channelSyncService.startChannelSync({
connectedAccountId,
workspaceId: workspace.id,
});
return { success: true };
}
}
@@ -0,0 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class ChannelSyncSuccess {
@Field(() => Boolean)
success: boolean;
}
@@ -0,0 +1,113 @@
import { Injectable } from '@nestjs/common';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
type MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
export type StartChannelSyncInput = {
connectedAccountId: string;
workspaceId: string;
};
@Injectable()
export class ChannelSyncService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly calendarQueueService: MessageQueueService,
) {}
async startChannelSync(input: StartChannelSyncInput): Promise<void> {
const { connectedAccountId, workspaceId } = input;
await this.startMessageChannelSync(connectedAccountId, workspaceId);
await this.startCalendarChannelSync(connectedAccountId, workspaceId);
}
private async startMessageChannelSync(
connectedAccountId: string,
workspaceId: string,
): Promise<void> {
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
},
});
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
await messageChannelRepository.update(messageChannel.id, {
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
syncStatus: MessageChannelSyncStatus.ONGOING,
});
}
}
private async startCalendarChannelSync(
connectedAccountId: string,
workspaceId: string,
): Promise<void> {
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
});
for (const calendarChannel of calendarChannels) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
await calendarChannelRepository.update(calendarChannel.id, {
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
syncStatus: CalendarChannelSyncStatus.ONGOING,
});
}
}
}
@@ -46,7 +46,7 @@ export class ImapSmtpCalDavAPIService {
workspaceId: string;
connectionParameters: EmailAccountConnectionParameters;
connectedAccountId?: string;
}) {
}): Promise<string> {
const { handle, workspaceId, workspaceMemberId, connectedAccountId } =
input;
@@ -125,6 +125,8 @@ export class ImapSmtpCalDavAPIService {
messageChannel,
calendarChannel,
);
return accountId;
}
private async upsertConnectedAccount(
@@ -31,6 +31,7 @@ export enum MessageChannelSyncStatus {
}
export enum MessageChannelSyncStage {
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION',
FULL_MESSAGE_LIST_FETCH_PENDING = 'FULL_MESSAGE_LIST_FETCH_PENDING', // WILL BE DEPRECATED
PARTIAL_MESSAGE_LIST_FETCH_PENDING = 'PARTIAL_MESSAGE_LIST_FETCH_PENDING', // DEPRECATED
MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING',
@@ -380,8 +381,14 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
position: 8,
color: 'blue',
},
{
value: MessageChannelSyncStage.PENDING_CONFIGURATION,
label: 'Pending configuration',
position: 9,
color: 'gray',
},
],
defaultValue: `'${MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING}'`,
defaultValue: `'${MessageChannelSyncStage.PENDING_CONFIGURATION}'`,
})
syncStage: MessageChannelSyncStage;
@@ -4,6 +4,7 @@ export enum SettingsPath {
Experience = 'experience',
Accounts = 'accounts',
NewAccount = 'accounts/new',
AccountsConfiguration = 'accounts/configuration/:connectedAccountId',
AccountsCalendars = 'accounts/calendars',
AccountsEmails = 'accounts/emails',
NewImapSmtpCaldavConnection = 'accounts/new-imap-smtp-caldav-connection',