Compare commits

...

10 Commits

Author SHA1 Message Date
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
64 changed files with 1160 additions and 636 deletions
@@ -3,11 +3,11 @@ import { CommandMenuAIChatThreadsPage } from '@/command-menu/pages/AIChatThreads
import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/CommandMenuAskAIPage';
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings';
import { CommandMenuPageLayoutGraphFilter } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphFilter';
import { CommandMenuPageLayoutGraphTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect';
import { CommandMenuPageLayoutIframeSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings';
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
import { CommandMenuPageLayoutTabSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings';
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
import { CommandMenuMergeRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuMergeRecordPage';
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
import { CommandMenuEditRichTextPage } from '@/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage';
@@ -48,7 +48,7 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
],
[
CommandMenuPages.PageLayoutGraphTypeSelect,
<CommandMenuPageLayoutGraphTypeSelect />,
<CommandMenuPageLayoutChartSettings />,
],
[
CommandMenuPages.PageLayoutGraphFilter,
@@ -14,7 +14,7 @@ const StyledContainer = styled.div`
height: 100%;
`;
export const CommandMenuPageLayoutGraphTypeSelect = () => {
export const CommandMenuPageLayoutChartSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const draftPageLayout = useRecoilComponentValue(
@@ -27,21 +27,12 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
pageLayoutId,
);
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
const widgetInEditMode = draftPageLayout.tabs
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === pageLayoutEditingWidgetId);
if (!isDefined(widgetInEditMode)) {
throw new Error(
`Widget with ID ${pageLayoutEditingWidgetId} not found in page layout`,
);
}
if (
!isDefined(widgetInEditMode) ||
!isDefined(widgetInEditMode.configuration) ||
!('graphType' in widgetInEditMode.configuration)
) {
@@ -1,10 +1,11 @@
import { ChartFiltersSettings } from '@/command-menu/pages/page-layout/components/ChartFiltersSettings';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const CommandMenuPageLayoutGraphFilter = () => {
@@ -24,28 +25,27 @@ export const CommandMenuPageLayoutGraphFilter = () => {
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === pageLayoutEditingWidgetId);
if (!isDefined(widgetInEditMode)) {
throw new Error(
`Widget with ID ${pageLayoutEditingWidgetId} not found in page layout`,
);
}
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
throw new Error('No data source in chart');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: widgetInEditMode.objectMetadataId,
});
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
if (!isChartWidget(widgetInEditMode)) {
if (
!isDefined(widgetInEditMode) ||
!isDefined(widgetInEditMode.objectMetadataId) ||
!isChartWidget(widgetInEditMode)
) {
return null;
}
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === widgetInEditMode?.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Object metadata item not found for id ${widgetInEditMode?.objectMetadataId}`,
);
}
return (
<>
<ChartFiltersSettings
@@ -1,3 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
@@ -24,9 +25,13 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutId,
);
const { closeCommandMenu } = useCommandMenu();
const deletePageLayoutWidget = useRecoilCallback(
({ snapshot, set }) =>
(widgetId: string) => {
closeCommandMenu();
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
@@ -53,7 +58,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
}));
}
},
[pageLayoutCurrentLayoutsState, pageLayoutDraftState],
[closeCommandMenu, pageLayoutCurrentLayoutsState, pageLayoutDraftState],
);
return { deletePageLayoutWidget };
@@ -45,23 +45,12 @@ const StyledTabList = styled(TabList)`
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledContentContainer = styled.div<{
isInRightDrawer: boolean;
}>`
background: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.background.secondary : theme.background.primary};
border: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? `1px solid ${theme.border.color.light}` : 'none'};
border-radius: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.border.radius.md : '0'};
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
flex: 1;
margin: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.spacing(3) : '0'};
overflow-y: auto;
.scroll-wrapper-y-enabled {
height: auto;
}
background: ${({ theme }) => theme.background.primary};
padding-bottom: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.spacing(16) : 0};
`;
type ShowPageSubContainerProps = {
@@ -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,
},
);
}
}
}
@@ -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,
@@ -1,35 +0,0 @@
import { Command, CommandRunner } from 'nest-commander';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import {
MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN,
MessagingProcessFolderActionsCronJob,
} from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-process-folder-actions.cron.job';
@Command({
name: 'cron:messaging:process-folder-actions',
description:
'Starts a cron job to process pending folder actions (deletion) for message channels',
})
export class MessagingProcessFolderActionsCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: MessagingProcessFolderActionsCronJob.name,
data: undefined,
options: {
repeat: {
pattern: MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN,
},
},
});
}
}
@@ -1,76 +0,0 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import {
MessagingProcessFolderActionsJob,
type MessagingProcessFolderActionsJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-process-folder-actions.job';
export const MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN = '*/15 * * * *';
@Processor(MessageQueue.cronQueue)
export class MessagingProcessFolderActionsCronJob {
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@Process(MessagingProcessFolderActionsCronJob.name)
@SentryCronMonitor(
MessagingProcessFolderActionsCronJob.name,
MESSAGING_PROCESS_FOLDER_ACTIONS_CRON_PATTERN,
)
async handle(): Promise<void> {
const activeWorkspaces = await this.workspaceRepository.find({
where: {
activationStatus: WorkspaceActivationStatus.ACTIVE,
},
});
for (const activeWorkspace of activeWorkspaces) {
try {
const schemaName = getWorkspaceSchemaName(activeWorkspace.id);
const messageChannels = await this.coreDataSource.query(
`SELECT DISTINCT mc.id
FROM ${schemaName}."messageChannel" mc
INNER JOIN ${schemaName}."messageFolder" mf ON mf."messageChannelId" = mc.id
WHERE mf."pendingSyncAction" = '${MessageFolderPendingSyncAction.FOLDER_DELETION}'`,
);
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingProcessFolderActionsJobData>(
MessagingProcessFolderActionsJob.name,
{
workspaceId: activeWorkspace.id,
messageChannelId: messageChannel.id,
},
);
}
} catch (error) {
this.exceptionHandlerService.captureExceptions([error], {
workspace: {
id: activeWorkspace.id,
},
});
}
}
}
}
@@ -1,92 +0,0 @@
import { Logger, Scope } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
export type MessagingProcessFolderActionsJobData = {
workspaceId: string;
messageChannelId: string;
};
@Processor({
queueName: MessageQueue.messagingQueue,
scope: Scope.REQUEST,
})
export class MessagingProcessFolderActionsJob {
private readonly logger = new Logger(MessagingProcessFolderActionsJob.name);
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly messagingProcessFolderActionsService: MessagingProcessFolderActionsService,
) {}
@Process(MessagingProcessFolderActionsJob.name)
async handle(data: MessagingProcessFolderActionsJobData): Promise<void> {
const { workspaceId, messageChannelId } = data;
this.logger.log(
`Processing pending folder actions for message channel ${messageChannelId} in workspace ${workspaceId}`,
);
const messageChannelRepository =
await this.twentyORMManager.getRepository<MessageChannelWorkspaceEntity>(
'messageChannel',
);
const messageChannel = await messageChannelRepository.findOne({
where: {
id: messageChannelId,
},
});
if (!messageChannel) {
this.logger.warn(
`Message channel ${messageChannelId} not found in workspace ${workspaceId}`,
);
return;
}
const messageFolderRepository =
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
'messageFolder',
);
const messageFolders = await messageFolderRepository.find({
where: {
messageChannelId: messageChannel.id,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
});
if (messageFolders.length === 0) {
this.logger.log(
`Message channel ${messageChannelId} has no folders with pending deletion actions, skipping`,
);
return;
}
try {
await this.messagingProcessFolderActionsService.processFolderActions(
messageChannel,
messageFolders,
workspaceId,
);
} catch (error) {
this.logger.error(
`Error processing folder actions for message channel ${messageChannelId} in workspace ${workspaceId}: ${error.message}`,
error.stack,
);
throw error;
}
}
}
@@ -18,12 +18,10 @@ import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/messa
import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command';
import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command';
import { MessagingOngoingStaleCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command';
import { MessagingProcessFolderActionsCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-process-folder-actions.cron.command';
import { MessagingRelaunchFailedMessageChannelsCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-relaunch-failed-message-channels.cron.command';
import { MessagingMessageListFetchCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job';
import { MessagingMessagesImportCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job';
import { MessagingOngoingStaleCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job';
import { MessagingProcessFolderActionsCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-process-folder-actions.cron.job';
import { MessagingRelaunchFailedMessageChannelsCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-relaunch-failed-message-channels.cron.job';
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
import { MessagingIMAPDriverModule } from 'src/modules/messaging/message-import-manager/drivers/imap/messaging-imap-driver.module';
@@ -34,7 +32,6 @@ import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-man
import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job';
import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { MessagingProcessFolderActionsJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-process-folder-actions.job';
import { MessagingRelaunchFailedMessageChannelJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job';
import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
@@ -83,18 +80,15 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingMessageListFetchCronCommand,
MessagingMessagesImportCronCommand,
MessagingOngoingStaleCronCommand,
MessagingProcessFolderActionsCronCommand,
MessagingRelaunchFailedMessageChannelsCronCommand,
MessagingSingleMessageImportCommand,
MessagingMessageListFetchJob,
MessagingMessagesImportJob,
MessagingOngoingStaleJob,
MessagingProcessFolderActionsJob,
MessagingRelaunchFailedMessageChannelJob,
MessagingMessageListFetchCronJob,
MessagingMessagesImportCronJob,
MessagingOngoingStaleCronJob,
MessagingProcessFolderActionsCronJob,
MessagingRelaunchFailedMessageChannelsCronJob,
MessagingAddSingleMessageToCacheForImportJob,
MessagingMessageImportManagerMessageChannelListener,
@@ -16,6 +16,7 @@ import { MessagingGetMessageListService } from 'src/modules/messaging/message-im
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
describe('MessagingMessageListFetchService', () => {
@@ -78,15 +79,21 @@ describe('MessagingMessageListFetchService', () => {
};
const mockMessageFolderRepository = {
find: jest.fn().mockResolvedValue([
{
id: 'inbox-folder-id',
name: 'inbox',
syncCursor: 'inbox-sync-cursor',
messageChannelId: 'microsoft-message-channel-id',
isSynced: true,
},
]),
find: jest.fn().mockImplementation(({ where }) => {
if (where?.pendingSyncAction === 'FOLDER_DELETION') {
return [];
}
return [
{
id: 'inbox-folder-id',
name: 'inbox',
syncCursor: 'inbox-sync-cursor',
messageChannelId: 'microsoft-message-channel-id',
isSynced: true,
},
];
}),
};
const module: TestingModule = await Test.createTestingModule({
@@ -238,6 +245,12 @@ describe('MessagingMessageListFetchService', () => {
processGroupEmailActions: jest.fn().mockResolvedValue(undefined),
},
},
{
provide: MessagingProcessFolderActionsService,
useValue: {
processFolderActions: jest.fn().mockResolvedValue(undefined),
},
},
],
}).compile();
@@ -30,6 +30,7 @@ import {
MessageImportSyncStep,
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
const ONE_WEEK_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
@@ -50,6 +51,7 @@ export class MessagingMessageListFetchService {
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
private readonly messagingProcessFolderActionsService: MessagingProcessFolderActionsService,
) {}
public async processMessageListFetch(
@@ -57,21 +59,17 @@ export class MessagingMessageListFetchService {
workspaceId: string,
) {
try {
if (
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION ||
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT
) {
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending group emails action before message list fetch: ${messageChannel.pendingGroupEmailsAction}`,
);
const pendingGroupEmailActionsProcessed =
await this.processPendingGroupEmailActions(messageChannel, workspaceId);
await this.messagingProcessGroupEmailActionsService.processGroupEmailActions(
messageChannel,
workspaceId,
);
if (pendingGroupEmailActionsProcessed) {
return;
}
const pendingFolderActionsProcessed =
await this.processPendingFolderActions(messageChannel, workspaceId);
if (pendingFolderActionsProcessed) {
return;
}
@@ -288,6 +286,65 @@ export class MessagingMessageListFetchService {
}
}
private async processPendingGroupEmailActions(
messageChannel: MessageChannelWorkspaceEntity,
workspaceId: string,
): Promise<boolean> {
const hasPendingGroupEmailAction =
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION ||
messageChannel.pendingGroupEmailsAction ===
MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT;
if (!hasPendingGroupEmailAction) {
return false;
}
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending group emails action before message list fetch: ${messageChannel.pendingGroupEmailsAction}`,
);
await this.messagingProcessGroupEmailActionsService.processGroupEmailActions(
messageChannel,
workspaceId,
);
return true;
}
private async processPendingFolderActions(
messageChannel: MessageChannelWorkspaceEntity,
workspaceId: string,
): Promise<boolean> {
const messageFolderRepository =
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
'messageFolder',
);
const foldersWithPendingActions = await messageFolderRepository.find({
where: {
messageChannelId: messageChannel.id,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
});
if (foldersWithPendingActions.length === 0) {
return false;
}
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending folder actions before message list fetch`,
);
await this.messagingProcessFolderActionsService.processFolderActions(
messageChannel,
foldersWithPendingActions,
workspaceId,
);
return true;
}
private async computeFullSyncMessageChannelMessageAssociationsToDelete(
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageExternalIds: string[],
@@ -53,7 +53,7 @@ export class MessagingMessagesImportService {
try {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.MESSAGES_IMPORT_PENDING
MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED
) {
return;
}
@@ -3,12 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Command, Option } from 'nest-commander';
import { LessThan, Repository } from 'typeorm';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
@Command({
@@ -22,8 +21,9 @@ export class DeleteWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrat
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
@Option({
@@ -6,6 +6,7 @@ import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.m
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
@@ -22,6 +23,7 @@ import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runne
RecordPositionModule,
CacheLockModule,
MetricsModule,
DataSourceModule,
],
providers: [
WorkflowRunWorkspaceService,