Compare commits

...

8 Commits

Author SHA1 Message Date
Charles Bochet 8d43ed4515 Fix message import scheduled 2025-11-25 19:24:00 +01:00
Baptiste Devessier 3d1d3d9f04 [Side Panel V2] Bring back old container (#16065)
## Before

<img width="1446" height="780" alt="image"
src="https://github.com/user-attachments/assets/fa25f22d-b979-4a8f-9989-004d07f862d7"
/>

## After

<img width="3456" height="2160" alt="CleanShot 2025-11-25 at 17 35
59@2x"
src="https://github.com/user-attachments/assets/a87880c4-3faa-47c3-9215-34afc36270a3"
/>
2025-11-25 18:16:32 +01:00
Etienne b19dc703cc Security - disable gql introspection for non-auth user (#16047)
closes https://github.com/twentyhq/private-issues/issues/351
closes https://github.com/twentyhq/private-issues/issues/350

Before, introspection query works without token. After, fails.

```

query IntrospectionQuery {
  __schema {
    queryType {
      name
    }
    mutationType {
      name
    }
    subscriptionType {
      name
    }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args {
        ...InputValue
      }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description
  fields(includeDeprecated: true) {
    name
    description
    args {
      ...InputValue
    }
    type {
      ...TypeRef
    }
    isDeprecated
    deprecationReason
  }
  inputFields {
    ...InputValue
  }
  interfaces {
    ...TypeRef
  }
  enumValues(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
  }
  possibleTypes {
    ...TypeRef
  }
}

fragment InputValue on __InputValue {
  name
  description
  type {
    ...TypeRef
  }
  defaultValue
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}
```
2025-11-25 18:16:07 +01:00
neo773 9b668619c5 Update channel sync service to immediately enqueue jobs (#16064) 2025-11-25 18:15:57 +01:00
neo773 e7c83dc04f move folder cron to message-list-fetch (#16062) 2025-11-25 18:15:49 +01:00
Charles Bochet 693fe01fa4 Fix upgrade command messaging (#16067) 2025-11-25 18:15:42 +01:00
Charles Bochet c616713435 Add import scheduled status to messaging sync (#16058)
We have introduced to new syncStage statuses:
`messageChannel.MESSAGES_IMPORT_SCHEDULED` and
`calendarChannel.CALENDAR_EVENTS_IMPORT_SCHEDULED`

We need to make sure all existing workspaces have it
2025-11-25 15:28:48 +01:00
Raphaël Bosi 859c948d01 Fix page layout widget deletion (#16035)
With the new side panel, we are able to delete a widget with the side
panel still open. This caused the app to crash because we threw when the
widget id wasn't defined.

This PR fixes this by closing the side panel in the delete action and by
returning null instead of throwing.

## Before



https://github.com/user-attachments/assets/092bfe62-82dc-4d83-9967-1cc753ecf55e



## After


https://github.com/user-attachments/assets/8bed6cc5-961b-4112-8cf5-e587865d14da
2025-11-25 14:08:39 +01:00
22 changed files with 718 additions and 286 deletions
@@ -3,11 +3,11 @@ import { CommandMenuAIChatThreadsPage } from '@/command-menu/pages/AIChatThreads
import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/CommandMenuAskAIPage';
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings';
import { CommandMenuPageLayoutGraphFilter } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphFilter';
import { CommandMenuPageLayoutGraphTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect';
import { CommandMenuPageLayoutIframeSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings';
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
import { CommandMenuPageLayoutTabSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings';
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
import { CommandMenuMergeRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuMergeRecordPage';
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
import { CommandMenuEditRichTextPage } from '@/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage';
@@ -48,7 +48,7 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
],
[
CommandMenuPages.PageLayoutGraphTypeSelect,
<CommandMenuPageLayoutGraphTypeSelect />,
<CommandMenuPageLayoutChartSettings />,
],
[
CommandMenuPages.PageLayoutGraphFilter,
@@ -14,7 +14,7 @@ const StyledContainer = styled.div`
height: 100%;
`;
export const CommandMenuPageLayoutGraphTypeSelect = () => {
export const CommandMenuPageLayoutChartSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const draftPageLayout = useRecoilComponentValue(
@@ -27,21 +27,12 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
pageLayoutId,
);
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
const widgetInEditMode = draftPageLayout.tabs
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === pageLayoutEditingWidgetId);
if (!isDefined(widgetInEditMode)) {
throw new Error(
`Widget with ID ${pageLayoutEditingWidgetId} not found in page layout`,
);
}
if (
!isDefined(widgetInEditMode) ||
!isDefined(widgetInEditMode.configuration) ||
!('graphType' in widgetInEditMode.configuration)
) {
@@ -1,10 +1,11 @@
import { ChartFiltersSettings } from '@/command-menu/pages/page-layout/components/ChartFiltersSettings';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const CommandMenuPageLayoutGraphFilter = () => {
@@ -24,28 +25,27 @@ export const CommandMenuPageLayoutGraphFilter = () => {
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === pageLayoutEditingWidgetId);
if (!isDefined(widgetInEditMode)) {
throw new Error(
`Widget with ID ${pageLayoutEditingWidgetId} not found in page layout`,
);
}
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
throw new Error('No data source in chart');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: widgetInEditMode.objectMetadataId,
});
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
if (!isChartWidget(widgetInEditMode)) {
if (
!isDefined(widgetInEditMode) ||
!isDefined(widgetInEditMode.objectMetadataId) ||
!isChartWidget(widgetInEditMode)
) {
return null;
}
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === widgetInEditMode?.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Object metadata item not found for id ${widgetInEditMode?.objectMetadataId}`,
);
}
return (
<>
<ChartFiltersSettings
@@ -1,3 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
@@ -24,9 +25,13 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutId,
);
const { closeCommandMenu } = useCommandMenu();
const deletePageLayoutWidget = useRecoilCallback(
({ snapshot, set }) =>
(widgetId: string) => {
closeCommandMenu();
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
@@ -53,7 +58,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
}));
}
},
[pageLayoutCurrentLayoutsState, pageLayoutDraftState],
[closeCommandMenu, pageLayoutCurrentLayoutsState, pageLayoutDraftState],
);
return { deletePageLayoutWidget };
@@ -45,23 +45,12 @@ const StyledTabList = styled(TabList)`
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledContentContainer = styled.div<{
isInRightDrawer: boolean;
}>`
background: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.background.secondary : theme.background.primary};
border: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? `1px solid ${theme.border.color.light}` : 'none'};
border-radius: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.border.radius.md : '0'};
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
flex: 1;
margin: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.spacing(3) : '0'};
overflow-y: auto;
.scroll-wrapper-y-enabled {
height: auto;
}
background: ${({ theme }) => theme.background.primary};
padding-bottom: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.spacing(16) : 0};
`;
type ShowPageSubContainerProps = {
@@ -0,0 +1,259 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { CALENDAR_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
@Command({
name: 'upgrade:1-12:add-calendar-events-import-scheduled-sync-stage',
description:
'Replace calendar channel syncStage enum with complete CalendarChannelSyncStage values and update default to PENDING_CONFIGURATION',
})
export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
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(
`Updating calendar channel syncStage enum and default value for workspace ${workspaceId}`,
);
await this.updateCalendarChannelSyncStageFieldMetadata(
workspaceId,
options,
);
await this.addCalendarEventsImportScheduledEnumValue(workspaceId, options);
this.logger.log(
`Successfully updated calendar channel syncStage enum and default value for workspace ${workspaceId}`,
);
}
private async addCalendarEventsImportScheduledEnumValue(
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 enum migration`,
);
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'calendarChannel';
const columnName = 'syncStage';
const enumName = 'calendarChannel_syncStage_enum';
if (options.dryRun) {
this.logger.log(
`Would replace ${enumName} with complete CalendarChannelSyncStage enum values for workspace ${workspaceId}`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Remove default value first
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT`,
);
// Convert column to text to remove enum dependency
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE text USING "${columnName}"::text`,
);
// Drop the old enum (now safe since no column uses it)
await queryRunner.query(
`DROP TYPE IF EXISTS ${schemaName}."${enumName}" CASCADE`,
);
// Create new enum with all CalendarChannelSyncStage values
await queryRunner.query(
`CREATE TYPE ${schemaName}."${enumName}" AS ENUM (
'${CalendarChannelSyncStage.PENDING_CONFIGURATION}',
'${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING}',
'${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED}',
'${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING}',
'${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING}',
'${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED}',
'${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING}',
'${CalendarChannelSyncStage.FAILED}'
)`,
);
// Convert column back to enum type
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" TYPE ${schemaName}."${enumName}"
USING "${columnName}"::${schemaName}."${enumName}"`,
);
// Update default value to PENDING_CONFIGURATION
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" SET DEFAULT '${CalendarChannelSyncStage.PENDING_CONFIGURATION}'::${schemaName}."${enumName}"`,
);
await queryRunner.commitTransaction();
this.logger.log(
`Successfully replaced ${enumName} with complete CalendarChannelSyncStage enum values and updated default to PENDING_CONFIGURATION for workspace ${workspaceId}`,
);
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Error replacing ${enumName} for workspace ${workspaceId}: ${error}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
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;
}
if (options.dryRun) {
this.logger.log(
`Would add CALENDAR_EVENTS_IMPORT_SCHEDULED to CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
);
return;
}
const syncStageFieldOptions = [
{
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
label: 'Calendar event list fetch pending',
position: 0,
color: 'blue',
},
{
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
label: 'Calendar event list fetch scheduled',
position: 1,
color: 'green',
},
{
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
label: 'Calendar event list fetch ongoing',
position: 2,
color: 'orange',
},
{
value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING,
label: 'Calendar events import pending',
position: 3,
color: 'blue',
},
{
value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED,
label: 'Calendar events import scheduled',
position: 4,
color: 'green',
},
{
value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
label: 'Calendar events import ongoing',
position: 5,
color: 'orange',
},
{
value: CalendarChannelSyncStage.FAILED,
label: 'Failed',
position: 6,
color: 'red',
},
{
value: CalendarChannelSyncStage.PENDING_CONFIGURATION,
label: 'Pending configuration',
position: 7,
color: 'gray',
},
];
syncStageField.options = syncStageFieldOptions;
syncStageField.defaultValue = `'${CalendarChannelSyncStage.PENDING_CONFIGURATION}'`;
await this.fieldMetadataRepository.save(syncStageField);
this.logger.log(
`Added CALENDAR_EVENTS_IMPORT_SCHEDULED to CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,256 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MESSAGE_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@Command({
name: 'upgrade:1-12:add-messages-import-scheduled-sync-stage',
description:
'Replace message channel syncStage enum with complete MessageChannelSyncStage values and update default to PENDING_CONFIGURATION',
})
export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
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(
`Updating message channel syncStage enum and default value for workspace ${workspaceId}`,
);
await this.updateMessageChannelSyncStageFieldMetadata(workspaceId, options);
await this.addMessagesImportScheduledEnumValue(workspaceId, options);
this.logger.log(
`Successfully updated message channel syncStage enum and default value for workspace ${workspaceId}`,
);
}
private async addMessagesImportScheduledEnumValue(
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 enum migration`,
);
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'messageChannel';
const columnName = 'syncStage';
const enumName = 'messageChannel_syncStage_enum';
if (options.dryRun) {
this.logger.log(
`Would replace ${enumName} with complete MessageChannelSyncStage enum values for workspace ${workspaceId}`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Remove default value first
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT`,
);
// Convert column to text to remove enum dependency
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE text USING "${columnName}"::text`,
);
// Drop the old enum (now safe since no column uses it)
await queryRunner.query(
`DROP TYPE IF EXISTS ${schemaName}."${enumName}" CASCADE`,
);
// Create new enum with all MessageChannelSyncStage values
await queryRunner.query(
`CREATE TYPE ${schemaName}."${enumName}" AS ENUM (
'${MessageChannelSyncStage.PENDING_CONFIGURATION}',
'${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}',
'${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}',
'${MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING}',
'${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}',
'${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}',
'${MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING}',
'${MessageChannelSyncStage.FAILED}'
)`,
);
// Convert column back to enum type
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" TYPE ${schemaName}."${enumName}"
USING "${columnName}"::${schemaName}."${enumName}"`,
);
// Update default value to PENDING_CONFIGURATION
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnName}" SET DEFAULT '${MessageChannelSyncStage.PENDING_CONFIGURATION}'::${schemaName}."${enumName}"`,
);
await queryRunner.commitTransaction();
this.logger.log(
`Successfully replaced ${enumName} with complete MessageChannelSyncStage enum values and updated default to PENDING_CONFIGURATION for workspace ${workspaceId}`,
);
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Error replacing ${enumName} for workspace ${workspaceId}: ${error}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
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;
}
if (options.dryRun) {
this.logger.log(
`Would add MESSAGES_IMPORT_SCHEDULED to MessageChannel syncStage field metadata for workspace ${workspaceId}`,
);
return;
}
const syncStageFieldOptions = [
{
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
label: 'Messages list fetch pending',
position: 0,
color: 'blue',
},
{
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
label: 'Messages list fetch scheduled',
position: 1,
color: 'green',
},
{
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
label: 'Messages list fetch ongoing',
position: 2,
color: 'orange',
},
{
value: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING,
label: 'Messages import pending',
position: 3,
color: 'blue',
},
{
value: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED,
label: 'Messages import scheduled',
position: 4,
color: 'green',
},
{
value: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
label: 'Messages import ongoing',
position: 5,
color: 'orange',
},
{
value: MessageChannelSyncStage.FAILED,
label: 'Failed',
position: 6,
color: 'red',
},
{
value: MessageChannelSyncStage.PENDING_CONFIGURATION,
label: 'Pending configuration',
position: 7,
color: 'gray',
},
];
syncStageField.options = syncStageFieldOptions;
syncStageField.defaultValue = `'${MessageChannelSyncStage.PENDING_CONFIGURATION}'`;
await this.fieldMetadataRepository.save(syncStageField);
this.logger.log(
`Added MESSAGES_IMPORT_SCHEDULED to MessageChannel syncStage field metadata for workspace ${workspaceId}`,
);
}
}
@@ -1,25 +1,39 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddCalendarEventsImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command';
import { AddMessagesImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command';
import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command';
import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command';
import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
@Module({
imports: [
TypeOrmModule.forFeature([WorkspaceEntity]),
TypeOrmModule.forFeature([
WorkspaceEntity,
ObjectMetadataEntity,
FieldMetadataEntity,
]),
WorkspaceSchemaManagerModule,
ApplicationModule,
FieldMetadataModule,
],
providers: [
AddCalendarEventsImportScheduledSyncStageCommand,
AddMessagesImportScheduledSyncStageCommand,
CreateWorkspaceCustomApplicationCommand,
SetStandardApplicationNotUninstallableCommand,
WorkspaceCustomApplicationIdNonNullableCommand,
],
exports: [
AddCalendarEventsImportScheduledSyncStageCommand,
AddMessagesImportScheduledSyncStageCommand,
CreateWorkspaceCustomApplicationCommand,
SetStandardApplicationNotUninstallableCommand,
WorkspaceCustomApplicationIdNonNullableCommand,
@@ -21,6 +21,8 @@ import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-ve
import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command';
import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command';
import { CreateTwentyStandardApplicationCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command';
import { AddCalendarEventsImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command';
import { AddMessagesImportScheduledSyncStageCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command';
import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command';
import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command';
import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command';
@@ -84,6 +86,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly createTwentyStandardApplicationCommand: CreateTwentyStandardApplicationCommand,
protected readonly createWorkspaceCustomApplicationCommand: CreateWorkspaceCustomApplicationCommand,
protected readonly workspaceCustomApplicationIdNonNullableCommand: WorkspaceCustomApplicationIdNonNullableCommand,
protected readonly addMessagesImportScheduledSyncStageCommand: AddMessagesImportScheduledSyncStageCommand,
protected readonly addCalendarEventsImportScheduledSyncStageCommand: AddCalendarEventsImportScheduledSyncStageCommand,
) {
super(
workspaceRepository,
@@ -143,6 +147,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
beforeSyncMetadata: [
this.createWorkspaceCustomApplicationCommand,
this.workspaceCustomApplicationIdNonNullableCommand,
this.addMessagesImportScheduledSyncStageCommand,
this.addCalendarEventsImportScheduledSyncStageCommand,
],
afterSyncMetadata: [this.setStandardApplicationNotUninstallableCommand],
};
@@ -33,6 +33,7 @@ import {
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing';
import { useDisableIntrospectionForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-for-unauthenticated-users.hook';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
@@ -71,6 +72,9 @@ export class GraphQLConfigService
i18nService: this.i18nService,
twentyConfigService: this.twentyConfigService,
}),
useDisableIntrospectionForUnauthenticatedUsers(
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
),
];
if (Sentry.isInitialized()) {
@@ -7,6 +7,7 @@ import { useCachedMetadata } from 'src/engine/api/graphql/graphql-config/hooks/u
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
import { type CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { type ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useDisableIntrospectionForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-for-unauthenticated-users.hook';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { type I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
@@ -41,6 +42,9 @@ export const metadataModuleFactory = async (
cacheSetter: cacheStorageService.set.bind(cacheStorageService),
operationsToCache: ['ObjectMetadataItems', 'FindAllCoreViews'],
}),
useDisableIntrospectionForUnauthenticatedUsers(
twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
),
],
path: '/metadata',
context: () => ({
@@ -0,0 +1,17 @@
import { type Plugin } from 'graphql-yoga';
import { NoSchemaIntrospectionCustomRule } from 'graphql/validation/rules/custom/NoSchemaIntrospectionCustomRule';
import { isDefined } from 'twenty-shared/utils';
import { type GraphQLContext } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
export const useDisableIntrospectionForUnauthenticatedUsers = (
isProductionEnvironment: boolean,
): Plugin<GraphQLContext> => ({
onValidate: ({ context, addValidationRule }) => {
const isAuthenticated = isDefined(context.req.workspace);
if (!isAuthenticated && isProductionEnvironment) {
addValidationRule(NoSchemaIntrospectionCustomRule);
}
},
});
@@ -146,9 +146,11 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEnt
async updateOneField({
updateFieldInput,
workspaceId,
isSystemBuild = false,
}: {
updateFieldInput: Omit<UpdateFieldInput, 'workspaceId'>;
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<FlatFieldMetadata> {
const {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
@@ -256,7 +258,7 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEnt
}),
},
buildOptions: {
isSystemBuild: false,
isSystemBuild,
inferDeletionFromMissingEntities: {
index: true,
viewGroup: true,
@@ -4,6 +4,10 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { 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,
@@ -14,6 +18,10 @@ import {
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;
@@ -56,9 +64,17 @@ export class ChannelSyncService {
for (const messageChannel of messageChannels) {
await messageChannelRepository.update(messageChannel.id, {
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
syncStatus: MessageChannelSyncStatus.ONGOING,
});
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
@@ -81,9 +97,17 @@ export class ChannelSyncService {
for (const calendarChannel of calendarChannels) {
await calendarChannelRepository.update(calendarChannel.id, {
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
syncStatus: CalendarChannelSyncStatus.ONGOING,
});
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
workspaceId,
calendarChannelId: calendarChannel.id,
},
);
}
}
}
@@ -1,8 +1,8 @@
import { registerEnumType } from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
@@ -1,35 +0,0 @@
import { Command, CommandRunner } from 'nest-commander';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import {
MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN,
MessagingProcessFolderActionsCronJob,
} from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-process-folder-actions.cron.job';
@Command({
name: 'cron:messaging:process-folder-actions',
description:
'Starts a cron job to process pending folder actions (deletion) for message channels',
})
export class MessagingProcessFolderActionsCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: MessagingProcessFolderActionsCronJob.name,
data: undefined,
options: {
repeat: {
pattern: MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN,
},
},
});
}
}
@@ -1,76 +0,0 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import {
MessagingProcessFolderActionsJob,
type MessagingProcessFolderActionsJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-process-folder-actions.job';
export const MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN = '*/15 * * * *';
@Processor(MessageQueue.cronQueue)
export class MessagingProcessFolderActionsCronJob {
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@Process(MessagingProcessFolderActionsCronJob.name)
@SentryCronMonitor(
MessagingProcessFolderActionsCronJob.name,
MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN,
)
async handle(): Promise<void> {
const activeWorkspaces = await this.workspaceRepository.find({
where: {
activationStatus: WorkspaceActivationStatus.ACTIVE,
},
});
for (const activeWorkspace of activeWorkspaces) {
try {
const schemaName = getWorkspaceSchemaName(activeWorkspace.id);
const messageChannels = await this.coreDataSource.query(
`SELECT DISTINCT mc.id
FROM ${schemaName}."messageChannel" mc
INNER JOIN ${schemaName}."messageFolder" mf ON mf."messageChannelId" = mc.id
WHERE mf."pendingSyncAction" = '${MessageFolderPendingSyncAction.FOLDER_DELETION}'`,
);
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingProcessFolderActionsJobData>(
MessagingProcessFolderActionsJob.name,
{
workspaceId: activeWorkspace.id,
messageChannelId: messageChannel.id,
},
);
}
} catch (error) {
this.exceptionHandlerService.captureExceptions([error], {
workspace: {
id: activeWorkspace.id,
},
});
}
}
}
}
@@ -1,92 +0,0 @@
import { Logger, Scope } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
export type MessagingProcessFolderActionsJobData = {
workspaceId: string;
messageChannelId: string;
};
@Processor({
queueName: MessageQueue.messagingQueue,
scope: Scope.REQUEST,
})
export class MessagingProcessFolderActionsJob {
private readonly logger = new Logger(MessagingProcessFolderActionsJob.name);
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly messagingProcessFolderActionsService: MessagingProcessFolderActionsService,
) {}
@Process(MessagingProcessFolderActionsJob.name)
async handle(data: MessagingProcessFolderActionsJobData): Promise<void> {
const { workspaceId, messageChannelId } = data;
this.logger.log(
`Processing pending folder actions for message channel ${messageChannelId} in workspace ${workspaceId}`,
);
const messageChannelRepository =
await this.twentyORMManager.getRepository<MessageChannelWorkspaceEntity>(
'messageChannel',
);
const messageChannel = await messageChannelRepository.findOne({
where: {
id: messageChannelId,
},
});
if (!messageChannel) {
this.logger.warn(
`Message channel ${messageChannelId} not found in workspace ${workspaceId}`,
);
return;
}
const messageFolderRepository =
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
'messageFolder',
);
const messageFolders = await messageFolderRepository.find({
where: {
messageChannelId: messageChannel.id,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
});
if (messageFolders.length === 0) {
this.logger.log(
`Message channel ${messageChannelId} has no folders with pending deletion actions, skipping`,
);
return;
}
try {
await this.messagingProcessFolderActionsService.processFolderActions(
messageChannel,
messageFolders,
workspaceId,
);
} catch (error) {
this.logger.error(
`Error processing folder actions for message channel ${messageChannelId} in workspace ${workspaceId}: ${error.message}`,
error.stack,
);
throw error;
}
}
}
@@ -18,12 +18,10 @@ import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/messa
import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command';
import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command';
import { MessagingOngoingStaleCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command';
import { MessagingProcessFolderActionsCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-process-folder-actions.cron.command';
import { MessagingRelaunchFailedMessageChannelsCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-relaunch-failed-message-channels.cron.command';
import { MessagingMessageListFetchCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job';
import { MessagingMessagesImportCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job';
import { MessagingOngoingStaleCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job';
import { MessagingProcessFolderActionsCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-process-folder-actions.cron.job';
import { MessagingRelaunchFailedMessageChannelsCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-relaunch-failed-message-channels.cron.job';
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
import { MessagingIMAPDriverModule } from 'src/modules/messaging/message-import-manager/drivers/imap/messaging-imap-driver.module';
@@ -34,7 +32,6 @@ import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-man
import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job';
import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { MessagingProcessFolderActionsJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-process-folder-actions.job';
import { MessagingRelaunchFailedMessageChannelJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job';
import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
@@ -83,18 +80,15 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingMessageListFetchCronCommand,
MessagingMessagesImportCronCommand,
MessagingOngoingStaleCronCommand,
MessagingProcessFolderActionsCronCommand,
MessagingRelaunchFailedMessageChannelsCronCommand,
MessagingSingleMessageImportCommand,
MessagingMessageListFetchJob,
MessagingMessagesImportJob,
MessagingOngoingStaleJob,
MessagingProcessFolderActionsJob,
MessagingRelaunchFailedMessageChannelJob,
MessagingMessageListFetchCronJob,
MessagingMessagesImportCronJob,
MessagingOngoingStaleCronJob,
MessagingProcessFolderActionsCronJob,
MessagingRelaunchFailedMessageChannelsCronJob,
MessagingAddSingleMessageToCacheForImportJob,
MessagingMessageImportManagerMessageChannelListener,
@@ -16,6 +16,7 @@ import { MessagingGetMessageListService } from 'src/modules/messaging/message-im
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
describe('MessagingMessageListFetchService', () => {
@@ -78,15 +79,21 @@ describe('MessagingMessageListFetchService', () => {
};
const mockMessageFolderRepository = {
find: jest.fn().mockResolvedValue([
{
id: 'inbox-folder-id',
name: 'inbox',
syncCursor: 'inbox-sync-cursor',
messageChannelId: 'microsoft-message-channel-id',
isSynced: true,
},
]),
find: jest.fn().mockImplementation(({ where }) => {
if (where?.pendingSyncAction === 'FOLDER_DELETION') {
return [];
}
return [
{
id: 'inbox-folder-id',
name: 'inbox',
syncCursor: 'inbox-sync-cursor',
messageChannelId: 'microsoft-message-channel-id',
isSynced: true,
},
];
}),
};
const module: TestingModule = await Test.createTestingModule({
@@ -238,6 +245,12 @@ describe('MessagingMessageListFetchService', () => {
processGroupEmailActions: jest.fn().mockResolvedValue(undefined),
},
},
{
provide: MessagingProcessFolderActionsService,
useValue: {
processFolderActions: jest.fn().mockResolvedValue(undefined),
},
},
],
}).compile();
@@ -30,6 +30,7 @@ import {
MessageImportSyncStep,
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
const ONE_WEEK_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
@@ -50,6 +51,7 @@ export class MessagingMessageListFetchService {
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
private readonly messagingProcessFolderActionsService: MessagingProcessFolderActionsService,
) {}
public async processMessageListFetch(
@@ -57,21 +59,17 @@ export class MessagingMessageListFetchService {
workspaceId: string,
) {
try {
if (
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION ||
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT
) {
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending group emails action before message list fetch: ${messageChannel.pendingGroupEmailsAction}`,
);
const pendingGroupEmailActionsProcessed =
await this.processPendingGroupEmailActions(messageChannel, workspaceId);
await this.messagingProcessGroupEmailActionsService.processGroupEmailActions(
messageChannel,
workspaceId,
);
if (pendingGroupEmailActionsProcessed) {
return;
}
const pendingFolderActionsProcessed =
await this.processPendingFolderActions(messageChannel, workspaceId);
if (pendingFolderActionsProcessed) {
return;
}
@@ -288,6 +286,65 @@ export class MessagingMessageListFetchService {
}
}
private async processPendingGroupEmailActions(
messageChannel: MessageChannelWorkspaceEntity,
workspaceId: string,
): Promise<boolean> {
const hasPendingGroupEmailAction =
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION ||
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT;
if (!hasPendingGroupEmailAction) {
return false;
}
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending group emails action before message list fetch: ${messageChannel.pendingGroupEmailsAction}`,
);
await this.messagingProcessGroupEmailActionsService.processGroupEmailActions(
messageChannel,
workspaceId,
);
return true;
}
private async processPendingFolderActions(
messageChannel: MessageChannelWorkspaceEntity,
workspaceId: string,
): Promise<boolean> {
const messageFolderRepository =
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
'messageFolder',
);
const foldersWithPendingActions = await messageFolderRepository.find({
where: {
messageChannelId: messageChannel.id,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
});
if (foldersWithPendingActions.length === 0) {
return false;
}
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending folder actions before message list fetch`,
);
await this.messagingProcessFolderActionsService.processFolderActions(
messageChannel,
foldersWithPendingActions,
workspaceId,
);
return true;
}
private async computeFullSyncMessageChannelMessageAssociationsToDelete(
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageExternalIds: string[],
@@ -53,7 +53,7 @@ export class MessagingMessagesImportService {
try {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.MESSAGES_IMPORT_PENDING
MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED
) {
return;
}