Compare commits

...

13 Commits

Author SHA1 Message Date
Baptiste Devessier 37200a1c3b [Workflows] Fix filtering on relative date (#16087)
https://github.com/user-attachments/assets/4079d59e-0550-401f-a29b-b235edd8ed48

Closes https://github.com/twentyhq/twenty/issues/16054
2025-11-26 18:04:52 +01:00
Charles Bochet bc8c9eabeb Refactor error messages messaging (#16094)
We should try catch locally gmail errors when:
- fetching message list
- fetching messages
- refreshing aliases
- fetching folders
2025-11-26 18:04:43 +01:00
neo773 c415660640 fix scheduling after folder actions are processed (#16097) 2025-11-26 18:04:35 +01:00
Paul Rastoin ad75662919 Non composite and non morph or relation field update fix (#16091) 2025-11-26 12:57:44 +01:00
Paul Rastoin aa7c5a778b Refactor upgrade devx to allow configuring workspaces status to pass over (#16066)
We need to be able to create custom workspace application on all
workspaces, even pending and ongoing etc
Right now the upgrade devx only allows and expect active or suspended
workspace to be passed to runOnWorkspace.

Created an intermediate class `WorkspacesMigrationRunner` that expect an
array `WorkspaceStatus` to be fetched for the current command to be run
on
The `ActiveOrSuspendedCommandRunner` statically passes both `SUSPENDED`
and `ACTIVE`, whereas the create workspace custom application passed all
the enum values

Workspace that are not fully init don't have a `workspace_schema` so
they don't have `dataSource`
Made a not very elegant check to see if current workspace we're about to
create dataSource on has one historically
Which means that dataSource is now optional, it had only one impact on
an existing command and the desired devx will become consuming existing
services that do not expect dataSource ( or at least yet )
2025-11-26 12:57:33 +01:00
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
112 changed files with 1785 additions and 1080 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
@@ -49,8 +49,12 @@ export const AdvancedFilterDropdownFilterInput = ({
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
))}
{filterType === 'RATING' && <ObjectFilterDropdownRatingInput />}
{filterType === 'DATE_TIME' && <ObjectFilterDropdownDateTimeInput />}
{filterType === 'DATE' && <ObjectFilterDropdownDateInput />}
{filterType === 'DATE_TIME' && (
<ObjectFilterDropdownDateTimeInput instanceId={filterDropdownId} />
)}
{filterType === 'DATE' && (
<ObjectFilterDropdownDateInput instanceId={filterDropdownId} />
)}
{filterType === 'RELATION' && (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<ObjectFilterDropdownSearchInput />
@@ -24,7 +24,13 @@ import {
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { formatDateString } from '~/utils/string/formatDateString';
export const ObjectFilterDropdownDateInput = () => {
type ObjectFilterDropdownDateInputProps = {
instanceId: string;
};
export const ObjectFilterDropdownDateInput = ({
instanceId,
}: ObjectFilterDropdownDateInputProps) => {
const { dateFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
@@ -109,6 +115,7 @@ export const ObjectFilterDropdownDateInput = () => {
return (
<DatePicker
instanceId={instanceId}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
date={plainDateValue ?? null}
@@ -24,7 +24,13 @@ import {
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
export const ObjectFilterDropdownDateTimeInput = () => {
type ObjectFilterDropdownDateTimeInputProps = {
instanceId: string;
};
export const ObjectFilterDropdownDateTimeInput = ({
instanceId,
}: ObjectFilterDropdownDateTimeInputProps) => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
@@ -112,6 +118,7 @@ export const ObjectFilterDropdownDateTimeInput = () => {
return (
<DateTimePicker
instanceId={instanceId}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
date={internalDate}
@@ -82,7 +82,7 @@ export const ObjectFilterDropdownFilterInput = ({
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
<ObjectFilterDropdownDateInput />
<ObjectFilterDropdownDateInput instanceId={filterDropdownId} />
</>
);
} else if (filterType === 'DATE_TIME') {
@@ -90,7 +90,7 @@ export const ObjectFilterDropdownFilterInput = ({
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
<ObjectFilterDropdownDateTimeInput />
<ObjectFilterDropdownDateTimeInput instanceId={filterDropdownId} />
</>
);
} else {
@@ -13,13 +13,9 @@ import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUs
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import {
isDefined,
type RelativeDateFilter,
type RelativeDateFilterDirection,
type RelativeDateFilterUnit,
} from 'twenty-shared/utils';
import { isDefined, type RelativeDateFilter } from 'twenty-shared/utils';
export const useApplyObjectFilterDropdownOperand = () => {
const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValue(
@@ -108,9 +104,7 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.displayValue = displayValue;
} else if (newOperand === RecordFilterOperand.IS_RELATIVE) {
const defaultRelativeDate: RelativeDateFilter = {
direction: 'THIS' as RelativeDateFilterDirection,
amount: 1,
unit: 'DAY' as RelativeDateFilterUnit,
...DEFAULT_RELATIVE_DATE_FILTER_VALUE,
timezone: userTimezone,
};
@@ -121,6 +121,7 @@ export const RecordCalendarTopBar = () => {
dropdownComponents={
<DropdownContent widthInPixels={280}>
<DateTimePicker
instanceId={recordCalendarId}
date={recordCalendarSelectedDate}
onChange={handleDateChange}
onClose={handleDateChange}
@@ -337,6 +337,7 @@ export const FormDateFieldInput = ({
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DatePicker
instanceId={instanceId}
date={pickerDate}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
@@ -336,6 +336,7 @@ export const FormDateTimeFieldInput = ({
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DateTimePicker
instanceId={instanceId}
date={pickerDate ?? new Date()}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
@@ -1,6 +1,7 @@
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
import { isNonEmptyString, isString } from '@sniptt/guards';
import { useId } from 'react';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import {
@@ -21,6 +22,8 @@ export const FormRelativeDatePicker = ({
onChange,
readonly,
}: FormRelativeDatePickerProps) => {
const instanceId = useId();
const value =
isString(defaultValue) && isNonEmptyString(defaultValue)
? safeParseRelativeDateFilterJSONStringified(defaultValue)
@@ -32,6 +35,7 @@ export const FormRelativeDatePicker = ({
return (
<RelativeDatePickerHeader
instanceId={instanceId}
onChange={handleValueChange}
direction={value?.direction ?? 'THIS'}
unit={value?.unit ?? 'DAY'}
@@ -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 };
@@ -109,6 +109,7 @@ export const DateInput = ({
return (
<div ref={wrapperRef}>
<DatePicker
instanceId={instanceId}
date={internalValue ?? null}
onChange={handleChange}
onClose={handleClose}
@@ -109,6 +109,7 @@ export const DateTimeInput = ({
return (
<div ref={wrapperRef}>
<DateTimePicker
instanceId={instanceId}
date={internalValue ?? null}
onChange={handleChange}
onClose={handleClose}
@@ -302,6 +302,7 @@ const StyledDatePickerFallback = styled.div`
`;
type DatePickerProps = {
instanceId: string;
isRelative?: boolean;
hideHeaderInput?: boolean;
date: Nullable<string>;
@@ -333,6 +334,7 @@ const ReactDatePicker = lazy<ComponentType<DatePickerPropsType>>(() =>
);
export const DatePicker = ({
instanceId,
date,
onChange,
onClose,
@@ -476,6 +478,7 @@ export const DatePicker = ({
}) =>
isRelative ? (
<RelativeDatePickerHeader
instanceId={instanceId}
direction={relativeDate?.direction ?? 'PAST'}
amount={relativeDate?.amount}
unit={relativeDate?.unit ?? 'DAY'}
@@ -300,6 +300,7 @@ const StyledDatePickerFallback = styled.div`
`;
type DateTimePickerProps = {
instanceId: string;
isRelative?: boolean;
hideHeaderInput?: boolean;
date: Date | null;
@@ -331,6 +332,7 @@ const ReactDatePicker = lazy<ComponentType<DatePickerPropsType>>(() =>
);
export const DateTimePicker = ({
instanceId,
date,
onChange,
onClose,
@@ -484,6 +486,7 @@ export const DateTimePicker = ({
}) =>
isRelative ? (
<RelativeDatePickerHeader
instanceId={instanceId}
direction={relativeDate?.direction ?? 'PAST'}
amount={relativeDate?.amount}
unit={relativeDate?.unit ?? 'DAY'}
@@ -21,6 +21,7 @@ const StyledContainer = styled.div<{ noPadding: boolean }>`
`;
type RelativeDatePickerHeaderProps = {
instanceId: string;
direction: RelativeDateFilterDirection;
amount?: Nullable<number>;
unit: RelativeDateFilterUnit;
@@ -31,6 +32,7 @@ type RelativeDatePickerHeaderProps = {
};
export const RelativeDatePickerHeader = ({
instanceId,
direction,
unit,
amount,
@@ -53,7 +55,7 @@ export const RelativeDatePickerHeader = ({
return (
<StyledContainer noPadding={isFormField ?? false}>
<Select
dropdownId="direction-select"
dropdownId={`direction-select-${instanceId}`}
value={direction}
onChange={(newDirection) => {
if (amount === undefined && newDirection !== 'THIS') {
@@ -71,7 +73,7 @@ export const RelativeDatePickerHeader = ({
disabled={readonly}
/>
<SettingsTextInput
instanceId="relative-date-picker-amount"
instanceId={`relative-date-picker-amount-${instanceId}`}
width={50}
value={textInputValue}
onChange={(text) => {
@@ -92,7 +94,7 @@ export const RelativeDatePickerHeader = ({
disabled={direction === 'THIS' || readonly}
/>
<Select
dropdownId="unit-select"
dropdownId={`unit-select-${instanceId}`}
value={unit}
onChange={(newUnit) => {
if (direction !== 'THIS' && amount === undefined) {
@@ -17,6 +17,7 @@ const meta: Meta<typeof DateTimePicker> = {
const [, updateArgs] = useArgs();
return (
<DateTimePicker
instanceId="story-date-time-picker"
date={isDefined(date) ? new Date(date) : new Date()}
onChange={(newDate) => updateArgs({ date: newDate })}
/>
@@ -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 = {
@@ -11,6 +11,7 @@ import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgr
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
@@ -72,9 +73,37 @@ const buildUpgradeCommandModule = async ({
appVersion,
commandRunner,
}: BuildUpgradeCommandModuleArgs) => {
const mockDataSourceService = {
getLastDataSourceMetadataFromWorkspaceId: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
commandRunner,
{
provide: commandRunner,
useFactory: (
workspaceRepository: Repository<WorkspaceEntity>,
twentyConfigService: TwentyConfigService,
twentyORMGlobalManager: TwentyORMGlobalManager,
dataSourceService: DataSourceService,
syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
) => {
return new commandRunner(
workspaceRepository,
twentyConfigService,
twentyORMGlobalManager,
dataSourceService,
syncWorkspaceMetadataCommand,
);
},
inject: [
getRepositoryToken(WorkspaceEntity),
TwentyConfigService,
TwentyORMGlobalManager,
DataSourceService,
SyncWorkspaceMetadataCommand,
],
},
{
provide: getRepositoryToken(WorkspaceEntity),
useValue: {
@@ -110,6 +139,10 @@ const buildUpgradeCommandModule = async ({
getDataSourceForWorkspace: jest.fn(),
},
},
{
provide: DataSourceService,
useValue: mockDataSourceService,
},
{
provide: SyncWorkspaceMetadataCommand,
useValue: {
@@ -1,180 +1,29 @@
import chalk from 'chalk';
import { Option } from 'nest-commander';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, MoreThanOrEqual, type Repository } from 'typeorm';
import { type Repository } from 'typeorm';
import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import {
WorkspacesMigrationCommandRunner,
type WorkspacesMigrationCommandOptions,
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export type ActiveOrSuspendedWorkspacesMigrationCommandOptions = {
workspaceIds: string[];
startFromWorkspaceId?: string;
workspaceCountLimit?: number;
dryRun?: boolean;
verbose?: boolean;
};
export type RunOnWorkspaceArgs = {
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
workspaceId: string;
dataSource: WorkspaceDataSource;
index: number;
total: number;
};
export type WorkspaceMigrationReport = {
fail: {
workspaceId: string;
error: Error;
}[];
success: {
workspaceId: string;
}[];
};
export type ActiveOrSuspendedWorkspacesMigrationCommandOptions =
WorkspacesMigrationCommandOptions;
export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
Options extends
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
> extends MigrationCommandRunner {
private workspaceIds: Set<string> = new Set();
private startFromWorkspaceId: string | undefined;
private workspaceCountLimit: number | undefined;
public migrationReport: WorkspaceMigrationReport = {
fail: [],
success: [],
};
> extends WorkspacesMigrationCommandRunner<Options> {
constructor(
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super();
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]);
}
@Option({
flags: '--start-from-workspace-id [workspace_id]',
description:
'Start from a specific workspace id. Workspaces are processed in ascending order of id.',
required: false,
})
parseStartFromWorkspaceId(val: string): string {
this.startFromWorkspaceId = val;
return val;
}
@Option({
flags: '--workspace-count-limit [count]',
description:
'Limit the number of workspaces to process. Workspaces are processed in ascending order of id.',
required: false,
})
parseWorkspaceCountLimit(val: string): number {
this.workspaceCountLimit = parseInt(val);
if (isNaN(this.workspaceCountLimit)) {
throw new Error('Workspace count limit must be a number');
}
if (this.workspaceCountLimit <= 0) {
throw new Error('Workspace count limit must be greater than 0');
}
return this.workspaceCountLimit;
}
@Option({
flags: '-w, --workspace-id [workspace_id]',
description:
'workspace id. Command runs on all active workspaces if not provided.',
required: false,
})
parseWorkspaceId(val: string): Set<string> {
this.workspaceIds.add(val);
return this.workspaceIds;
}
protected async fetchActiveWorkspaceIds(): Promise<string[]> {
const activeWorkspaces = await this.workspaceRepository.find({
select: ['id'],
where: {
activationStatus: In([
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]),
...(this.startFromWorkspaceId
? { id: MoreThanOrEqual(this.startFromWorkspaceId) }
: {}),
},
order: {
id: 'ASC',
},
take: this.workspaceCountLimit,
});
return activeWorkspaces.map((workspace) => workspace.id);
}
override async runMigrationCommand(
_passedParams: string[],
options: Options,
) {
const activeWorkspaceIds =
this.workspaceIds.size > 0
? Array.from(this.workspaceIds)
: await this.fetchActiveWorkspaceIds();
if (options.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
this.logger.log(
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
);
try {
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
await this.runOnWorkspace({
options,
workspaceId,
dataSource,
index: index,
total: activeWorkspaceIds.length,
});
this.migrationReport.success.push({
workspaceId,
});
} catch (error) {
this.migrationReport.fail.push({
error,
workspaceId,
});
this.logger.warn(
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
);
}
try {
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
} catch (error) {
this.logger.error(error);
}
}
this.migrationReport.fail.forEach(({ error, workspaceId }) =>
this.logger.error(`Error in workspace ${workspaceId}: ${error.message}`),
);
}
public abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
}
@@ -12,10 +12,14 @@ import { In, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import {
RunOnWorkspaceArgs,
WorkspacesMigrationCommandRunner,
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import {
@@ -25,8 +29,14 @@ import {
import { getPreviousVersion } from 'src/utils/version/get-previous-version';
export type VersionCommands = {
beforeSyncMetadata: ActiveOrSuspendedWorkspacesMigrationCommandRunner[];
afterSyncMetadata: ActiveOrSuspendedWorkspacesMigrationCommandRunner[];
beforeSyncMetadata: (
| WorkspacesMigrationCommandRunner
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
)[];
afterSyncMetadata: (
| WorkspacesMigrationCommandRunner
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
)[];
};
export type AllCommands = Record<string, VersionCommands>;
const execPromise = promisify(exec);
@@ -43,9 +53,10 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyConfigService: TwentyConfigService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
private async loadActiveOrSuspendedWorkspace() {
@@ -0,0 +1,187 @@
import chalk from 'chalk';
import { Option } from 'nest-commander';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, MoreThanOrEqual, type Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { type TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export type WorkspacesMigrationCommandOptions = {
workspaceIds: string[];
startFromWorkspaceId?: string;
workspaceCountLimit?: number;
dryRun?: boolean;
verbose?: boolean;
};
export type RunOnWorkspaceArgs = {
options: WorkspacesMigrationCommandOptions;
workspaceId: string;
dataSource?: WorkspaceDataSource;
index: number;
total: number;
};
export type WorkspaceMigrationReport = {
fail: {
workspaceId: string;
error: Error;
}[];
success: {
workspaceId: string;
}[];
};
export abstract class WorkspacesMigrationCommandRunner<
Options extends
WorkspacesMigrationCommandOptions = WorkspacesMigrationCommandOptions,
> extends MigrationCommandRunner {
protected workspaceIds: Set<string> = new Set();
private startFromWorkspaceId: string | undefined;
private workspaceCountLimit: number | undefined;
public migrationReport: WorkspaceMigrationReport = {
fail: [],
success: [],
};
constructor(
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
protected readonly activationStatuses: WorkspaceActivationStatus[],
) {
super();
}
@Option({
flags: '--start-from-workspace-id [workspace_id]',
description:
'Start from a specific workspace id. Workspaces are processed in ascending order of id.',
required: false,
})
parseStartFromWorkspaceId(val: string): string {
this.startFromWorkspaceId = val;
return val;
}
@Option({
flags: '--workspace-count-limit [count]',
description:
'Limit the number of workspaces to process. Workspaces are processed in ascending order of id.',
required: false,
})
parseWorkspaceCountLimit(val: string): number {
this.workspaceCountLimit = parseInt(val);
if (isNaN(this.workspaceCountLimit)) {
throw new Error('Workspace count limit must be a number');
}
if (this.workspaceCountLimit <= 0) {
throw new Error('Workspace count limit must be greater than 0');
}
return this.workspaceCountLimit;
}
@Option({
flags: '-w, --workspace-id [workspace_id]',
description:
'workspace id. Command runs on all workspaces matching the activation statuses if not provided.',
required: false,
})
parseWorkspaceId(val: string): Set<string> {
this.workspaceIds.add(val);
return this.workspaceIds;
}
protected async fetchWorkspaceIds(): Promise<string[]> {
const workspaces = await this.workspaceRepository.find({
select: ['id'],
where: {
activationStatus: In(this.activationStatuses),
...(this.startFromWorkspaceId
? { id: MoreThanOrEqual(this.startFromWorkspaceId) }
: {}),
},
order: {
id: 'ASC',
},
take: this.workspaceCountLimit,
});
return workspaces.map((workspace) => workspace.id);
}
override async runMigrationCommand(
_passedParams: string[],
options: Options,
) {
const workspaceIdsToProcess =
this.workspaceIds.size > 0
? Array.from(this.workspaceIds)
: await this.fetchWorkspaceIds();
if (options.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
for (const [index, workspaceId] of workspaceIdsToProcess.entries()) {
this.logger.log(
`Running command on workspace ${workspaceId} ${index + 1}/${workspaceIdsToProcess.length}`,
);
try {
const workspaceHasDataSource =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
workspaceId,
);
const dataSource = isDefined(workspaceHasDataSource)
? await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
})
: undefined;
await this.runOnWorkspace({
options,
workspaceId,
dataSource,
index: index,
total: workspaceIdsToProcess.length,
});
this.migrationReport.success.push({
workspaceId,
});
} catch (error) {
this.migrationReport.fail.push({
error,
workspaceId,
});
this.logger.warn(
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
);
}
try {
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
} catch (error) {
this.logger.error(error);
}
}
this.migrationReport.fail.forEach(({ error, workspaceId }) =>
this.logger.error(`Error in workspace ${workspaceId}: ${error.message}`),
);
}
public abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
}
@@ -4,11 +4,10 @@ import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import { v4 } from 'uuid';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -25,12 +24,13 @@ export class AddWorkflowRunStopStatusesCommand extends ActiveOrSuspendedWorkspac
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -4,11 +4,10 @@ import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { In, IsNull, Not, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
@@ -24,12 +23,13 @@ export class CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand extends
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,11 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
@@ -22,10 +21,11 @@ export class CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand extends A
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,11 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@@ -20,9 +19,10 @@ export class FlushCacheCommand extends ActiveOrSuspendedWorkspacesMigrationComma
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -4,11 +4,10 @@ import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -22,11 +21,12 @@ export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWo
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -1,15 +1,14 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Not, Repository } from 'typeorm';
import { FieldActorSource } from 'twenty-shared/types';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -24,8 +23,9 @@ export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspended
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,11 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
@@ -32,8 +31,9 @@ export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspende
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,11 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@@ -21,10 +20,11 @@ export class MigrateChannelPartialFullSyncStagesCommand extends ActiveOrSuspende
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -1,15 +1,14 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, In, Repository, type QueryRunner } from 'typeorm';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { DataSource, In, Repository, type QueryRunner } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.workspace-entity';
@@ -42,6 +41,7 @@ export class RegenerateSearchVectorsCommand extends ActiveOrSuspendedWorkspacesM
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@@ -50,7 +50,7 @@ export class RegenerateSearchVectorsCommand extends ActiveOrSuspendedWorkspacesM
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -1,17 +1,16 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, Repository } from 'typeorm';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
@@ -32,6 +31,7 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(DataSourceEntity)
@@ -40,7 +40,7 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati
private readonly viewRepository: Repository<ViewEntity>,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -15,6 +15,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
@@ -35,6 +36,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
DataSourceEntity,
ApplicationEntity,
]),
DataSourceModule,
WorkspaceSchemaManagerModule,
WorkspaceCacheStorageModule,
ObjectMetadataModule,
@@ -3,12 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { In, type Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -25,8 +24,9 @@ export class CleanOrphanedRoleTargetsCommand extends ActiveOrSuspendedWorkspaces
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,12 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { In, type Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -22,10 +21,11 @@ export class CleanOrphanedUserWorkspacesCommand extends ActiveOrSuspendedWorkspa
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,14 +3,13 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { In, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/core-modules/application/constants/twenty-standard-applications';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
@@ -23,11 +22,12 @@ export class CreateTwentyStandardApplicationCommand extends ActiveOrSuspendedWor
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -8,6 +8,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -27,6 +28,7 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
RoleTargetsEntity,
ApplicationEntity,
]),
DataSourceModule,
WorkspaceSchemaManagerModule,
ApplicationModule,
],
@@ -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 } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
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,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
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 } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
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,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
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}`,
);
}
}
@@ -2,14 +2,16 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
WorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
@@ -17,14 +19,21 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
description:
'Create workspace-custom application for workspaces that do not have them',
})
export class CreateWorkspaceCustomApplicationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
export class CreateWorkspaceCustomApplicationCommand extends WorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
WorkspaceActivationStatus.ONGOING_CREATION,
WorkspaceActivationStatus.PENDING_CREATION,
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.INACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]);
}
override async runOnWorkspace({
@@ -3,12 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
// Note: this command needs to be run after CreateWorkspaceCustomApplicationCommand
@@ -21,9 +20,10 @@ export class SetStandardApplicationNotUninstallableCommand extends ActiveOrSuspe
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -1,25 +1,41 @@
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 { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
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,
]),
DataSourceModule,
WorkspaceSchemaManagerModule,
ApplicationModule,
FieldMetadataModule,
],
providers: [
AddCalendarEventsImportScheduledSyncStageCommand,
AddMessagesImportScheduledSyncStageCommand,
CreateWorkspaceCustomApplicationCommand,
SetStandardApplicationNotUninstallableCommand,
WorkspaceCustomApplicationIdNonNullableCommand,
],
exports: [
AddCalendarEventsImportScheduledSyncStageCommand,
AddMessagesImportScheduledSyncStageCommand,
CreateWorkspaceCustomApplicationCommand,
SetStandardApplicationNotUninstallableCommand,
WorkspaceCustomApplicationIdNonNullableCommand,
@@ -3,11 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
@@ -21,10 +20,11 @@ export class WorkspaceCustomApplicationIdNonNullableCommand extends ActiveOrSusp
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,11 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -22,12 +21,13 @@ export class FixLabelIdentifierPositionAndVisibilityCommand extends ActiveOrSusp
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
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 { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
@@ -20,6 +21,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
ViewEntity,
ViewFieldEntity,
]),
DataSourceModule,
WorkspaceDataSourceModule,
WorkspaceSchemaManagerModule,
WorkspaceMetadataVersionModule,
@@ -4,11 +4,10 @@ import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
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 { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
@@ -23,10 +22,11 @@ export class BackfillWorkflowManualTriggerAvailabilityCommand extends ActiveOrSu
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@Module({
imports: [TypeOrmModule.forFeature([WorkspaceEntity])],
imports: [TypeOrmModule.forFeature([WorkspaceEntity]), DataSourceModule],
providers: [BackfillWorkflowManualTriggerAvailabilityCommand],
exports: [BackfillWorkflowManualTriggerAvailabilityCommand],
})
@@ -2,13 +2,13 @@ import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Not, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
@@ -38,6 +38,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
protected readonly indexMetadataService: IndexMetadataService,
protected readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
protected readonly workspaceMigrationService: WorkspaceMigrationService,
@@ -46,7 +47,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
@InjectRepository(IndexMetadataEntity)
protected readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -57,6 +58,11 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
this.logger.log(
`Deduplicating indexed fields for workspace ${workspaceId}`,
);
if (!isDefined(dataSource)) {
throw new Error(
'Could not find workspace dataSource, should never occur',
);
}
await this.deduplicateUniqueUserEmailFieldForWorkspaceMembers({
dataSource,
@@ -4,14 +4,13 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:1-8:fill-null-serverless-function-layer-id',
@@ -26,12 +25,13 @@ export class FillNullServerlessFunctionLayerIdCommand extends ActiveOrSuspendedW
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ServerlessFunctionEntity)
protected readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@InjectRepository(ServerlessFunctionLayerEntity)
protected readonly serverlessFunctionLayerRepository: Repository<ServerlessFunctionLayerEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -1,14 +1,13 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
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 { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -32,6 +31,7 @@ export class MigrateChannelSyncStagesCommand extends ActiveOrSuspendedWorkspaces
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
@@ -39,7 +39,7 @@ export class MigrateChannelSyncStagesCommand extends ActiveOrSuspendedWorkspaces
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -8,11 +8,10 @@ import {
} from 'twenty-shared/utils';
import { Raw, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard';
@@ -28,8 +27,9 @@ export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspen
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -3,11 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository, type QueryRunner } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@@ -26,9 +25,10 @@ export class RegeneratePersonSearchVectorWithPhonesCommand extends ActiveOrSuspe
@InjectDataSource()
private readonly coreDataSource: DataSource,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -7,6 +7,7 @@ import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-v
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
@@ -27,6 +28,7 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
ServerlessFunctionEntity,
ServerlessFunctionLayerEntity,
]),
DataSourceModule,
IndexMetadataModule,
WorkspaceMigrationRunnerModule,
WorkspaceMigrationModule,
@@ -9,6 +9,7 @@ import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@Module({
@@ -20,6 +21,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
V1_10_UpgradeVersionCommandModule,
V1_11_UpgradeVersionCommandModule,
V1_12_UpgradeVersionCommandModule,
DataSourceModule,
WorkspaceSyncMetadataModule,
],
providers: [UpgradeCommand],
@@ -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';
@@ -33,6 +35,7 @@ import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/comma
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
@@ -48,6 +51,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyConfigService: TwentyConfigService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
// 1.6 Commands
@@ -84,11 +88,14 @@ 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,
twentyConfigService,
twentyORMGlobalManager,
dataSourceService,
syncWorkspaceMetadataCommand,
);
@@ -143,6 +150,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: () => ({
@@ -34,6 +34,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
@@ -56,6 +57,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
UserWorkspaceEntity,
FeatureFlagEntity,
]),
DataSourceModule,
],
providers: [
BillingSubscriptionService,
@@ -6,13 +6,12 @@ import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
@@ -27,8 +26,9 @@ export class BillingSyncCustomerDataCommand extends ActiveOrSuspendedWorkspacesM
@InjectRepository(BillingCustomerEntity)
protected readonly billingCustomerRepository: Repository<BillingCustomerEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -6,14 +6,13 @@ import { Command, Option } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
@@ -33,8 +32,9 @@ export class BillingUpdateSubscriptionPriceCommand extends ActiveOrSuspendedWork
protected readonly billingSubscriptionRepository: Repository<BillingSubscriptionEntity>,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
@Option({
@@ -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,
@@ -10,7 +10,6 @@ import {
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@@ -270,6 +269,14 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
newColumnName: toCompositeColumnName,
});
}
} else if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
await this.workspaceSchemaManagerService.columnManager.renameColumn({
queryRunner,
schemaName,
tableName,
oldColumnName: update.from,
newColumnName: update.to,
});
}
const enumOperations = collectEnumOperationsForField({
@@ -411,33 +418,4 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
});
}
}
private async handleMorphOrRelationSettingsUpdate({
queryRunner,
schemaName,
tableName,
update,
}: UpdateFieldPropertyUpdateHandlerArgs<
'settings',
MorphOrRelationFieldMetadataType
>) {
const fromJoinColumnName = update.from.joinColumnName;
const toJoinColumnName = update.to.joinColumnName;
if (
!isDefined(fromJoinColumnName) ||
!isDefined(toJoinColumnName) ||
fromJoinColumnName === toJoinColumnName
) {
return;
}
await this.workspaceSchemaManagerService.columnManager.renameColumn({
oldColumnName: fromJoinColumnName,
newColumnName: toJoinColumnName,
queryRunner,
schemaName,
tableName,
});
}
}
@@ -3,15 +3,13 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
@@ -24,12 +22,12 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly dataSourceService: DataSourceService,
protected readonly dataSourceService: DataSourceService,
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
private readonly featureFlagService: FeatureFlagService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -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,
},
);
}
}
}
@@ -0,0 +1,33 @@
import { Injectable, Logger } from '@nestjs/common';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailEmailAliasErrorHandlerService {
private readonly logger = new Logger(GmailEmailAliasErrorHandlerService.name);
constructor() {}
public handleError(error: unknown): void {
this.logger.error(`Google: Error getting email aliases: ${error}`);
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
if (isGmailApiError(error)) {
throw parseGmailApiError(error);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { google } from 'googleapis';
import { GmailEmailAliasErrorHandlerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-error-handler.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -9,6 +10,7 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco
export class GoogleEmailAliasManagerService {
constructor(
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly gmailEmailAliasErrorHandlerService: GmailEmailAliasErrorHandlerService,
) {}
public async getHandleAliases(
@@ -24,10 +26,14 @@ export class GoogleEmailAliasManagerService {
auth: oAuth2Client,
});
const emailsResponse = await peopleClient.people.get({
resourceName: 'people/me',
personFields: 'emailAddresses',
});
const emailsResponse = await peopleClient.people
.get({
resourceName: 'people/me',
personFields: 'emailAddresses',
})
.catch((error) => {
throw this.gmailEmailAliasErrorHandlerService.handleError(error);
});
const emailAddresses = emailsResponse.data.emailAddresses;
@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service';
import { GmailEmailAliasErrorHandlerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-error-handler.service';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
@@ -10,6 +11,7 @@ import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-
providers: [
EmailAliasManagerService,
GoogleEmailAliasManagerService,
GmailEmailAliasErrorHandlerService,
MicrosoftEmailAliasManagerService,
],
exports: [EmailAliasManagerService],
@@ -4,9 +4,9 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { type Repository } from 'typeorm';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
import { microsoftGraphMeResponseWithProxyAddresses } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/mocks/microsoft-api-examples';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -4,8 +4,8 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
@@ -11,7 +11,7 @@ import {
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { isAxiosTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-axios-gaxios-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
export type ConnectedAccountTokens = {
accessToken: string;
@@ -142,7 +142,7 @@ export class ConnectedAccountRefreshTokensService {
);
}
} catch (error) {
if (isAxiosTemporaryError(error)) {
if (isGmailNetworkError(error)) {
throw new ConnectedAccountRefreshAccessTokenException(
`Error refreshing tokens for connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}: ${error.code}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
@@ -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';
@@ -3,11 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
@@ -21,8 +20,9 @@ export class MessagingMessageCleanerRemoveOrphansCommand extends ActiveOrSuspend
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly messagingMessageCleanerService: MessagingMessageCleanerService,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
@@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { MessagingMessageCleanerRemoveOrphansCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command';
import { MessagingConnectedAccountDeletionCleanupJob } from 'src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job';
import { MessagingMessageCleanerConnectedAccountListener } from 'src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
@Module({
imports: [TypeOrmModule.forFeature([WorkspaceEntity])],
imports: [TypeOrmModule.forFeature([WorkspaceEntity]), DataSourceModule],
providers: [
MessagingMessageCleanerService,
MessagingConnectedAccountDeletionCleanupJob,
@@ -0,0 +1,33 @@
import { Injectable, Logger } from '@nestjs/common';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailFoldersErrorHandlerService {
private readonly logger = new Logger(GmailFoldersErrorHandlerService.name);
constructor() {}
public handleError(error: unknown): void {
this.logger.error(`Gmail: Error fetching folders: ${error}`);
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
if (isGmailApiError(error)) {
throw parseGmailApiError(error);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -10,11 +10,11 @@ import {
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
import { extractGmailFolderName } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util';
import { getGmailFolderParentId } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
import { MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-not-synced-labels';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
@Injectable()
export class GmailGetAllFoldersService implements MessageFolderDriver {
@@ -22,7 +22,7 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
constructor(
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly gmailMessageListFetchErrorHandler: GmailMessageListFetchErrorHandler,
private readonly gmailFoldersErrorHandlerService: GmailFoldersErrorHandlerService,
) {}
async getAllMessageFolders(
@@ -53,7 +53,7 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
`Connected account ${connectedAccount.id}: Error fetching labels: ${error.message}`,
);
this.gmailMessageListFetchErrorHandler.handleError(error);
this.gmailFoldersErrorHandlerService.handleError(error);
return { data: { labels: [] } };
});
@@ -6,9 +6,10 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
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';
@@ -28,6 +29,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im
providers: [
SyncMessageFoldersService,
GmailGetAllFoldersService,
GmailFoldersErrorHandlerService,
ImapGetAllFoldersService,
MicrosoftGetAllFoldersService,
],
@@ -14,9 +14,9 @@ import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders';
type SyncMessageFoldersInput = {
@@ -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,
},
});
}
}
}
}
@@ -17,7 +17,6 @@ import { GmailGetMessageListService } from 'src/modules/messaging/message-import
import { GmailGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
import { GmailNetworkErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-network-error-handler.service';
import { MessageParticipantManagerModule } from 'src/modules/messaging/message-participant-manager/message-participant-manager.module';
@Module({
@@ -40,14 +39,12 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
GmailFetchByBatchService,
GmailGetMessagesService,
GmailGetMessageListService,
GmailNetworkErrorHandler,
GmailMessageListFetchErrorHandler,
GmailMessagesImportErrorHandler,
],
exports: [
GmailGetMessagesService,
GmailGetMessageListService,
GmailNetworkErrorHandler,
GmailMessageListFetchErrorHandler,
GmailMessagesImportErrorHandler,
],
@@ -1,196 +1,70 @@
// Gmail API Error Response Mocks for users.messages.list
import { type GmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type';
const gmailApiErrorMocks = {
// 400 Bad Request - Invalid query parameters
badRequest: {
error: {
code: 400,
errors: [
{
domain: 'global',
location: 'orderBy',
locationType: 'parameter',
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
reason: 'badRequest',
},
],
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
},
code: '400',
message: 'badRequest',
},
// 400 Invalid Grant
invalidGrant: {
error: {
code: 400,
errors: [
{
domain: 'global',
reason: 'invalid_grant',
message: 'Invalid Credentials',
},
],
message: 'Invalid Credentials',
},
code: '400',
message: 'invalid_grant',
},
// 400 Failed Precondition
failedPrecondition: {
error: {
code: 400,
errors: [
{
domain: 'global',
reason: 'failedPrecondition',
message: 'Failed Precondition',
},
],
message: 'Failed Precondition',
},
code: '400',
message: 'failedPrecondition',
},
// 401 Invalid Credentials
invalidCredentials: {
error: {
errors: [
{
domain: 'global',
reason: 'authError',
message: 'Invalid Credentials',
locationType: 'header',
location: 'Authorization',
},
],
code: 401,
message: 'Invalid Credentials',
},
code: '401',
message: 'authError',
},
// 404 Not Found
notFound: {
error: {
errors: [
{
domain: 'global',
reason: 'notFound',
message: 'Resource not found: userId',
location: 'userId',
locationType: 'parameter',
},
],
code: 404,
message: 'Resource not found: userId',
},
code: '404',
message: 'notFound',
},
// 410 Gone
gone: {
error: {
errors: [
{
domain: 'global',
reason: 'resourceGone',
message: 'Resource has been deleted',
location: 'messageId',
locationType: 'parameter',
},
],
code: 410,
message: 'Resource has been deleted',
},
code: '410',
message: 'resourceGone',
},
// 403 Daily Limit Exceeded
dailyLimitExceeded: {
error: {
errors: [
{
domain: 'usageLimits',
reason: 'dailyLimitExceeded',
message: 'Daily Limit Exceeded',
},
],
code: 403,
message: 'Daily Limit Exceeded',
},
code: '403',
message: 'dailyLimitExceeded',
},
// 403 User Rate Limit Exceeded
userRateLimitExceeded: {
error: {
errors: [
{
domain: 'usageLimits',
reason: 'userRateLimitExceeded',
message: 'User Rate Limit Exceeded',
},
],
code: 403,
message: 'User Rate Limit Exceeded',
},
code: '403',
message: 'userRateLimitExceeded',
},
// 403 Rate Limit Exceeded
rateLimitExceeded: {
error: {
errors: [
{
domain: 'usageLimits',
reason: 'rateLimitExceeded',
message: 'Rate Limit Exceeded',
},
],
code: 403,
message: 'Rate Limit Exceeded',
},
code: '403',
message: 'rateLimitExceeded',
},
// 403 Domain Policy Error
domainPolicyError: {
error: {
errors: [
{
domain: 'global',
reason: 'domainPolicy',
message: 'The domain administrators have disabled Gmail apps.',
},
],
code: 403,
message: 'The domain administrators have disabled Gmail apps.',
},
code: '403',
message: 'domainPolicy',
},
// 429 Too Many Requests (Concurrent Requests)
tooManyConcurrentRequests: {
error: {
errors: [
{
domain: 'global',
reason: 'rateLimitExceeded',
message: 'Too many concurrent requests for user',
},
],
code: 429,
message: 'Too many concurrent requests for user',
},
code: '429',
message: 'tooManyConcurrentRequests',
},
// 500 Backend Error
backendError: {
error: {
errors: [
{
domain: 'global',
reason: 'backendError',
message: 'Backend Error',
},
],
code: 500,
message: 'Backend Error',
},
code: '500',
message: 'backendError',
},
getError: function (code: number, type?: string) {
getError: function (code: number, type?: string): GmailApiError {
switch (code) {
case 400:
switch (type) {
@@ -0,0 +1,217 @@
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
const gmailBatchApiErrorMocks = {
// 400 Bad Request - Invalid query parameters
badRequest: {
code: 400,
errors: [
{
domain: 'global',
location: 'orderBy',
locationType: 'parameter',
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
reason: 'badRequest',
},
],
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
},
// 400 Invalid Grant
invalidGrant: {
code: 400,
errors: [
{
domain: 'global',
reason: 'invalid_grant',
message: 'Invalid Credentials',
},
],
message: 'Invalid Credentials',
},
// 400 Failed Precondition
failedPrecondition: {
code: 400,
errors: [
{
domain: 'global',
reason: 'failedPrecondition',
message: 'Failed Precondition',
},
],
message: 'Failed Precondition',
},
// 401 Invalid Credentials
invalidCredentials: {
errors: [
{
domain: 'global',
reason: 'authError',
message: 'Invalid Credentials',
locationType: 'header',
location: 'Authorization',
},
],
code: 401,
message: 'Invalid Credentials',
},
// 404 Not Found
notFound: {
errors: [
{
domain: 'global',
reason: 'notFound',
message: 'Resource not found: userId',
location: 'userId',
locationType: 'parameter',
},
],
code: 404,
message: 'Resource not found: userId',
},
// 410 Gone
gone: {
errors: [
{
domain: 'global',
reason: 'resourceGone',
message: 'Resource has been deleted',
location: 'messageId',
locationType: 'parameter',
},
],
code: 410,
message: 'Resource has been deleted',
},
// 403 Daily Limit Exceeded
dailyLimitExceeded: {
errors: [
{
domain: 'usageLimits',
reason: 'dailyLimitExceeded',
message: 'Daily Limit Exceeded',
},
],
code: 403,
message: 'Daily Limit Exceeded',
},
// 403 User Rate Limit Exceeded
userRateLimitExceeded: {
errors: [
{
domain: 'usageLimits',
reason: 'userRateLimitExceeded',
message: 'User Rate Limit Exceeded',
},
],
code: 403,
message: 'User Rate Limit Exceeded',
},
// 403 Rate Limit Exceeded
rateLimitExceeded: {
errors: [
{
domain: 'usageLimits',
reason: 'rateLimitExceeded',
message: 'Rate Limit Exceeded',
},
],
code: 403,
message: 'Rate Limit Exceeded',
},
// 403 Domain Policy Error
domainPolicyError: {
errors: [
{
domain: 'global',
reason: 'domainPolicy',
message: 'The domain administrators have disabled Gmail apps.',
},
],
code: 403,
message: 'The domain administrators have disabled Gmail apps.',
},
// 429 Too Many Requests (Concurrent Requests)
tooManyConcurrentRequests: {
errors: [
{
domain: 'global',
reason: 'rateLimitExceeded',
message: 'Too many concurrent requests for user',
},
],
code: 429,
message: 'Too many concurrent requests for user',
},
// 500 Backend Error
backendError: {
errors: [
{
domain: 'global',
reason: 'backendError',
message: 'Backend Error',
},
],
code: 500,
message: 'Backend Error',
},
getError: function (code: number, type?: string): GmailApiBatchError {
switch (code) {
case 400:
switch (type) {
case 'invalid_grant':
return this.invalidGrant;
case 'failedPrecondition':
return this.failedPrecondition;
default:
return this.badRequest;
}
case 401:
return this.invalidCredentials;
case 403:
switch (type) {
case 'dailyLimit':
return this.dailyLimitExceeded;
case 'userRateLimit':
return this.userRateLimitExceeded;
case 'rateLimit':
return this.rateLimitExceeded;
case 'domainPolicy':
return this.domainPolicyError;
default:
return this.rateLimitExceeded;
}
case 404:
return this.notFound;
case 410:
return this.gone;
case 429:
switch (type) {
case 'concurrent':
return this.tooManyConcurrentRequests;
case 'mailSending':
return this.mailSendingLimitExceeded;
default:
return this.tooManyConcurrentRequests;
}
case 500:
return this.backendError;
default:
throw new Error(`Unknown error code: ${code}`);
}
},
};
export default gmailBatchApiErrorMocks;
@@ -1,26 +1,33 @@
import { Injectable, Logger } from '@nestjs/common';
import { GmailNetworkErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-network-error-handler.service';
import { parseGmailMessageListFetchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailMessageListFetchErrorHandler {
private readonly logger = new Logger(GmailMessageListFetchErrorHandler.name);
constructor(
private readonly gmailNetworkErrorHandler: GmailNetworkErrorHandler,
) {}
constructor() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleError(error: any): void {
this.logger.log(`Error fetching message list`, error);
const networkError = this.gmailNetworkErrorHandler.handleError(error);
if (networkError) {
throw networkError;
public handleError(error: unknown): void {
this.logger.error(`Gmail: Error fetching message list: ${error}`);
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
throw parseGmailMessageListFetchError(error, { cause: error });
if (isGmailApiError(error)) {
throw parseGmailApiError(error);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -1,32 +1,34 @@
import { Injectable, Logger } from '@nestjs/common';
import { GmailNetworkErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-network-error-handler.service';
import { parseGmailMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailMessagesImportErrorHandler {
private readonly logger = new Logger(GmailMessagesImportErrorHandler.name);
constructor(
private readonly gmailNetworkErrorHandler: GmailNetworkErrorHandler,
) {}
constructor() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleError(error: any, messageExternalId: string): void {
this.logger.log(`Error fetching messages`, error);
public handleError(error: unknown, messageExternalId: string): void {
this.logger.error(`Gmail: Error importing messages: ${error}`);
const networkError = this.gmailNetworkErrorHandler.handleError(error);
if (networkError) {
throw networkError;
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
const gmailError = parseGmailMessagesImportError(error, messageExternalId, {
cause: error,
});
if (gmailError) {
throw gmailError;
if (isGmailApiBatchError(error)) {
throw parseGmailApiBatchError(error, messageExternalId);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -1,23 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isAxiosTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-axios-gaxios-error.util';
@Injectable()
export class GmailNetworkErrorHandler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleError(error: any): MessageImportDriverException | null {
if (isAxiosTemporaryError(error)) {
return new MessageImportDriverException(
error.message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: error },
);
}
return null;
}
}
@@ -0,0 +1,7 @@
export type GmailApiBatchError = {
code: number;
errors: {
reason: string;
message: string;
}[];
};
@@ -0,0 +1,4 @@
export type GmailApiError = {
code: string;
message: string;
};
@@ -1,45 +1,45 @@
import gaxiosErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks';
import { isAxiosTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-axios-gaxios-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
describe('parseGaxiosError', () => {
describe('isGmailNetworkError', () => {
it('should return a MessageImportDriverException for ECONNRESET', () => {
const error = gaxiosErrorMocks.getError('ECONNRESET');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ENOTFOUND', () => {
const error = gaxiosErrorMocks.getError('ENOTFOUND');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ECONNABORTED', () => {
const error = gaxiosErrorMocks.getError('ECONNABORTED');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ETIMEDOUT', () => {
const error = gaxiosErrorMocks.getError('ETIMEDOUT');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ERR_NETWORK', () => {
const error = gaxiosErrorMocks.getError('ERR_NETWORK');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return undefined for unknown error codes', () => {
const error = { code: 'UNKNOWN_ERROR' } as any;
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(false);
});
@@ -3,12 +3,12 @@ import {
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import gmailApiErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks';
import { parseGmailMessageListFetchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
describe('parseGmailMessageListFetchError', () => {
describe('parseGmailApiError', () => {
it('should handle 400 Bad Request', () => {
const error = gmailApiErrorMocks.getError(400);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
@@ -16,7 +16,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 400 Invalid Grant', () => {
const error = gmailApiErrorMocks.getError(400, 'invalid_grant');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -26,7 +26,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 400 Failed Precondition', () => {
const error = gmailApiErrorMocks.getError(400, 'failedPrecondition');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -36,7 +36,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 401 Invalid Credentials', () => {
const error = gmailApiErrorMocks.getError(401);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -46,7 +46,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 Daily Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'dailyLimit');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -56,7 +56,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 User Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'userRateLimit');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -66,7 +66,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'rateLimit');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -76,7 +76,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 Domain Policy Error', () => {
const error = gmailApiErrorMocks.getError(403, 'domainPolicy');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -86,7 +86,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 404 as sync cursor error', () => {
const error = gmailApiErrorMocks.getError(404);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -96,7 +96,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 410 Gone', () => {
const error = gmailApiErrorMocks.getError(410);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
@@ -104,7 +104,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 429 Too Many Requests', () => {
const error = gmailApiErrorMocks.getError(429, 'concurrent');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -114,7 +114,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 500 Backend Error', () => {
const error = gmailApiErrorMocks.getError(500);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -2,48 +2,39 @@ import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import gmailApiErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks';
import { parseGmailMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util';
import { default as gmailBatchApiErrorMocks } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks';
import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util';
const messageExternalId = '123';
describe('parseGmailMessagesImportError', () => {
describe('parseGmailApiBatchError', () => {
it('should handle 400 Bad Request', () => {
const error = gmailApiErrorMocks.getError(400);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(400);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 400 Invalid Grant', () => {
const error = gmailApiErrorMocks.getError(400, 'invalid_grant');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(400, 'invalid_grant');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 400 Failed Precondition', () => {
const error = gmailApiErrorMocks.getError(400, 'failedPrecondition');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(400, 'failedPrecondition');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
@@ -52,146 +43,119 @@ describe('parseGmailMessagesImportError', () => {
});
it('should handle 401 Invalid Credentials', () => {
const error = gmailApiErrorMocks.getError(401);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(401);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 Daily Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'dailyLimit');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'dailyLimit');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 User Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'userRateLimit');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'userRateLimit');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'rateLimit');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'rateLimit');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 Domain Policy Error', () => {
const error = gmailApiErrorMocks.getError(403, 'domainPolicy');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'domainPolicy');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 404 Not Found', () => {
const error = gmailApiErrorMocks.getError(404);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(404);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 410 Gone', () => {
const error = gmailApiErrorMocks.getError(410);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(410);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 429 Too Many Requests', () => {
const error = gmailApiErrorMocks.getError(429, 'concurrent');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(429, 'concurrent');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 500 Backend Error', () => {
const error = gmailApiErrorMocks.getError(500);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(500);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
});
@@ -0,0 +1,21 @@
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
export const isGmailApiBatchError = (
error: unknown,
): error is GmailApiBatchError => {
if (error === null || typeof error !== 'object') {
return false;
}
if (
!('code' in error) ||
!('errors' in error) ||
!Array.isArray(error.errors) ||
error.errors.length === 0 ||
error.errors.some((error) => !('reason' in error) || !('message' in error))
) {
return false;
}
return true;
};
@@ -0,0 +1,18 @@
import { type GmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type';
export const isGmailApiError = (error: unknown): error is GmailApiError => {
if (error === null || typeof error !== 'object') {
return false;
}
if (
!('code' in error) ||
typeof error.code !== 'string' ||
!('message' in error) ||
typeof error.message !== 'string'
) {
return false;
}
return true;
};
@@ -2,10 +2,16 @@ import { type GaxiosError } from 'gaxios';
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
export const isAxiosTemporaryError = (error: GaxiosError): boolean => {
const { code } = error;
export const isGmailNetworkError = (error: unknown): error is GaxiosError => {
if (error === null || typeof error !== 'object') {
return false;
}
switch (code) {
if (!('code' in error)) {
return false;
}
switch (error.code) {
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNABORTED:
@@ -13,7 +19,6 @@ export const isAxiosTemporaryError = (error: GaxiosError): boolean => {
case MessageNetworkExceptionCode.ERR_NETWORK:
case MessageNetworkExceptionCode.EHOSTUNREACH:
return true;
default:
return false;
}
@@ -2,17 +2,11 @@ import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
export const parseGmailMessagesImportError = (
error: {
code?: number;
errors: {
reason: string;
message: string;
}[];
},
messageExternalId: string,
options?: { cause?: Error },
export const parseGmailApiBatchError = (
error: GmailApiBatchError,
messageExternalId?: string,
): MessageImportDriverException | undefined => {
const { code, errors } = error;
@@ -26,7 +20,6 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
if (reason === 'failedPrecondition') {
@@ -34,21 +27,18 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.UNKNOWN,
{ cause: options?.cause },
);
case 404:
@@ -56,14 +46,12 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
{ cause: options?.cause },
);
case 429:
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
case 403:
@@ -75,14 +63,12 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
if (reason === 'domainPolicy') {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
@@ -92,14 +78,12 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
case 503:
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
case 500:
@@ -109,7 +93,6 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
@@ -117,7 +100,6 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
`${code} - ${reason} - ${message}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
break;
@@ -129,6 +111,5 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.UNKNOWN,
{ cause: options?.cause },
);
};

Some files were not shown because too many files have changed in this diff Show More