Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d43ed4515 | |||
| 3d1d3d9f04 | |||
| b19dc703cc | |||
| 9b668619c5 | |||
| e7c83dc04f | |||
| 693fe01fa4 | |||
| c616713435 | |||
| 859c948d01 |
@@ -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,
|
||||
|
||||
+2
-11
@@ -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)
|
||||
) {
|
||||
+19
-19
@@ -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 };
|
||||
|
||||
+4
-15
@@ -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 = {
|
||||
|
||||
+259
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+256
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -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,
|
||||
|
||||
+6
@@ -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: () => ({
|
||||
|
||||
+17
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
+3
-1
@@ -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,
|
||||
|
||||
+26
-2
@@ -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
-1
@@ -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';
|
||||
|
||||
-35
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
-76
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-92
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-6
@@ -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,
|
||||
|
||||
+22
-9
@@ -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();
|
||||
|
||||
|
||||
+70
-13
@@ -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[],
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export class MessagingMessagesImportService {
|
||||
try {
|
||||
if (
|
||||
messageChannel.syncStage !==
|
||||
MessageChannelSyncStage.MESSAGES_IMPORT_PENDING
|
||||
MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user