Remove sync metadata from upgrade (#16491)
# Introduction Related https://github.com/twentyhq/core-team-issues/issues/1910 From now on the upgrade won't integrate any sync metadata as it's going to be deprecated very soon Any updates to about to removes workspace-entity or standard flat entity will require a dedicated upgrade command, what we've already started doing during the 1.13 sprint, until we have totally migrated the v2 to be workspace agnostic
This commit is contained in:
+7
-78
@@ -13,31 +13,18 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
|||||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
|
||||||
|
|
||||||
class BasicUpgradeCommandRunner extends UpgradeCommandRunner {
|
class BasicUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||||
allCommands = {
|
allCommands = {
|
||||||
'1.0.0': {
|
'1.0.0': [],
|
||||||
beforeSyncMetadata: [],
|
'2.0.0': [],
|
||||||
afterSyncMetadata: [],
|
|
||||||
},
|
|
||||||
'2.0.0': {
|
|
||||||
beforeSyncMetadata: [],
|
|
||||||
afterSyncMetadata: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidUpgradeCommandRunner extends UpgradeCommandRunner {
|
class InvalidUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||||
allCommands = {
|
allCommands = {
|
||||||
invalid: {
|
invalid: [],
|
||||||
beforeSyncMetadata: [],
|
'2.0.0': [],
|
||||||
afterSyncMetadata: [],
|
|
||||||
},
|
|
||||||
'2.0.0': {
|
|
||||||
beforeSyncMetadata: [],
|
|
||||||
afterSyncMetadata: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +73,12 @@ const buildUpgradeCommandModule = async ({
|
|||||||
twentyConfigService: TwentyConfigService,
|
twentyConfigService: TwentyConfigService,
|
||||||
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||||
dataSourceService: DataSourceService,
|
dataSourceService: DataSourceService,
|
||||||
syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
|
||||||
) => {
|
) => {
|
||||||
return new commandRunner(
|
return new commandRunner(
|
||||||
workspaceRepository,
|
workspaceRepository,
|
||||||
twentyConfigService,
|
twentyConfigService,
|
||||||
globalWorkspaceOrmManager,
|
globalWorkspaceOrmManager,
|
||||||
dataSourceService,
|
dataSourceService,
|
||||||
syncWorkspaceMetadataCommand,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
inject: [
|
inject: [
|
||||||
@@ -101,7 +86,6 @@ const buildUpgradeCommandModule = async ({
|
|||||||
TwentyConfigService,
|
TwentyConfigService,
|
||||||
GlobalWorkspaceOrmManager,
|
GlobalWorkspaceOrmManager,
|
||||||
DataSourceService,
|
DataSourceService,
|
||||||
SyncWorkspaceMetadataCommand,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,12 +130,6 @@ const buildUpgradeCommandModule = async ({
|
|||||||
provide: DataSourceService,
|
provide: DataSourceService,
|
||||||
useValue: mockDataSourceService,
|
useValue: mockDataSourceService,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: SyncWorkspaceMetadataCommand,
|
|
||||||
useValue: {
|
|
||||||
runOnWorkspace: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@@ -161,9 +139,6 @@ const buildUpgradeCommandModule = async ({
|
|||||||
describe('UpgradeCommandRunner', () => {
|
describe('UpgradeCommandRunner', () => {
|
||||||
let upgradeCommandRunner: BasicUpgradeCommandRunner;
|
let upgradeCommandRunner: BasicUpgradeCommandRunner;
|
||||||
let workspaceRepository: Repository<WorkspaceEntity>;
|
let workspaceRepository: Repository<WorkspaceEntity>;
|
||||||
let syncWorkspaceMetadataCommand: jest.Mocked<SyncWorkspaceMetadataCommand>;
|
|
||||||
let runAfterSyncMetadataSpy: jest.SpyInstance;
|
|
||||||
let runBeforeSyncMetadataSpy: jest.SpyInstance;
|
|
||||||
let runCoreMigrationsSpy: jest.SpyInstance;
|
let runCoreMigrationsSpy: jest.SpyInstance;
|
||||||
let globalWorkspaceOrmManagerSpy: GlobalWorkspaceOrmManager;
|
let globalWorkspaceOrmManagerSpy: GlobalWorkspaceOrmManager;
|
||||||
|
|
||||||
@@ -201,14 +176,6 @@ describe('UpgradeCommandRunner', () => {
|
|||||||
jest.spyOn(upgradeCommandRunner['logger'], 'error').mockImplementation();
|
jest.spyOn(upgradeCommandRunner['logger'], 'error').mockImplementation();
|
||||||
jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation();
|
jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation();
|
||||||
|
|
||||||
runBeforeSyncMetadataSpy = jest.spyOn(
|
|
||||||
upgradeCommandRunner,
|
|
||||||
'runBeforeSyncMetadata',
|
|
||||||
);
|
|
||||||
runAfterSyncMetadataSpy = jest.spyOn(
|
|
||||||
upgradeCommandRunner,
|
|
||||||
'runAfterSyncMetadata',
|
|
||||||
);
|
|
||||||
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
|
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
|
||||||
runCoreMigrationsSpy = jest
|
runCoreMigrationsSpy = jest
|
||||||
.spyOn(upgradeCommandRunner, 'runCoreMigrations')
|
.spyOn(upgradeCommandRunner, 'runCoreMigrations')
|
||||||
@@ -217,7 +184,6 @@ describe('UpgradeCommandRunner', () => {
|
|||||||
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
|
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
|
||||||
getRepositoryToken(WorkspaceEntity),
|
getRepositoryToken(WorkspaceEntity),
|
||||||
);
|
);
|
||||||
syncWorkspaceMetadataCommand = module.get(SyncWorkspaceMetadataCommand);
|
|
||||||
globalWorkspaceOrmManagerSpy = module.get<GlobalWorkspaceOrmManager>(
|
globalWorkspaceOrmManagerSpy = module.get<GlobalWorkspaceOrmManager>(
|
||||||
GlobalWorkspaceOrmManager,
|
GlobalWorkspaceOrmManager,
|
||||||
);
|
);
|
||||||
@@ -253,12 +219,9 @@ describe('UpgradeCommandRunner', () => {
|
|||||||
upgradeCommandRunner.runOnWorkspace,
|
upgradeCommandRunner.runOnWorkspace,
|
||||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
[
|
[workspaceRepository.update].forEach((fn) =>
|
||||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
expect(fn).not.toHaveBeenCalled(),
|
||||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
);
|
||||||
upgradeCommandRunner.runAfterSyncMetadata,
|
|
||||||
workspaceRepository.update,
|
|
||||||
].forEach((fn) => expect(fn).not.toHaveBeenCalled());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run upgrade over several workspaces', async () => {
|
it('should run upgrade over several workspaces', async () => {
|
||||||
@@ -278,9 +241,6 @@ describe('UpgradeCommandRunner', () => {
|
|||||||
|
|
||||||
[
|
[
|
||||||
upgradeCommandRunner.runOnWorkspace,
|
upgradeCommandRunner.runOnWorkspace,
|
||||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
|
||||||
upgradeCommandRunner.runAfterSyncMetadata,
|
|
||||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
|
||||||
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
|
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
|
||||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace));
|
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace));
|
||||||
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
|
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
|
||||||
@@ -292,35 +252,6 @@ describe('UpgradeCommandRunner', () => {
|
|||||||
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run syncMetadataCommand betweensuccessful beforeSyncMetadataUpgradeCommandsToRun and afterSyncMetadataUpgradeCommandsToRun', async () => {
|
|
||||||
await buildModuleAndSetupSpies({});
|
|
||||||
// @ts-expect-error legacy noImplicitAny
|
|
||||||
const passedParams = [];
|
|
||||||
const options = {};
|
|
||||||
|
|
||||||
// @ts-expect-error legacy noImplicitAny
|
|
||||||
await upgradeCommandRunner.run(passedParams, options);
|
|
||||||
|
|
||||||
[
|
|
||||||
upgradeCommandRunner.runOnWorkspace,
|
|
||||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
|
||||||
upgradeCommandRunner.runAfterSyncMetadata,
|
|
||||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
|
||||||
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
|
|
||||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
|
||||||
|
|
||||||
// Verify order of execution
|
|
||||||
const beforeSyncCall = runBeforeSyncMetadataSpy.mock.invocationCallOrder[0];
|
|
||||||
const afterSyncCall = runAfterSyncMetadataSpy.mock.invocationCallOrder[0];
|
|
||||||
const syncMetadataCall =
|
|
||||||
syncWorkspaceMetadataCommand.runOnWorkspace.mock.invocationCallOrder[0];
|
|
||||||
|
|
||||||
expect(beforeSyncCall).toBeLessThan(syncMetadataCall);
|
|
||||||
expect(syncMetadataCall).toBeLessThan(afterSyncCall);
|
|
||||||
expect(upgradeCommandRunner.migrationReport.success.length).toBe(1);
|
|
||||||
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Workspace upgrade should succeed ', () => {
|
describe('Workspace upgrade should succeed ', () => {
|
||||||
const successfulTestUseCases: EachTestingContext<{
|
const successfulTestUseCases: EachTestingContext<{
|
||||||
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
|
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
|
||||||
@@ -379,8 +310,6 @@ describe('UpgradeCommandRunner', () => {
|
|||||||
expect(failReport.length).toBe(0);
|
expect(failReport.length).toBe(0);
|
||||||
expect(successReport.length).toBe(1);
|
expect(successReport.length).toBe(1);
|
||||||
expect(runCoreMigrationsSpy).toHaveBeenCalledTimes(1);
|
expect(runCoreMigrationsSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(runAfterSyncMetadataSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(runBeforeSyncMetadataSpy).toHaveBeenCalledTimes(1);
|
|
||||||
const { workspaceId } = successReport[0];
|
const { workspaceId } = successReport[0];
|
||||||
|
|
||||||
expect(workspaceId).toBe('workspace_0');
|
expect(workspaceId).toBe('workspace_0');
|
||||||
|
|||||||
+8
-28
@@ -21,23 +21,16 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
|||||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
|
||||||
import {
|
import {
|
||||||
type CompareVersionMajorAndMinorReturnType,
|
type CompareVersionMajorAndMinorReturnType,
|
||||||
compareVersionMajorAndMinor,
|
compareVersionMajorAndMinor,
|
||||||
} from 'src/utils/version/compare-version-minor-and-major';
|
} from 'src/utils/version/compare-version-minor-and-major';
|
||||||
import { getPreviousVersion } from 'src/utils/version/get-previous-version';
|
import { getPreviousVersion } from 'src/utils/version/get-previous-version';
|
||||||
|
|
||||||
export type VersionCommands = {
|
export type VersionCommands = (
|
||||||
beforeSyncMetadata: (
|
| WorkspacesMigrationCommandRunner
|
||||||
| WorkspacesMigrationCommandRunner
|
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
||||||
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
)[];
|
||||||
)[];
|
|
||||||
afterSyncMetadata: (
|
|
||||||
| WorkspacesMigrationCommandRunner
|
|
||||||
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
|
||||||
)[];
|
|
||||||
};
|
|
||||||
export type AllCommands = Record<string, VersionCommands>;
|
export type AllCommands = Record<string, VersionCommands>;
|
||||||
const execPromise = promisify(exec);
|
const execPromise = promisify(exec);
|
||||||
|
|
||||||
@@ -54,7 +47,6 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
|
|||||||
protected readonly twentyConfigService: TwentyConfigService,
|
protected readonly twentyConfigService: TwentyConfigService,
|
||||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||||
protected readonly dataSourceService: DataSourceService,
|
protected readonly dataSourceService: DataSourceService,
|
||||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
|
||||||
) {
|
) {
|
||||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||||
}
|
}
|
||||||
@@ -184,7 +176,7 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
|
|||||||
'Initialized upgrade context with:',
|
'Initialized upgrade context with:',
|
||||||
`- currentVersion (migrating to): ${currentAppVersion}`,
|
`- currentVersion (migrating to): ${currentAppVersion}`,
|
||||||
`- fromWorkspaceVersion: ${previousVersion}`,
|
`- fromWorkspaceVersion: ${previousVersion}`,
|
||||||
`- ${this.commands.beforeSyncMetadata.length + this.commands.afterSyncMetadata.length} commands`,
|
`- ${this.commands.length} commands`,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.logger.log(chalk.blue(message.join('\n ')));
|
this.logger.log(chalk.blue(message.join('\n ')));
|
||||||
@@ -269,9 +261,9 @@ If any workspaces are not on the previous minor version, roll back to that versi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'equal': {
|
case 'equal': {
|
||||||
await this.runBeforeSyncMetadata(args);
|
for (const command of this.commands) {
|
||||||
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
await command.runOnWorkspace(args);
|
||||||
await this.runAfterSyncMetadata(args);
|
}
|
||||||
|
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await this.workspaceRepository.update(
|
await this.workspaceRepository.update(
|
||||||
@@ -303,18 +295,6 @@ If any workspaces are not on the previous minor version, roll back to that versi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly runBeforeSyncMetadata = async (args: RunOnWorkspaceArgs) => {
|
|
||||||
for (const command of this.commands.beforeSyncMetadata) {
|
|
||||||
await command.runOnWorkspace(args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public readonly runAfterSyncMetadata = async (args: RunOnWorkspaceArgs) => {
|
|
||||||
for (const command of this.commands.afterSyncMetadata) {
|
|
||||||
await command.runOnWorkspace(args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private retrieveCurrentAppVersion() {
|
private retrieveCurrentAppVersion() {
|
||||||
const appVersion = this.twentyConfigService.get('APP_VERSION');
|
const appVersion = this.twentyConfigService.get('APP_VERSION');
|
||||||
|
|
||||||
|
|||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
|
||||||
import { WORKFLOW_RUN_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
||||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:add-workflow-run-stop-statuses',
|
|
||||||
description: 'Add stopped and stopping statuses to workflow runs',
|
|
||||||
})
|
|
||||||
export class AddWorkflowRunStopStatusesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(FieldMetadataEntity)
|
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Adding stopped and stopping statuses to workflow runs for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowRunStatusFieldMetadata =
|
|
||||||
await this.fieldMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.status,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!workflowRunStatusFieldMetadata) {
|
|
||||||
this.logger.error(
|
|
||||||
`Workflow run status field metadata not found for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowRunStatusFieldMetadataOptions =
|
|
||||||
workflowRunStatusFieldMetadata.options;
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would add stopped and stopping statuses to workflow run status field metadata for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const workflowRunStatusFieldMetadataOptionsSpecificStoppingAndStoppedTreatment =
|
|
||||||
workflowRunStatusFieldMetadataOptions?.filter(
|
|
||||||
(option) =>
|
|
||||||
option.value !== WorkflowRunStatus.STOPPED &&
|
|
||||||
option.value !== WorkflowRunStatus.STOPPING,
|
|
||||||
) as FieldMetadataComplexOption[];
|
|
||||||
|
|
||||||
workflowRunStatusFieldMetadataOptionsSpecificStoppingAndStoppedTreatment?.push(
|
|
||||||
{
|
|
||||||
id: v4(),
|
|
||||||
value: WorkflowRunStatus.STOPPING,
|
|
||||||
label: 'Stopping',
|
|
||||||
position: 5,
|
|
||||||
color: 'orange',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
workflowRunStatusFieldMetadataOptionsSpecificStoppingAndStoppedTreatment?.push(
|
|
||||||
{
|
|
||||||
id: v4(),
|
|
||||||
value: WorkflowRunStatus.STOPPED,
|
|
||||||
label: 'Stopped',
|
|
||||||
position: 6,
|
|
||||||
color: 'gray',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.fieldMetadataRepository.save({
|
|
||||||
...workflowRunStatusFieldMetadata,
|
|
||||||
options:
|
|
||||||
workflowRunStatusFieldMetadataOptionsSpecificStoppingAndStoppedTreatment,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Stopped and stopping statuses added to workflow run status field metadata for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
workflowRunStatusFieldMetadataOptions?.some(
|
|
||||||
(option) =>
|
|
||||||
option.value === WorkflowRunStatus.STOPPED ||
|
|
||||||
option.value === WorkflowRunStatus.STOPPING,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.logger.log(
|
|
||||||
`We do not want to re-run the workspace command for those already added`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would try to add stopped and stopping statuses to workflow run status enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."workflowRun_status_enum" ADD VALUE 'STOPPING'`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
`Stopping status added to workflow run status enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."workflowRun_status_enum" ADD VALUE 'STOPPED'`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
`Stopped status added to workflow run status enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error adding stopped and stopping statuses to workflow run status enum for workspace ${workspaceId}: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { In, IsNull, Not, 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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
|
||||||
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:clean-orphaned-kanban-aggregate-operation-field-metadata-id',
|
|
||||||
description:
|
|
||||||
"Delete all kanbanAggregateOperationFieldMetadataId in views that don't map to a real fieldMetadata (this is because we want to introduce a FK later)",
|
|
||||||
})
|
|
||||||
export class CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(ViewEntity)
|
|
||||||
private readonly viewRepository: Repository<ViewEntity>,
|
|
||||||
@InjectRepository(FieldMetadataEntity)
|
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
index,
|
|
||||||
total,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`[${index + 1}/${total}] Cleaning orphaned kanbanAggregateOperationFieldMetadataId for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingFieldMetadataIds = await this.fieldMetadataRepository.find({
|
|
||||||
where: { workspaceId },
|
|
||||||
select: ['id'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingFieldMetadataIdSet = new Set(
|
|
||||||
existingFieldMetadataIds.map((fm) => fm.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewsWithOrphanedFieldMetadata = await this.viewRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
kanbanAggregateOperationFieldMetadataId: Not(IsNull()),
|
|
||||||
},
|
|
||||||
select: ['id', 'kanbanAggregateOperationFieldMetadataId', 'type'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const orphanedViews = viewsWithOrphanedFieldMetadata.filter(
|
|
||||||
(view) =>
|
|
||||||
isDefined(view.kanbanAggregateOperationFieldMetadataId) &&
|
|
||||||
!existingFieldMetadataIdSet.has(
|
|
||||||
view.kanbanAggregateOperationFieldMetadataId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (orphanedViews.length === 0) {
|
|
||||||
this.logger.log(
|
|
||||||
'No orphaned kanbanAggregateOperationFieldMetadataId references found',
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kanbanViewsToDelete = orphanedViews.filter(
|
|
||||||
(view) => view.type === ViewType.KANBAN,
|
|
||||||
);
|
|
||||||
const otherViewsToUpdate = orphanedViews.filter(
|
|
||||||
(view) => view.type !== ViewType.KANBAN,
|
|
||||||
);
|
|
||||||
|
|
||||||
let deletedCount = 0;
|
|
||||||
let updatedCount = 0;
|
|
||||||
|
|
||||||
if (!options.dryRun) {
|
|
||||||
if (kanbanViewsToDelete.length > 0) {
|
|
||||||
const kanbanViewIds = kanbanViewsToDelete.map((view) => view.id);
|
|
||||||
|
|
||||||
await this.viewRepository.delete({ id: In(kanbanViewIds) });
|
|
||||||
deletedCount = kanbanViewsToDelete.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (otherViewsToUpdate.length > 0) {
|
|
||||||
const otherViewIds = otherViewsToUpdate.map((view) => view.id);
|
|
||||||
|
|
||||||
await this.viewRepository.update(
|
|
||||||
{ id: In(otherViewIds) },
|
|
||||||
{ kanbanAggregateOperationFieldMetadataId: null },
|
|
||||||
);
|
|
||||||
updatedCount = otherViewsToUpdate.length;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
deletedCount = kanbanViewsToDelete.length;
|
|
||||||
updatedCount = otherViewsToUpdate.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`${options.dryRun ? 'DRY RUN - Would have' : ''}Deleted ${deletedCount} KANBAN views and updated ${updatedCount} other views with orphaned kanbanAggregateOperationFieldMetadataId references`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: '1-10-create-view-kanban-field-metadata-id-foreign-key-migration',
|
|
||||||
description:
|
|
||||||
'Create FK_b3cc95732479f7a1337350c398f foreign key on view kanban field metadata id column',
|
|
||||||
})
|
|
||||||
export class CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
private hasRunOnce = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
if (this.hasRunOnce) {
|
|
||||||
this.logger.log('Skipping kanban field metadata id foreign key creation');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.dryRun) {
|
|
||||||
try {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_b3cc95732479f7a1337350c398f" FOREIGN KEY ("kanbanAggregateOperationFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
'Successfully added foreign key constraint for kanbanAggregateOperationFieldMetadataId',
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
`Foreign key constraint already exists or could not be created (this is expected for subsequent workspaces): ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasRunOnce = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:flush-cache',
|
|
||||||
description: 'Flush cache for workspace',
|
|
||||||
})
|
|
||||||
export class FlushCacheCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(`Flush cache for workspace ${workspaceId}`);
|
|
||||||
await this.workspaceCacheStorageService.flush(workspaceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:make-sure-dashboard-naming-available',
|
|
||||||
description: 'Make sure the dashboard naming is available',
|
|
||||||
})
|
|
||||||
export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(ObjectMetadataEntity)
|
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
private readonly objectMetadataService: ObjectMetadataService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
const potentialCustomDashboardObjectMetadata =
|
|
||||||
await this.objectMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
nameSingular: 'dashboard',
|
|
||||||
workspaceId,
|
|
||||||
isCustom: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(potentialCustomDashboardObjectMetadata)) {
|
|
||||||
this.logger.log(
|
|
||||||
`No custom dashboard object metadata found for workspace ${workspaceId}. Skipping...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would have updated the dashboard object metadata for workspace ${workspaceId}. Skipping...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Updating the dashboard object metadata for workspace ${workspaceId}...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.objectMetadataService.updateOneObject({
|
|
||||||
workspaceId,
|
|
||||||
updateObjectInput: {
|
|
||||||
id: potentialCustomDashboardObjectMetadata.id,
|
|
||||||
update: {
|
|
||||||
nameSingular: 'myDashboard',
|
|
||||||
namePlural: 'myDashboards',
|
|
||||||
labelSingular: 'My Dashboard',
|
|
||||||
labelPlural: 'My Dashboards',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.logger.log(
|
|
||||||
`Updated the dashboard object metadata for workspace ${workspaceId}...`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
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 { 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:migrate-attachment-author-to-created-by',
|
|
||||||
description:
|
|
||||||
'Migrate attachment author field data to createdBy composite field',
|
|
||||||
})
|
|
||||||
export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Migrating attachment author to createdBy for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const attachmentRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepository<AttachmentWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'attachment',
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceMemberRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'workspaceMember',
|
|
||||||
);
|
|
||||||
|
|
||||||
const attachments = await attachmentRepository.find({
|
|
||||||
where: {
|
|
||||||
authorId: Not(IsNull()),
|
|
||||||
createdBy: {
|
|
||||||
workspaceMemberId: IsNull(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: ['id', 'authorId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
let migratedCount = 0;
|
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
|
||||||
const { id, authorId } = attachment;
|
|
||||||
|
|
||||||
if (!isDefined(authorId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
|
||||||
where: { id: authorId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(workspaceMember)) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Workspace member ${authorId} not found for attachment ${id}, skipping`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstName = workspaceMember.name?.firstName || '';
|
|
||||||
const lastName = workspaceMember.name?.lastName || '';
|
|
||||||
const displayName =
|
|
||||||
firstName || lastName ? `${firstName} ${lastName}`.trim() : 'Unknown';
|
|
||||||
|
|
||||||
await attachmentRepository.update(
|
|
||||||
{ id },
|
|
||||||
{
|
|
||||||
createdBy: {
|
|
||||||
source: FieldActorSource.MANUAL,
|
|
||||||
workspaceMemberId: workspaceMember.id,
|
|
||||||
name: displayName,
|
|
||||||
context: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
migratedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-88
@@ -1,88 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
|
||||||
|
|
||||||
const TYPE_TO_FILE_CATEGORY_MAPPING: Record<string, string> = {
|
|
||||||
Archive: 'ARCHIVE',
|
|
||||||
Audio: 'AUDIO',
|
|
||||||
Image: 'IMAGE',
|
|
||||||
Presentation: 'PRESENTATION',
|
|
||||||
Spreadsheet: 'SPREADSHEET',
|
|
||||||
TextDocument: 'TEXT_DOCUMENT',
|
|
||||||
Video: 'VIDEO',
|
|
||||||
Other: 'OTHER',
|
|
||||||
};
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:migrate-attachment-type-to-file-category',
|
|
||||||
description:
|
|
||||||
'Migrate attachment type field data to fileCategory SELECT field',
|
|
||||||
})
|
|
||||||
export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Migrating attachment type to fileCategory for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const attachmentRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepository<AttachmentWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'attachment',
|
|
||||||
);
|
|
||||||
|
|
||||||
const attachments = await attachmentRepository.find({
|
|
||||||
select: ['id', 'type'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
let migratedCount = 0;
|
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
|
||||||
const { id, type } = attachment;
|
|
||||||
|
|
||||||
if (!isDefined(type)) {
|
|
||||||
throw new Error(`Attachment ${id} has no type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileCategory =
|
|
||||||
TYPE_TO_FILE_CATEGORY_MAPPING[type] ||
|
|
||||||
TYPE_TO_FILE_CATEGORY_MAPPING.Other;
|
|
||||||
|
|
||||||
await attachmentRepository.update(
|
|
||||||
{ id },
|
|
||||||
{
|
|
||||||
fileCategory,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
migratedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-134
@@ -1,134 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:migrate-channel-partial-full-sync-stages',
|
|
||||||
description:
|
|
||||||
'Migrate message and calendar channel partial and full sync stages',
|
|
||||||
})
|
|
||||||
export class MigrateChannelPartialFullSyncStagesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Migrating channel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
|
|
||||||
await this.migrateMessageChannelSyncStages(
|
|
||||||
workspaceId,
|
|
||||||
schemaName,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.migrateCalendarChannelSyncStages(
|
|
||||||
workspaceId,
|
|
||||||
schemaName,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Successfully migrated channel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async migrateMessageChannelSyncStages(
|
|
||||||
workspaceId: string,
|
|
||||||
schemaName: string,
|
|
||||||
options: RunOnWorkspaceArgs['options'],
|
|
||||||
): Promise<void> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let messageChannelUpdateResult: any;
|
|
||||||
|
|
||||||
const tableName = 'messageChannel';
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would migrate deprecated messageChannel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
messageChannelUpdateResult = await this.coreDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}"
|
|
||||||
SET "syncStage" = 'MESSAGE_LIST_FETCH_PENDING'
|
|
||||||
WHERE "syncStage" IN ('FULL_MESSAGE_LIST_FETCH_PENDING', 'PARTIAL_MESSAGE_LIST_FETCH_PENDING')`,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
this.logger.log(
|
|
||||||
`Error (expected) while trying to migrate messageChannel sync stages for workspace ${workspaceId}, nothing to migrate`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageChannelRowsUpdated = messageChannelUpdateResult[1] || 0;
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Migrated ${messageChannelRowsUpdated} messageChannel records from deprecated sync stages in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async migrateCalendarChannelSyncStages(
|
|
||||||
workspaceId: string,
|
|
||||||
schemaName: string,
|
|
||||||
options: RunOnWorkspaceArgs['options'],
|
|
||||||
): Promise<void> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let calendarChannelUpdateResult: any;
|
|
||||||
|
|
||||||
const tableName = 'calendarChannel';
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would migrate deprecated calendarChannel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
calendarChannelUpdateResult = await this.coreDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}"
|
|
||||||
SET "syncStage" = 'CALENDAR_EVENT_LIST_FETCH_PENDING'
|
|
||||||
WHERE "syncStage" IN ('FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING')`,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
this.logger.log(
|
|
||||||
`Error (expected) while trying to migrate calendarChannel sync stages for workspace ${workspaceId}, nothing to migrate`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const calendarChannelRowsUpdated = calendarChannelUpdateResult[1] || 0;
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Migrated ${calendarChannelRowsUpdated} calendarChannel records from deprecated sync stages in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-425
@@ -1,425 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
|
||||||
import { DataSource, In, Repository, type QueryRunner } 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 { 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_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
|
||||||
import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.workspace-entity';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
|
||||||
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
|
||||||
import { type SearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
|
||||||
import { SEARCH_FIELDS_FOR_COMPANY } from 'src/modules/company/standard-objects/company.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_DASHBOARD } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_OPPORTUNITY } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_WORKFLOW_RUNS } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_WORKFLOW_VERSIONS } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_WORKFLOWS } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
|
||||||
import { SEARCH_FIELDS_FOR_WORKSPACE_MEMBER } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
|
||||||
|
|
||||||
const STANDARD_SEARCH_EXPRESSIONS = buildStandardSearchExpressions();
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:regenerate-search-vectors',
|
|
||||||
description:
|
|
||||||
'Regenerate searchVector generated columns using unaccent-aware expressions for every searchable object',
|
|
||||||
})
|
|
||||||
export class RegenerateSearchVectorsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
|
|
||||||
@InjectRepository(ObjectMetadataEntity)
|
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
@InjectRepository(IndexMetadataEntity)
|
|
||||||
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
await this.ensureUnaccentFunctionExists();
|
|
||||||
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
const isDryRun = options.dryRun || false;
|
|
||||||
|
|
||||||
const searchableObjects = await this.fetchSearchableObjects(workspaceId);
|
|
||||||
|
|
||||||
if (searchableObjects.length === 0) {
|
|
||||||
this.logger.log(
|
|
||||||
`No searchable objects found for workspace ${workspaceId}, skipping`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectIds = searchableObjects.map((obj) => obj.id);
|
|
||||||
const allIndexes = await this.fetchIndexesForObjects(objectIds);
|
|
||||||
|
|
||||||
let queryRunner: QueryRunner | undefined;
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
queryRunner = this.coreDataSource.createQueryRunner();
|
|
||||||
await queryRunner.connect();
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
let successCount = 0;
|
|
||||||
let skipCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const object of searchableObjects) {
|
|
||||||
try {
|
|
||||||
const result = await this.regenerateSearchVectorForObject(
|
|
||||||
queryRunner,
|
|
||||||
object,
|
|
||||||
allIndexes,
|
|
||||||
schemaName,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result === 'skipped') {
|
|
||||||
skipCount++;
|
|
||||||
} else {
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorCount++;
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to regenerate search vector for ${object.nameSingular} in workspace ${workspaceId}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDryRun && queryRunner) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
this.logger.log(
|
|
||||||
`DRY RUN: Would regenerate ${successCount} search vectors, skip ${skipCount}, ${errorCount} errors - rolled back all changes`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.logger.log(
|
|
||||||
`Search vector regeneration complete for workspace ${workspaceId}: ${successCount} succeeded, ${skipCount} skipped, ${errorCount} failed out of ${searchableObjects.length} total`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (queryRunner?.isTransactionActive) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
if (queryRunner) {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchSearchableObjects(
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<ObjectMetadataEntity[]> {
|
|
||||||
return this.objectMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
isSearchable: true,
|
|
||||||
},
|
|
||||||
relations: ['fields'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchIndexesForObjects(
|
|
||||||
objectIds: string[],
|
|
||||||
): Promise<IndexMetadataEntity[]> {
|
|
||||||
return this.indexMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
objectMetadataId: In(objectIds),
|
|
||||||
},
|
|
||||||
relations: ['indexFieldMetadatas'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async regenerateSearchVectorForObject(
|
|
||||||
queryRunner: QueryRunner | undefined,
|
|
||||||
object: ObjectMetadataEntity,
|
|
||||||
allIndexes: IndexMetadataEntity[],
|
|
||||||
schemaName: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<'success' | 'skipped'> {
|
|
||||||
const searchVectorField = this.findSearchVectorField(object);
|
|
||||||
|
|
||||||
if (!searchVectorField) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Search vector field not found for ${object.nameSingular} in workspace ${workspaceId}, skipping`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return 'skipped';
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchExpression = this.buildSearchExpression(object);
|
|
||||||
|
|
||||||
if (!searchExpression) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Cannot determine search expression for ${object.nameSingular} in workspace ${workspaceId}, skipping`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return 'skipped';
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableName = computeObjectTargetTable({
|
|
||||||
nameSingular: object.nameSingular,
|
|
||||||
isCustom: object.isCustom,
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingIndex = this.findSearchVectorIndex(
|
|
||||||
allIndexes,
|
|
||||||
object.id,
|
|
||||||
searchVectorField.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let localQueryRunner: QueryRunner;
|
|
||||||
const shouldRelease = !queryRunner;
|
|
||||||
|
|
||||||
if (queryRunner) {
|
|
||||||
localQueryRunner = queryRunner;
|
|
||||||
} else {
|
|
||||||
localQueryRunner = this.coreDataSource.createQueryRunner();
|
|
||||||
await localQueryRunner.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (existingIndex) {
|
|
||||||
await this.dropIndex(localQueryRunner, schemaName, existingIndex.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.dropSearchVectorColumn(
|
|
||||||
localQueryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.createSearchVectorColumn(
|
|
||||||
localQueryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
searchExpression,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIndex) {
|
|
||||||
await this.recreateIndex(
|
|
||||||
localQueryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
existingIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Regenerated search vector for ${object.nameSingular} in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return 'success';
|
|
||||||
} finally {
|
|
||||||
if (shouldRelease) {
|
|
||||||
await localQueryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private findSearchVectorField(
|
|
||||||
object: ObjectMetadataEntity,
|
|
||||||
): ObjectMetadataEntity['fields'][0] | undefined {
|
|
||||||
return object.fields.find(
|
|
||||||
(field) => field.name === SEARCH_VECTOR_FIELD.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private findSearchVectorIndex(
|
|
||||||
allIndexes: IndexMetadataEntity[],
|
|
||||||
objectId: string,
|
|
||||||
searchVectorFieldId: string,
|
|
||||||
): IndexMetadataEntity | undefined {
|
|
||||||
return allIndexes.find(
|
|
||||||
(index) =>
|
|
||||||
index.objectMetadataId === objectId &&
|
|
||||||
index.indexFieldMetadatas.some(
|
|
||||||
(indexField) => indexField.fieldMetadataId === searchVectorFieldId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildSearchExpression(
|
|
||||||
object: ObjectMetadataEntity,
|
|
||||||
): string | undefined {
|
|
||||||
if (object.standardId) {
|
|
||||||
const standardExpression = STANDARD_SEARCH_EXPRESSIONS[object.standardId];
|
|
||||||
|
|
||||||
if (standardExpression) {
|
|
||||||
return standardExpression;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.isCustom) {
|
|
||||||
return this.buildCustomObjectSearchExpression(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildCustomObjectSearchExpression(
|
|
||||||
object: ObjectMetadataEntity,
|
|
||||||
): string {
|
|
||||||
const labelField = object.fields.find(
|
|
||||||
(field) => field.id === object.labelIdentifierFieldMetadataId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!labelField) {
|
|
||||||
return getTsVectorColumnExpressionFromFields(
|
|
||||||
SEARCH_FIELDS_FOR_CUSTOM_OBJECT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getTsVectorColumnExpressionFromFields([
|
|
||||||
{
|
|
||||||
name: labelField.name,
|
|
||||||
type: labelField.type as SearchableFieldType,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async dropIndex(
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
schemaName: string,
|
|
||||||
indexName: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.workspaceSchemaManager.indexManager.dropIndex({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
indexName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async dropSearchVectorColumn(
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.workspaceSchemaManager.columnManager.dropColumns({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
columnNames: [SEARCH_VECTOR_FIELD.name],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createSearchVectorColumn(
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string,
|
|
||||||
expression: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.workspaceSchemaManager.columnManager.addColumns({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
columnDefinitions: [
|
|
||||||
{
|
|
||||||
name: SEARCH_VECTOR_FIELD.name,
|
|
||||||
type: 'tsvector',
|
|
||||||
isNullable: true,
|
|
||||||
generatedType: 'STORED',
|
|
||||||
asExpression: expression,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async recreateIndex(
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string,
|
|
||||||
index: IndexMetadataEntity,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.workspaceSchemaManager.indexManager.createIndex({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
index: {
|
|
||||||
name: index.name,
|
|
||||||
columns: [SEARCH_VECTOR_FIELD.name],
|
|
||||||
type: index.indexType,
|
|
||||||
isUnique: index.isUnique,
|
|
||||||
where: index.indexWhereClause ?? undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureUnaccentFunctionExists(): Promise<void> {
|
|
||||||
const result = await this.coreDataSource.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1 FROM pg_proc p
|
|
||||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
||||||
WHERE n.nspname = 'public'
|
|
||||||
AND p.proname = 'unaccent_immutable'
|
|
||||||
) as function_exists
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!result[0]?.function_exists) {
|
|
||||||
throw new Error(
|
|
||||||
'The public.unaccent_immutable() function is required but not found. Please run database migrations first.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildStandardSearchExpressions(): Partial<Record<string, string>> {
|
|
||||||
const standardObjectSearchFields = {
|
|
||||||
person: SEARCH_FIELDS_FOR_PERSON,
|
|
||||||
company: SEARCH_FIELDS_FOR_COMPANY,
|
|
||||||
opportunity: SEARCH_FIELDS_FOR_OPPORTUNITY,
|
|
||||||
task: SEARCH_FIELDS_FOR_TASKS,
|
|
||||||
note: SEARCH_FIELDS_FOR_NOTES,
|
|
||||||
dashboard: SEARCH_FIELDS_FOR_DASHBOARD,
|
|
||||||
workspaceMember: SEARCH_FIELDS_FOR_WORKSPACE_MEMBER,
|
|
||||||
workflow: SEARCH_FIELDS_FOR_WORKFLOWS,
|
|
||||||
workflowVersion: SEARCH_FIELDS_FOR_WORKFLOW_VERSIONS,
|
|
||||||
workflowRun: SEARCH_FIELDS_FOR_WORKFLOW_RUNS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expressions: Partial<Record<string, string>> = {};
|
|
||||||
|
|
||||||
for (const [objectKey, searchFields] of Object.entries(
|
|
||||||
standardObjectSearchFields,
|
|
||||||
)) {
|
|
||||||
const standardId =
|
|
||||||
STANDARD_OBJECT_IDS[objectKey as keyof typeof standardObjectSearchFields];
|
|
||||||
|
|
||||||
expressions[standardId] =
|
|
||||||
getTsVectorColumnExpressionFromFields(searchFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
return expressions;
|
|
||||||
}
|
|
||||||
-132
@@ -1,132 +0,0 @@
|
|||||||
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 { 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';
|
|
||||||
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { createCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views';
|
|
||||||
import { prefillWorkspaceFavorites } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workspace-favorites';
|
|
||||||
import { dashboardsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/dashboards-all.view';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-10:seed-dashboard-view',
|
|
||||||
description: 'Seed the dashboard view',
|
|
||||||
})
|
|
||||||
export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(ObjectMetadataEntity)
|
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
@InjectRepository(DataSourceEntity)
|
|
||||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
|
||||||
@InjectRepository(ViewEntity)
|
|
||||||
private readonly viewRepository: Repository<ViewEntity>,
|
|
||||||
private readonly applicationService: ApplicationService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
const [dashboardObjectMetadata] = await this.objectMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
standardId: STANDARD_OBJECT_IDS.dashboard,
|
|
||||||
},
|
|
||||||
relations: ['fields'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(dashboardObjectMetadata)) {
|
|
||||||
throw new Error(
|
|
||||||
`Dashboard object metadata not found for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { twentyStandardFlatApplication } =
|
|
||||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
||||||
{ workspaceId },
|
|
||||||
);
|
|
||||||
const views = [
|
|
||||||
dashboardsAllView({
|
|
||||||
objectMetadataItems: [dashboardObjectMetadata],
|
|
||||||
useCoreNaming: true,
|
|
||||||
twentyStandardFlatApplication,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
const schema = await this.dataSourceRepository.findOne({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(schema)) {
|
|
||||||
throw new Error(`Schema not found for workspace ${workspaceId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingViews = await this.viewRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataId: dashboardObjectMetadata.id,
|
|
||||||
key: ViewKey.INDEX,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingViews.length > 0) {
|
|
||||||
this.logger.log(
|
|
||||||
`Dashboard view already exists for workspace ${workspaceId}. Skipping...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would have seeded dashboard view for workspace ${workspaceId}. Skipping...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
|
||||||
|
|
||||||
await queryRunner.connect();
|
|
||||||
|
|
||||||
const createdViews = await createCoreViews(
|
|
||||||
queryRunner,
|
|
||||||
workspaceId,
|
|
||||||
views,
|
|
||||||
twentyStandardFlatApplication,
|
|
||||||
);
|
|
||||||
|
|
||||||
await prefillWorkspaceFavorites(
|
|
||||||
createdViews.map((view) => view.id),
|
|
||||||
queryRunner.manager as WorkspaceEntityManager,
|
|
||||||
schema.schema,
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryRunner.release();
|
|
||||||
this.logger.log(
|
|
||||||
`Successfully seeded dashboard view for workspace ${workspaceId}: ${createdViews.map((view) => view.name).join(', ')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
|
|
||||||
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
|
|
||||||
import { CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command';
|
|
||||||
import { FlushCacheCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-flush-cache.command';
|
|
||||||
import { MakeSureDashboardNamingAvailableCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command';
|
|
||||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
|
||||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
|
||||||
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
|
|
||||||
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
|
|
||||||
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
|
|
||||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
|
||||||
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';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
|
||||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
|
||||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
|
||||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([
|
|
||||||
WorkspaceEntity,
|
|
||||||
ObjectMetadataEntity,
|
|
||||||
FieldMetadataEntity,
|
|
||||||
IndexMetadataEntity,
|
|
||||||
ViewEntity,
|
|
||||||
DataSourceEntity,
|
|
||||||
ApplicationEntity,
|
|
||||||
]),
|
|
||||||
DataSourceModule,
|
|
||||||
WorkspaceSchemaManagerModule,
|
|
||||||
WorkspaceCacheStorageModule,
|
|
||||||
ObjectMetadataModule,
|
|
||||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
|
||||||
ApplicationModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
MigrateChannelPartialFullSyncStagesCommand,
|
|
||||||
MigrateAttachmentAuthorToCreatedByCommand,
|
|
||||||
MigrateAttachmentTypeToFileCategoryCommand,
|
|
||||||
RegenerateSearchVectorsCommand,
|
|
||||||
AddWorkflowRunStopStatusesCommand,
|
|
||||||
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
|
|
||||||
MakeSureDashboardNamingAvailableCommand,
|
|
||||||
SeedDashboardViewCommand,
|
|
||||||
CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
|
|
||||||
FlushCacheCommand,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
MigrateAttachmentAuthorToCreatedByCommand,
|
|
||||||
MigrateAttachmentTypeToFileCategoryCommand,
|
|
||||||
RegenerateSearchVectorsCommand,
|
|
||||||
AddWorkflowRunStopStatusesCommand,
|
|
||||||
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
|
|
||||||
MigrateChannelPartialFullSyncStagesCommand,
|
|
||||||
MakeSureDashboardNamingAvailableCommand,
|
|
||||||
SeedDashboardViewCommand,
|
|
||||||
CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
|
|
||||||
FlushCacheCommand,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class V1_10_UpgradeVersionCommandModule {}
|
|
||||||
-93
@@ -1,93 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { In, type 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 { 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 { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-11:clean-orphaned-role-targets',
|
|
||||||
description:
|
|
||||||
'Clean up roleTargets records that reference non-existent userWorkspaces',
|
|
||||||
})
|
|
||||||
export class CleanOrphanedRoleTargetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
private hasRunOnce = false;
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(RoleTargetEntity)
|
|
||||||
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
if (this.hasRunOnce) {
|
|
||||||
this.logger.log('This command has already been run');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isDryRun = options.dryRun || false;
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
this.logger.log('Dry run mode: No changes will be applied');
|
|
||||||
}
|
|
||||||
|
|
||||||
const orphanedRoleTargets = await this.roleTargetRepository
|
|
||||||
.createQueryBuilder('roleTarget')
|
|
||||||
.leftJoin(
|
|
||||||
UserWorkspaceEntity,
|
|
||||||
'userWorkspace',
|
|
||||||
'userWorkspace.id = roleTarget.userWorkspaceId',
|
|
||||||
)
|
|
||||||
.where('roleTarget.userWorkspaceId IS NOT NULL')
|
|
||||||
.andWhere('userWorkspace.id IS NULL')
|
|
||||||
.select([
|
|
||||||
'roleTarget.id',
|
|
||||||
'roleTarget.userWorkspaceId',
|
|
||||||
'roleTarget.workspaceId',
|
|
||||||
])
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
if (orphanedRoleTargets.length === 0) {
|
|
||||||
this.logger.log('No orphaned roleTargets found');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`DRY RUN: Would delete ${orphanedRoleTargets.length} orphaned roleTarget(s):`,
|
|
||||||
);
|
|
||||||
orphanedRoleTargets.forEach((roleTarget) => {
|
|
||||||
this.logger.log(
|
|
||||||
` - roleTargetId: ${roleTarget.id}, userWorkspaceId: ${roleTarget.userWorkspaceId}, workspaceId: ${roleTarget.workspaceId}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.hasRunOnce = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orphanedIds = orphanedRoleTargets.map((roleTarget) => roleTarget.id);
|
|
||||||
|
|
||||||
await this.roleTargetRepository.delete({ id: In(orphanedIds) });
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Deleted ${orphanedRoleTargets.length} orphaned roleTarget(s)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.hasRunOnce = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-100
@@ -1,100 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { In, type 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 { 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-11:clean-orphaned-user-workspaces',
|
|
||||||
description:
|
|
||||||
'Clean up userWorkspace records that do not have a corresponding workspaceMember in the workspace schema',
|
|
||||||
})
|
|
||||||
export class CleanOrphanedUserWorkspacesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(UserWorkspaceEntity)
|
|
||||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
const isDryRun = options.dryRun || false;
|
|
||||||
|
|
||||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userWorkspaces.length === 0) {
|
|
||||||
this.logger.log(
|
|
||||||
`No userWorkspaces found for workspace ${workspaceId}, skipping`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceMemberRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'workspaceMember',
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceMembers = await workspaceMemberRepository.find({
|
|
||||||
select: ['userId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const workspaceMemberUserIds = new Set(
|
|
||||||
workspaceMembers.map((member) => member.userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const orphanedUserWorkspaces = userWorkspaces.filter(
|
|
||||||
(userWorkspace) => !workspaceMemberUserIds.has(userWorkspace.userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (orphanedUserWorkspaces.length === 0) {
|
|
||||||
this.logger.log(
|
|
||||||
`No orphaned userWorkspaces found for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`DRY RUN: Would delete ${orphanedUserWorkspaces.length} orphaned userWorkspace(s) for workspace ${workspaceId}:`,
|
|
||||||
);
|
|
||||||
orphanedUserWorkspaces.forEach((userWorkspace) => {
|
|
||||||
this.logger.log(
|
|
||||||
` - userWorkspaceId: ${userWorkspace.id}, userId: ${userWorkspace.userId}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete orphaned userWorkspaces
|
|
||||||
const orphanedIds = orphanedUserWorkspaces.map((uw) => uw.id);
|
|
||||||
|
|
||||||
await this.userWorkspaceRepository.delete({ id: In(orphanedIds) });
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Deleted ${orphanedUserWorkspaces.length} orphaned userWorkspace(s) for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-77
@@ -1,77 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { In, 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 { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
|
||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-11:create-twenty-standard-application',
|
|
||||||
description:
|
|
||||||
'Create twenty-standard application for workspaces that do not have them',
|
|
||||||
})
|
|
||||||
export class CreateTwentyStandardApplicationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(ApplicationEntity)
|
|
||||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
|
||||||
private readonly applicationService: ApplicationService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Checking standard applications for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingApplications = await this.applicationRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
universalIdentifier: In([
|
|
||||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingIds = new Set(
|
|
||||||
existingApplications.map((app) => app.universalIdentifier),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIds.has(TWENTY_STANDARD_APPLICATION.universalIdentifier)) {
|
|
||||||
this.logger.log(
|
|
||||||
`Skipping twenty standard application as it already exists`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`About to seed twenty standard application`);
|
|
||||||
if (options.dryRun) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.applicationService.createTwentyStandardApplication({
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(`Successfully seeded twenty standard`);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error(`Failed to seed twenty standard`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
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 { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
|
||||||
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';
|
|
||||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
|
||||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
|
||||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([
|
|
||||||
WorkspaceEntity,
|
|
||||||
ObjectMetadataEntity,
|
|
||||||
FieldMetadataEntity,
|
|
||||||
IndexMetadataEntity,
|
|
||||||
ViewEntity,
|
|
||||||
UserWorkspaceEntity,
|
|
||||||
RoleTargetEntity,
|
|
||||||
ApplicationEntity,
|
|
||||||
]),
|
|
||||||
DataSourceModule,
|
|
||||||
WorkspaceSchemaManagerModule,
|
|
||||||
ApplicationModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
CleanOrphanedUserWorkspacesCommand,
|
|
||||||
CleanOrphanedRoleTargetsCommand,
|
|
||||||
CreateTwentyStandardApplicationCommand,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
CleanOrphanedUserWorkspacesCommand,
|
|
||||||
CleanOrphanedRoleTargetsCommand,
|
|
||||||
CreateTwentyStandardApplicationCommand,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class V1_11_UpgradeVersionCommandModule {}
|
|
||||||
-259
@@ -1,259 +0,0 @@
|
|||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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: GlobalWorkspaceOrmManager,
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-256
@@ -1,256 +0,0 @@
|
|||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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: GlobalWorkspaceOrmManager,
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-273
@@ -1,273 +0,0 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import deepEqual from 'deep-equal';
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import {
|
|
||||||
CompositeType,
|
|
||||||
compositeTypeDefinitions,
|
|
||||||
FieldMetadataType,
|
|
||||||
} from 'twenty-shared/types';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { 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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
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 { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
||||||
import { deprecatedGenerateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/deprecated-generate-default-value';
|
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
||||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.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 { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
|
||||||
import {
|
|
||||||
WorkspaceMigrationIndexAction,
|
|
||||||
WorkspaceMigrationIndexActionType,
|
|
||||||
WorkspaceMigrationTableAction,
|
|
||||||
WorkspaceMigrationTableActionType,
|
|
||||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
||||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
|
||||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-12:clean-null-equivalent-values',
|
|
||||||
description: 'Clean up null equivalent values in the database',
|
|
||||||
})
|
|
||||||
export class CleanNullEquivalentValuesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
protected readonly logger = new Logger(CleanNullEquivalentValuesCommand.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
@InjectRepository(ObjectMetadataEntity)
|
|
||||||
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
@InjectRepository(FieldMetadataEntity)
|
|
||||||
protected readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
@InjectRepository(IndexMetadataEntity)
|
|
||||||
protected readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
|
||||||
@InjectRepository(FeatureFlagEntity)
|
|
||||||
protected readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
|
||||||
protected readonly workspaceMigrationService: WorkspaceMigrationService,
|
|
||||||
protected readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
protected readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
dataSource,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
const isDryRun = options.dryRun || false;
|
|
||||||
|
|
||||||
if (!isDefined(dataSource)) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find data source for workspace ${workspaceId}, should never occur`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
this.logger.log('Dry run mode: No changes will be applied');
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectMetadataItems = await this.objectMetadataRepository.find({
|
|
||||||
where: { workspaceId },
|
|
||||||
relations: [
|
|
||||||
'fields',
|
|
||||||
'indexMetadatas',
|
|
||||||
'indexMetadatas.indexFieldMetadatas',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const objectMetadataItem of objectMetadataItems) {
|
|
||||||
const tableName = computeObjectTargetTable(objectMetadataItem);
|
|
||||||
|
|
||||||
for (const field of objectMetadataItem.fields) {
|
|
||||||
const fieldDefaultDefaultValue = deprecatedGenerateDefaultValue(
|
|
||||||
field.type,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
isDefined(field.defaultValue) &&
|
|
||||||
((typeof field.defaultValue === 'string' &&
|
|
||||||
field.defaultValue === fieldDefaultDefaultValue) ||
|
|
||||||
this.objectEquals(
|
|
||||||
field.defaultValue,
|
|
||||||
fieldDefaultDefaultValue,
|
|
||||||
)) &&
|
|
||||||
field.type !== FieldMetadataType.ACTOR
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Processing field ${field.name} on object ${objectMetadataItem.nameSingular} (Table: ${tableName})`,
|
|
||||||
);
|
|
||||||
if (!isDryRun) {
|
|
||||||
if (isCompositeFieldMetadataType(field.type)) {
|
|
||||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
|
||||||
|
|
||||||
if (isDefined(compositeType)) {
|
|
||||||
for (const property of compositeType.properties) {
|
|
||||||
const columnName = computeCompositeColumnName(
|
|
||||||
field.name,
|
|
||||||
property,
|
|
||||||
);
|
|
||||||
|
|
||||||
await dataSource.query(
|
|
||||||
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL, ALTER COLUMN "${columnName}" DROP DEFAULT`,
|
|
||||||
[],
|
|
||||||
undefined,
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
await dataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}" SET "${columnName}" = NULL WHERE "${columnName}" = ${field.defaultValue[property.name as keyof typeof field.defaultValue]}`,
|
|
||||||
[],
|
|
||||||
undefined,
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await dataSource.query(
|
|
||||||
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${field.name}" DROP NOT NULL, ALTER COLUMN "${field.name}" DROP DEFAULT`,
|
|
||||||
[],
|
|
||||||
undefined,
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
await dataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}" SET "${field.name}" = NULL WHERE "${field.name}" = ${field.defaultValue}`,
|
|
||||||
[],
|
|
||||||
undefined,
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.fieldMetadataRepository.update(field.id, {
|
|
||||||
isNullable: true,
|
|
||||||
defaultValue: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.invalidateFlatEntityMaps(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
flatMapsKeys: ['flatFieldMetadataMaps'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relevantIndexes = objectMetadataItem.indexMetadatas.filter(
|
|
||||||
(index) =>
|
|
||||||
index.isUnique &&
|
|
||||||
index.indexFieldMetadatas.some(
|
|
||||||
(ifm) => ifm.fieldMetadataId === field.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const index of relevantIndexes) {
|
|
||||||
if (!isDryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Removing where clause from index ${index.name} on ${tableName}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.indexMetadataRepository.update(index.id, {
|
|
||||||
indexWhereClause: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.invalidateFlatEntityMaps(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
flatMapsKeys: ['flatIndexMaps'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnNames = index.indexFieldMetadatas.flatMap(
|
|
||||||
(indexFieldMetadata) => {
|
|
||||||
const fieldMetadata = objectMetadataItem.fields.find(
|
|
||||||
(f) => f.id === indexFieldMetadata.fieldMetadataId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDefined(fieldMetadata)) {
|
|
||||||
throw new Error(
|
|
||||||
`Field metadata not found for index field metadata ${indexFieldMetadata.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
const compositeType = compositeTypeDefinitions.get(
|
|
||||||
fieldMetadata.type,
|
|
||||||
) as CompositeType;
|
|
||||||
|
|
||||||
const uniqueCompositeProperties =
|
|
||||||
compositeType.properties.filter(
|
|
||||||
(property) => property.isIncludedInUniqueConstraint,
|
|
||||||
);
|
|
||||||
|
|
||||||
return uniqueCompositeProperties.map((subField) =>
|
|
||||||
computeCompositeColumnName(fieldMetadata.name, subField),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [fieldMetadata.name];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const dropIndexAction: WorkspaceMigrationIndexAction = {
|
|
||||||
action: WorkspaceMigrationIndexActionType.DROP,
|
|
||||||
name: index.name,
|
|
||||||
columns: [],
|
|
||||||
isUnique: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createIndexAction: WorkspaceMigrationIndexAction = {
|
|
||||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
|
||||||
name: index.name,
|
|
||||||
columns: columnNames,
|
|
||||||
isUnique: true,
|
|
||||||
where: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const migration: WorkspaceMigrationTableAction = {
|
|
||||||
name: tableName,
|
|
||||||
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
|
|
||||||
indexes: [dropIndexAction, createIndexAction],
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
|
||||||
generateMigrationName(`update-index-${index.name}-remove-where`),
|
|
||||||
workspaceId,
|
|
||||||
[migration],
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private objectEquals(obj1: unknown, obj2: unknown): boolean {
|
|
||||||
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return deepEqual(obj1, obj2, { strict: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
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 {
|
|
||||||
WorkspacesMigrationCommandRunner,
|
|
||||||
type 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-12:create-workspace-custom-application',
|
|
||||||
description:
|
|
||||||
'Create workspace-custom application for workspaces that do not have them',
|
|
||||||
})
|
|
||||||
export class CreateWorkspaceCustomApplicationCommand extends WorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly applicationService: ApplicationService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
|
|
||||||
WorkspaceActivationStatus.ONGOING_CREATION,
|
|
||||||
WorkspaceActivationStatus.PENDING_CREATION,
|
|
||||||
WorkspaceActivationStatus.ACTIVE,
|
|
||||||
WorkspaceActivationStatus.INACTIVE,
|
|
||||||
WorkspaceActivationStatus.SUSPENDED,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Checking standard applications for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
|
||||||
where: { id: workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(workspace)) {
|
|
||||||
throw new Error(`${workspaceId} workspace not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefined(workspace.workspaceCustomApplicationId)) {
|
|
||||||
this.logger.log(
|
|
||||||
`${workspaceId} skipping custom workspace application creation as already exists`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customWorkspaceApplication =
|
|
||||||
await this.applicationService.createWorkspaceCustomApplication({
|
|
||||||
workspaceId,
|
|
||||||
workspaceDisplayName: workspace.displayName,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceRepository.update(workspace.id, {
|
|
||||||
workspaceCustomApplicationId: customWorkspaceApplication.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(`Successfully create workspace custom application`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { 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 { 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
// Note: this command needs to be run after CreateWorkspaceCustomApplicationCommand
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-12:set-standard-application-not-uninstallable',
|
|
||||||
description: 'Set canBeUninstalled flag to false for standard applications',
|
|
||||||
})
|
|
||||||
export class SetStandardApplicationNotUninstallableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly applicationService: ApplicationService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Checking workspace applications for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
|
||||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const existingApplication of [
|
|
||||||
twentyStandardFlatApplication,
|
|
||||||
workspaceCustomFlatApplication,
|
|
||||||
]) {
|
|
||||||
await this.applicationService.update(existingApplication.id, {
|
|
||||||
canBeUninstalled: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.logger.log(`Successfully updated workspace application`);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error(`Failed to update workspace application`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
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 { CleanNullEquivalentValuesCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-clean-null-equivalent-values';
|
|
||||||
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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.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 { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
|
||||||
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';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
|
||||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([
|
|
||||||
WorkspaceEntity,
|
|
||||||
ObjectMetadataEntity,
|
|
||||||
FieldMetadataEntity,
|
|
||||||
IndexMetadataEntity,
|
|
||||||
FeatureFlagEntity,
|
|
||||||
]),
|
|
||||||
WorkspaceSchemaManagerModule,
|
|
||||||
WorkspaceMigrationModule,
|
|
||||||
WorkspaceMigrationRunnerModule,
|
|
||||||
ApplicationModule,
|
|
||||||
FieldMetadataModule,
|
|
||||||
DataSourceModule,
|
|
||||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
AddCalendarEventsImportScheduledSyncStageCommand,
|
|
||||||
AddMessagesImportScheduledSyncStageCommand,
|
|
||||||
CreateWorkspaceCustomApplicationCommand,
|
|
||||||
SetStandardApplicationNotUninstallableCommand,
|
|
||||||
WorkspaceCustomApplicationIdNonNullableCommand,
|
|
||||||
CleanNullEquivalentValuesCommand,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
AddCalendarEventsImportScheduledSyncStageCommand,
|
|
||||||
AddMessagesImportScheduledSyncStageCommand,
|
|
||||||
CreateWorkspaceCustomApplicationCommand,
|
|
||||||
SetStandardApplicationNotUninstallableCommand,
|
|
||||||
WorkspaceCustomApplicationIdNonNullableCommand,
|
|
||||||
CleanNullEquivalentValuesCommand,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class V1_12_UpgradeVersionCommandModule {}
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: '1-12-workspace-custom-application-id-non-nullable-migration',
|
|
||||||
description: 'Create FK_3b1acb13a5dac9956d1a4b32755 foreign key',
|
|
||||||
})
|
|
||||||
export class WorkspaceCustomApplicationIdNonNullableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
private hasRunOnce = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
if (this.hasRunOnce) {
|
|
||||||
this.logger.log(
|
|
||||||
'Skipping has already been run once WorkspaceCustomApplicationIdNonNullableCommand',
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
|
||||||
|
|
||||||
await queryRunner.connect();
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
if (!options.dryRun) {
|
|
||||||
try {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "core"."workspace" ALTER COLUMN "workspaceCustomApplicationId" SET NOT NULL`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
this.logger.log(
|
|
||||||
'Successfully run WorkspaceCustomApplicationIdNonNullableCommand',
|
|
||||||
);
|
|
||||||
this.hasRunOnce = true;
|
|
||||||
} catch (error) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
this.logger.log(
|
|
||||||
`Rollbacking WorkspaceCustomApplicationIdNonNullableCommand: ${error.message}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/u
|
|||||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||||
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
||||||
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
||||||
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command';
|
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-index.command';
|
||||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
|||||||
-132
@@ -1,132 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { 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 { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
|
||||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-6:fix-label-identifier-position-and-visibility',
|
|
||||||
description:
|
|
||||||
'Fix label identifier position to ensure it has the minimal position in each view',
|
|
||||||
})
|
|
||||||
export class FixLabelIdentifierPositionAndVisibilityCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectRepository(ViewEntity)
|
|
||||||
private readonly viewRepository: Repository<ViewEntity>,
|
|
||||||
@InjectRepository(ViewFieldEntity)
|
|
||||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Checking label identifiers position and visibility for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const views = await this.viewRepository.find({
|
|
||||||
where: { workspaceId },
|
|
||||||
relations: {
|
|
||||||
objectMetadata: true,
|
|
||||||
viewFields: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const view of views) {
|
|
||||||
const { objectMetadata, viewFields } = view;
|
|
||||||
|
|
||||||
if (!objectMetadata.labelIdentifierFieldMetadataId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelIdentifierViewField = viewFields.find(
|
|
||||||
(viewField: ViewFieldEntity) =>
|
|
||||||
viewField.fieldMetadataId ===
|
|
||||||
objectMetadata.labelIdentifierFieldMetadataId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!labelIdentifierViewField) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find minimum position and count fields at that position in single pass
|
|
||||||
const minPositionData = viewFields.reduce(
|
|
||||||
(acc, viewField: ViewFieldEntity) => {
|
|
||||||
if (viewField.position < acc.minPosition) {
|
|
||||||
return { minPosition: viewField.position, count: 1 };
|
|
||||||
}
|
|
||||||
if (viewField.position === acc.minPosition) {
|
|
||||||
return { ...acc, count: acc.count + 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ minPosition: Number.MAX_SAFE_INTEGER, count: 0 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const minPosition = minPositionData.minPosition;
|
|
||||||
const numberOfViewFieldsAtTheMinimalPosition = minPositionData.count;
|
|
||||||
|
|
||||||
const labelIdentifierPositionIsAlreadyTheMinimalPosition =
|
|
||||||
labelIdentifierViewField.position === minPosition &&
|
|
||||||
numberOfViewFieldsAtTheMinimalPosition === 1;
|
|
||||||
|
|
||||||
const labelIdentifierIsAlreadyVisible =
|
|
||||||
labelIdentifierViewField.isVisible;
|
|
||||||
|
|
||||||
if (
|
|
||||||
labelIdentifierPositionIsAlreadyTheMinimalPosition &&
|
|
||||||
labelIdentifierIsAlreadyVisible
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelIdentifierPositionIsAlreadyTheMinimalPosition) {
|
|
||||||
// Update the label identifier position to be the minimal one
|
|
||||||
const newPosition = minPosition - 1;
|
|
||||||
|
|
||||||
if (!options.dryRun) {
|
|
||||||
await this.viewFieldRepository.update(
|
|
||||||
{ id: labelIdentifierViewField.id },
|
|
||||||
{ position: newPosition },
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Fixed label identifier position for view ${view.id} in workspace ${workspaceId}: ${labelIdentifierViewField.position} -> ${newPosition}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.logger.log(
|
|
||||||
`Would fix label identifier position for view ${view.id} in workspace ${workspaceId}: ${labelIdentifierViewField.position} -> ${newPosition}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelIdentifierIsAlreadyVisible) {
|
|
||||||
if (!options.dryRun) {
|
|
||||||
await this.viewFieldRepository.update(
|
|
||||||
{ id: labelIdentifierViewField.id },
|
|
||||||
{ isVisible: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Fixed label identifier visibility for view ${view.id} in workspace ${workspaceId}: ${labelIdentifierViewField.isVisible} -> true`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
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';
|
|
||||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
|
||||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
|
||||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([
|
|
||||||
WorkspaceEntity,
|
|
||||||
FieldMetadataEntity,
|
|
||||||
ObjectMetadataEntity,
|
|
||||||
ViewEntity,
|
|
||||||
ViewFieldEntity,
|
|
||||||
]),
|
|
||||||
DataSourceModule,
|
|
||||||
WorkspaceDataSourceModule,
|
|
||||||
WorkspaceSchemaManagerModule,
|
|
||||||
WorkspaceMetadataVersionModule,
|
|
||||||
],
|
|
||||||
providers: [FixLabelIdentifierPositionAndVisibilityCommand],
|
|
||||||
exports: [FixLabelIdentifierPositionAndVisibilityCommand],
|
|
||||||
})
|
|
||||||
export class V1_6_UpgradeVersionCommandModule {}
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-7:backfill-workflow-manual-trigger-availability',
|
|
||||||
description:
|
|
||||||
'Backfill workflow manual trigger availability based on objectType',
|
|
||||||
})
|
|
||||||
export class BackfillWorkflowManualTriggerAvailabilityCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
|
|
||||||
const workflowVersions = await this.coreDataSource.query(
|
|
||||||
`SELECT * FROM ${schemaName}."workflowVersion"`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const workflowVersion of workflowVersions) {
|
|
||||||
const { trigger } = workflowVersion;
|
|
||||||
|
|
||||||
if (!isDefined(trigger) || trigger?.type !== WorkflowTriggerType.MANUAL) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const availability = trigger.settings.availability;
|
|
||||||
const objectType = trigger.settings.objectType;
|
|
||||||
|
|
||||||
if (isDefined(availability)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAvailability = objectType
|
|
||||||
? {
|
|
||||||
type: 'SINGLE_RECORD',
|
|
||||||
objectNameSingular: objectType,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: 'GLOBAL',
|
|
||||||
locations: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedTrigger = {
|
|
||||||
...trigger,
|
|
||||||
settings: {
|
|
||||||
...trigger.settings,
|
|
||||||
availability: newAvailability,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Updating workflow version ${workflowVersion.id} with new availability ${JSON.stringify(
|
|
||||||
newAvailability,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`UPDATE ${schemaName}."workflowVersion" SET trigger = $1 WHERE id = $2`,
|
|
||||||
[updatedTrigger, workflowVersion.id],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
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]), DataSourceModule],
|
|
||||||
providers: [BackfillWorkflowManualTriggerAvailabilityCommand],
|
|
||||||
exports: [BackfillWorkflowManualTriggerAvailabilityCommand],
|
|
||||||
})
|
|
||||||
export class V1_7_UpgradeVersionCommandModule {}
|
|
||||||
-371
@@ -1,371 +0,0 @@
|
|||||||
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 { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
|
|
||||||
|
|
||||||
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';
|
|
||||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
|
||||||
import {
|
|
||||||
WorkspaceMigrationIndexAction,
|
|
||||||
WorkspaceMigrationIndexActionType,
|
|
||||||
WorkspaceMigrationTableAction,
|
|
||||||
WorkspaceMigrationTableActionType,
|
|
||||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
||||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-8:deduplicate-unique-fields',
|
|
||||||
description:
|
|
||||||
'Deduplicate unique fields for workspaceMembers, companies and people because we changed the unique constraint',
|
|
||||||
})
|
|
||||||
export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
protected readonly logger = new Logger(DeduplicateUniqueFieldsCommand.name);
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
protected readonly indexMetadataService: IndexMetadataService,
|
|
||||||
protected readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
|
||||||
protected readonly workspaceMigrationService: WorkspaceMigrationService,
|
|
||||||
@InjectRepository(ObjectMetadataEntity)
|
|
||||||
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
@InjectRepository(IndexMetadataEntity)
|
|
||||||
protected readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
dataSource,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
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,
|
|
||||||
dryRun: options.dryRun ?? false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.deduplicateUniqueDomainNameFieldForCompanies({
|
|
||||||
dataSource,
|
|
||||||
dryRun: options.dryRun ?? false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.deduplicateUniqueEmailFieldForPeople({
|
|
||||||
dataSource,
|
|
||||||
dryRun: options.dryRun ?? false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!options.dryRun) {
|
|
||||||
await this.updateExistingIndexedFields({
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataNameSingular: 'workspaceMember',
|
|
||||||
columnName: 'userEmail',
|
|
||||||
});
|
|
||||||
await this.updateExistingIndexedFields({
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataNameSingular: 'company',
|
|
||||||
columnName: 'domainNamePrimaryLinkUrl',
|
|
||||||
});
|
|
||||||
await this.updateExistingIndexedFields({
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataNameSingular: 'person',
|
|
||||||
columnName: 'emailsPrimaryEmail',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeExistingUniqueIndexName({
|
|
||||||
objectMetadata,
|
|
||||||
fieldMetadataToIndex,
|
|
||||||
}: {
|
|
||||||
objectMetadata: ObjectMetadataEntity;
|
|
||||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[];
|
|
||||||
}) {
|
|
||||||
const tableName = computeObjectTargetTable(objectMetadata);
|
|
||||||
const columnNames: string[] = fieldMetadataToIndex.map(
|
|
||||||
(fieldMetadata) => fieldMetadata.name as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `IDX_UNIQUE_${generateDeterministicIndexName([tableName, ...columnNames])}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async computeExistingIndexDeletionMigration({
|
|
||||||
objectMetadata,
|
|
||||||
fieldMetadataToIndex,
|
|
||||||
}: {
|
|
||||||
objectMetadata: ObjectMetadataEntity;
|
|
||||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[];
|
|
||||||
}) {
|
|
||||||
const tableName = computeObjectTargetTable(objectMetadata);
|
|
||||||
|
|
||||||
const indexName = this.computeExistingUniqueIndexName({
|
|
||||||
objectMetadata,
|
|
||||||
fieldMetadataToIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: tableName,
|
|
||||||
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
action: WorkspaceMigrationIndexActionType.DROP,
|
|
||||||
name: indexName,
|
|
||||||
columns: [],
|
|
||||||
isUnique: true,
|
|
||||||
} satisfies WorkspaceMigrationIndexAction,
|
|
||||||
],
|
|
||||||
} satisfies WorkspaceMigrationTableAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateExistingIndexedFields({
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataNameSingular,
|
|
||||||
columnName,
|
|
||||||
}: {
|
|
||||||
workspaceId: string;
|
|
||||||
objectMetadataNameSingular: string;
|
|
||||||
columnName: string;
|
|
||||||
}) {
|
|
||||||
this.logger.log(
|
|
||||||
`Updating existing indexed fields for workspace members for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceMemberObjectMetadata =
|
|
||||||
await this.objectMetadataRepository.findOneByOrFail({
|
|
||||||
nameSingular: objectMetadataNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.indexMetadataRepository.delete({
|
|
||||||
workspaceId,
|
|
||||||
name: this.computeExistingUniqueIndexName({
|
|
||||||
objectMetadata: workspaceMemberObjectMetadata,
|
|
||||||
fieldMetadataToIndex: [{ name: columnName }],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const indexDeletionMigration =
|
|
||||||
await this.computeExistingIndexDeletionMigration({
|
|
||||||
objectMetadata: workspaceMemberObjectMetadata,
|
|
||||||
fieldMetadataToIndex: [{ name: columnName }],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
|
||||||
generateMigrationName(`delete-${objectMetadataNameSingular}-index`),
|
|
||||||
workspaceId,
|
|
||||||
[indexDeletionMigration],
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deduplicateUniqueUserEmailFieldForWorkspaceMembers({
|
|
||||||
dataSource,
|
|
||||||
dryRun,
|
|
||||||
}: {
|
|
||||||
dataSource: WorkspaceDataSourceInterface;
|
|
||||||
dryRun: boolean;
|
|
||||||
}) {
|
|
||||||
const workspaceMemberRepository = dataSource.getRepository(
|
|
||||||
'workspaceMember',
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const duplicates = await workspaceMemberRepository
|
|
||||||
.createQueryBuilder('workspaceMember')
|
|
||||||
.select('workspaceMember.userEmail', 'userEmail')
|
|
||||||
.addSelect('COUNT(*)', 'count')
|
|
||||||
.andWhere('workspaceMember.userEmail IS NOT NULL')
|
|
||||||
.andWhere("workspaceMember.userEmail != ''")
|
|
||||||
.withDeleted()
|
|
||||||
.groupBy('workspaceMember.userEmail')
|
|
||||||
.having('COUNT(*) > 1')
|
|
||||||
.getRawMany();
|
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
|
||||||
const { userEmail } = duplicate;
|
|
||||||
|
|
||||||
const softDeletedWorkspaceMembers = await workspaceMemberRepository.find({
|
|
||||||
where: {
|
|
||||||
userEmail,
|
|
||||||
deletedAt: Not(IsNull()),
|
|
||||||
},
|
|
||||||
withDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [
|
|
||||||
i,
|
|
||||||
softDeletedWorkspaceMember,
|
|
||||||
] of softDeletedWorkspaceMembers.entries()) {
|
|
||||||
const newUserEmail = this.computeNewFieldValues(
|
|
||||||
softDeletedWorkspaceMember.userEmail,
|
|
||||||
i,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
await workspaceMemberRepository
|
|
||||||
.createQueryBuilder('workspaceMember')
|
|
||||||
.update()
|
|
||||||
.set({
|
|
||||||
userEmail: newUserEmail,
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: softDeletedWorkspaceMember.id })
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
this.logger.log(
|
|
||||||
`Updated workspaceMember ${softDeletedWorkspaceMembers[i].id} userEmail from ${userEmail} to ${newUserEmail}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deduplicateUniqueDomainNameFieldForCompanies({
|
|
||||||
dataSource,
|
|
||||||
dryRun,
|
|
||||||
}: {
|
|
||||||
dataSource: WorkspaceDataSourceInterface;
|
|
||||||
dryRun: boolean;
|
|
||||||
}) {
|
|
||||||
const companyRepository = dataSource.getRepository('company', {
|
|
||||||
shouldBypassPermissionChecks: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const duplicates = await companyRepository
|
|
||||||
.createQueryBuilder('company')
|
|
||||||
.select('company.domainNamePrimaryLinkUrl', 'domainNamePrimaryLinkUrl')
|
|
||||||
.addSelect('COUNT(*)', 'count')
|
|
||||||
.andWhere('company.domainNamePrimaryLinkUrl IS NOT NULL')
|
|
||||||
.andWhere("company.domainNamePrimaryLinkUrl != ''")
|
|
||||||
.withDeleted()
|
|
||||||
.groupBy('company.domainNamePrimaryLinkUrl')
|
|
||||||
.having('COUNT(*) > 1')
|
|
||||||
.getRawMany();
|
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
|
||||||
const { domainNamePrimaryLinkUrl } = duplicate;
|
|
||||||
|
|
||||||
const softDeletedCompanies = await companyRepository.find({
|
|
||||||
where: {
|
|
||||||
domainName: {
|
|
||||||
primaryLinkUrl: domainNamePrimaryLinkUrl,
|
|
||||||
},
|
|
||||||
deletedAt: Not(IsNull()),
|
|
||||||
},
|
|
||||||
withDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [i, softDeletedCompany] of softDeletedCompanies.entries()) {
|
|
||||||
const newDomainNamePrimaryLinkUrl = this.computeNewFieldValues(
|
|
||||||
softDeletedCompany.domainName.primaryLinkUrl,
|
|
||||||
i,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
await companyRepository
|
|
||||||
.createQueryBuilder('company')
|
|
||||||
.update()
|
|
||||||
.set({
|
|
||||||
domainName: {
|
|
||||||
primaryLinkUrl: newDomainNamePrimaryLinkUrl,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: softDeletedCompany.id })
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
this.logger.log(
|
|
||||||
`Updated company ${softDeletedCompany.id} domainNamePrimaryLinkUrl from ${domainNamePrimaryLinkUrl} to ${newDomainNamePrimaryLinkUrl}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deduplicateUniqueEmailFieldForPeople({
|
|
||||||
dataSource,
|
|
||||||
dryRun,
|
|
||||||
}: {
|
|
||||||
dataSource: WorkspaceDataSourceInterface;
|
|
||||||
dryRun: boolean;
|
|
||||||
}) {
|
|
||||||
const personRepository = dataSource.getRepository('person', {
|
|
||||||
shouldBypassPermissionChecks: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const duplicates = await personRepository
|
|
||||||
.createQueryBuilder('person')
|
|
||||||
.select('person.emailsPrimaryEmail', 'emailsPrimaryEmail')
|
|
||||||
.addSelect('COUNT(*)', 'count')
|
|
||||||
.andWhere('person.emailsPrimaryEmail IS NOT NULL')
|
|
||||||
.andWhere("person.emailsPrimaryEmail != ''")
|
|
||||||
.withDeleted()
|
|
||||||
.groupBy('person.emailsPrimaryEmail')
|
|
||||||
.having('COUNT(*) > 1')
|
|
||||||
.getRawMany();
|
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
|
||||||
const { emailsPrimaryEmail } = duplicate;
|
|
||||||
|
|
||||||
const softDeletedPersons = await personRepository.find({
|
|
||||||
where: {
|
|
||||||
emails: {
|
|
||||||
primaryEmail: emailsPrimaryEmail,
|
|
||||||
},
|
|
||||||
deletedAt: Not(IsNull()),
|
|
||||||
},
|
|
||||||
withDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [i, softDeletedPerson] of softDeletedPersons.entries()) {
|
|
||||||
const newEmailsPrimaryEmail = this.computeNewFieldValues(
|
|
||||||
softDeletedPerson.emails.primaryEmail,
|
|
||||||
i,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
await personRepository
|
|
||||||
.createQueryBuilder('person')
|
|
||||||
.update()
|
|
||||||
.set({
|
|
||||||
emails: {
|
|
||||||
primaryEmail: newEmailsPrimaryEmail,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: softDeletedPerson.id })
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
this.logger.log(
|
|
||||||
`Updated person ${softDeletedPerson.id} emailsPrimaryEmail from ${emailsPrimaryEmail} to ${newEmailsPrimaryEmail}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeNewFieldValues(fieldValue: string, i: number) {
|
|
||||||
return `${fieldValue}-old-${i}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-3363
File diff suppressed because it is too large
Load Diff
-489
@@ -1,489 +0,0 @@
|
|||||||
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 { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
|
||||||
import {
|
|
||||||
CALENDAR_CHANNEL_STANDARD_FIELD_IDS,
|
|
||||||
MESSAGE_CHANNEL_STANDARD_FIELD_IDS,
|
|
||||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
||||||
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
|
||||||
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-8:migrate-channel-sync-stages',
|
|
||||||
description:
|
|
||||||
'Migrate message and calendar channel sync stages: add PENDING_CONFIGURATION, add MESSAGE_LIST_FETCH_SCHEDULED, migrate deprecated stages',
|
|
||||||
})
|
|
||||||
export class MigrateChannelSyncStagesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
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(
|
|
||||||
`Migrating channel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.updateMessageChannelSyncStageFieldMetadata(workspaceId, options);
|
|
||||||
|
|
||||||
await this.updateCalendarChannelSyncStageFieldMetadata(
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
|
|
||||||
await this.migrateMessageChannelSyncStages(
|
|
||||||
workspaceId,
|
|
||||||
schemaName,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.migrateCalendarChannelSyncStages(
|
|
||||||
workspaceId,
|
|
||||||
schemaName,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Successfully migrated channel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async migrateMessageChannelSyncStages(
|
|
||||||
workspaceId: string,
|
|
||||||
schemaName: string,
|
|
||||||
options: RunOnWorkspaceArgs['options'],
|
|
||||||
): Promise<void> {
|
|
||||||
const messageChannelObject = await this.objectMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: STANDARD_OBJECT_IDS.messageChannel,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!messageChannelObject) {
|
|
||||||
this.logger.log(
|
|
||||||
`MessageChannel object not found for workspace ${workspaceId}, skipping schema migration`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableName = computeObjectTargetTable(messageChannelObject);
|
|
||||||
|
|
||||||
// Add new enum values for MessageChannelSyncStage
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would try to add PENDING_CONFIGURATION to messageChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."messageChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'PENDING_CONFIGURATION'`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
`Added PENDING_CONFIGURATION to messageChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error adding PENDING_CONFIGURATION to messageChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would try to add MESSAGE_LIST_FETCH_SCHEDULED to messageChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."messageChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'MESSAGE_LIST_FETCH_SCHEDULED'`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
`Added MESSAGE_LIST_FETCH_SCHEDULED to messageChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error adding MESSAGE_LIST_FETCH_SCHEDULED to messageChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate deprecated MessageChannel sync stages
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would migrate deprecated messageChannel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let messageChannelUpdateResult: any;
|
|
||||||
|
|
||||||
try {
|
|
||||||
messageChannelUpdateResult = await this.coreDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}"
|
|
||||||
SET "syncStage" = 'MESSAGE_LIST_FETCH_PENDING'
|
|
||||||
WHERE "syncStage" IN ('FULL_MESSAGE_LIST_FETCH_PENDING', 'PARTIAL_MESSAGE_LIST_FETCH_PENDING')`,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."messageChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'MESSAGE_LIST_FETCH_PENDING'`,
|
|
||||||
);
|
|
||||||
|
|
||||||
messageChannelUpdateResult = await this.coreDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}"
|
|
||||||
SET "syncStage" = 'MESSAGE_LIST_FETCH_PENDING'
|
|
||||||
WHERE "syncStage" IN ('FULL_MESSAGE_LIST_FETCH_PENDING', 'PARTIAL_MESSAGE_LIST_FETCH_PENDING')`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageChannelRowsUpdated = messageChannelUpdateResult[1] || 0;
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Migrated ${messageChannelRowsUpdated} messageChannel records from deprecated sync stages in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async migrateCalendarChannelSyncStages(
|
|
||||||
workspaceId: string,
|
|
||||||
schemaName: string,
|
|
||||||
options: RunOnWorkspaceArgs['options'],
|
|
||||||
): Promise<void> {
|
|
||||||
const calendarChannelObject = await this.objectMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: STANDARD_OBJECT_IDS.calendarChannel,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!calendarChannelObject) {
|
|
||||||
this.logger.log(
|
|
||||||
`CalendarChannel object not found for workspace ${workspaceId}, skipping schema migration`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableName = computeObjectTargetTable(calendarChannelObject);
|
|
||||||
|
|
||||||
// Add new enum values for CalendarChannelSyncStage
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would try to add PENDING_CONFIGURATION to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."calendarChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'PENDING_CONFIGURATION'`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
`Added PENDING_CONFIGURATION to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error adding PENDING_CONFIGURATION to calendarChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would try to add CALENDAR_EVENT_LIST_FETCH_SCHEDULED to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."calendarChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED'`,
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
`Added CALENDAR_EVENT_LIST_FETCH_SCHEDULED to calendarChannel_syncStage_enum for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error adding CALENDAR_EVENT_LIST_FETCH_SCHEDULED to calendarChannel_syncStage_enum for workspace ${workspaceId}: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate deprecated CalendarChannel sync stages
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would migrate deprecated calendarChannel sync stages for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let calendarChannelUpdateResult: any;
|
|
||||||
|
|
||||||
try {
|
|
||||||
calendarChannelUpdateResult = await this.coreDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}"
|
|
||||||
SET "syncStage" = 'CALENDAR_EVENT_LIST_FETCH_PENDING'
|
|
||||||
WHERE "syncStage" IN ('FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING')`,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
await this.coreDataSource.query(
|
|
||||||
`ALTER TYPE ${schemaName}."calendarChannel_syncStage_enum" ADD VALUE IF NOT EXISTS 'CALENDAR_EVENT_LIST_FETCH_PENDING'`,
|
|
||||||
);
|
|
||||||
|
|
||||||
calendarChannelUpdateResult = await this.coreDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${tableName}"
|
|
||||||
SET "syncStage" = 'CALENDAR_EVENT_LIST_FETCH_PENDING'
|
|
||||||
WHERE "syncStage" IN ('FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING')`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const calendarChannelRowsUpdated = calendarChannelUpdateResult[1] || 0;
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Migrated ${calendarChannelRowsUpdated} calendarChannel records from deprecated sync stages in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateMessageChannelSyncStageFieldMetadata(
|
|
||||||
workspaceId: string,
|
|
||||||
options: RunOnWorkspaceArgs['options'],
|
|
||||||
): Promise<void> {
|
|
||||||
const messageChannelObject = await this.objectMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: STANDARD_OBJECT_IDS.messageChannel,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!messageChannelObject) {
|
|
||||||
this.logger.log(
|
|
||||||
`MessageChannel object not found for workspace ${workspaceId}, skipping field metadata update`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncStageField = await this.fieldMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStage,
|
|
||||||
objectMetadataId: messageChannelObject.id,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!syncStageField) {
|
|
||||||
this.logger.log(
|
|
||||||
`MessageChannel syncStage field not found for workspace ${workspaceId}, skipping field metadata update`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldOptions = (syncStageField.options ||
|
|
||||||
[]) as FieldMetadataComplexOption[];
|
|
||||||
|
|
||||||
const hasMessageListFetchScheduled = fieldOptions.some(
|
|
||||||
(option) =>
|
|
||||||
option.value === MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPendingConfiguration = fieldOptions.some(
|
|
||||||
(option) =>
|
|
||||||
option.value === MessageChannelSyncStage.PENDING_CONFIGURATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasMessageListFetchPending = fieldOptions.some(
|
|
||||||
(option) =>
|
|
||||||
option.value === MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasMessageListFetchScheduled &&
|
|
||||||
hasPendingConfiguration &&
|
|
||||||
hasMessageListFetchPending
|
|
||||||
) {
|
|
||||||
this.logger.log(
|
|
||||||
`MessageChannel syncStage field metadata already migrated for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would update MessageChannel syncStage field metadata for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasMessageListFetchScheduled) {
|
|
||||||
fieldOptions.push({
|
|
||||||
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
|
|
||||||
label: 'Messages list fetch scheduled',
|
|
||||||
position: 1,
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPendingConfiguration) {
|
|
||||||
fieldOptions.push({
|
|
||||||
value: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
|
||||||
label: 'Pending configuration',
|
|
||||||
position: 9,
|
|
||||||
color: 'gray',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasMessageListFetchPending) {
|
|
||||||
fieldOptions.push({
|
|
||||||
value: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
|
||||||
label: 'Messages list fetch pending',
|
|
||||||
position: 0,
|
|
||||||
color: 'blue',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
syncStageField.options = fieldOptions;
|
|
||||||
|
|
||||||
await this.fieldMetadataRepository.save(syncStageField);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Updated MessageChannel syncStage field metadata for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateCalendarChannelSyncStageFieldMetadata(
|
|
||||||
workspaceId: string,
|
|
||||||
options: RunOnWorkspaceArgs['options'],
|
|
||||||
): Promise<void> {
|
|
||||||
const calendarChannelObject = await this.objectMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: STANDARD_OBJECT_IDS.calendarChannel,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!calendarChannelObject) {
|
|
||||||
this.logger.log(
|
|
||||||
`CalendarChannel object not found for workspace ${workspaceId}, skipping field metadata update`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncStageField = await this.fieldMetadataRepository.findOne({
|
|
||||||
where: {
|
|
||||||
standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStage,
|
|
||||||
objectMetadataId: calendarChannelObject.id,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!syncStageField) {
|
|
||||||
this.logger.log(
|
|
||||||
`CalendarChannel syncStage field not found for workspace ${workspaceId}, skipping field metadata update`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldOptions = (syncStageField.options ||
|
|
||||||
[]) as FieldMetadataComplexOption[];
|
|
||||||
|
|
||||||
const hasCalendarEventListFetchScheduled = fieldOptions.some(
|
|
||||||
(option) =>
|
|
||||||
option.value ===
|
|
||||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPendingConfiguration = fieldOptions.some(
|
|
||||||
(option) =>
|
|
||||||
option.value === CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasCalendarEventListFetchPending = fieldOptions.some(
|
|
||||||
(option) =>
|
|
||||||
option.value ===
|
|
||||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasCalendarEventListFetchScheduled &&
|
|
||||||
hasPendingConfiguration &&
|
|
||||||
hasCalendarEventListFetchPending
|
|
||||||
) {
|
|
||||||
this.logger.log(
|
|
||||||
`CalendarChannel syncStage field metadata already migrated for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
this.logger.log(
|
|
||||||
`Would update CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasCalendarEventListFetchScheduled) {
|
|
||||||
fieldOptions.push({
|
|
||||||
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
|
||||||
label: 'Calendar event list fetch scheduled',
|
|
||||||
position: 1,
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPendingConfiguration) {
|
|
||||||
fieldOptions.push({
|
|
||||||
value: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
|
||||||
label: 'Pending configuration',
|
|
||||||
position: 9,
|
|
||||||
color: 'gray',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasCalendarEventListFetchPending) {
|
|
||||||
fieldOptions.push({
|
|
||||||
value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
|
||||||
label: 'Calendar event list fetch pending',
|
|
||||||
position: 0,
|
|
||||||
color: 'blue',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
syncStageField.options = fieldOptions;
|
|
||||||
|
|
||||||
await this.fieldMetadataRepository.save(syncStageField);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Updated CalendarChannel syncStage field metadata for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-126
@@ -1,126 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { isString } from 'class-validator';
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import {
|
|
||||||
convertViewFilterOperandToCoreOperand,
|
|
||||||
isDefined,
|
|
||||||
} from 'twenty-shared/utils';
|
|
||||||
import { Raw, 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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';
|
|
||||||
import { isWorkflowFindRecordsAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-find-records-action.guard';
|
|
||||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-8:migrate-workflow-step-filter-operand-value',
|
|
||||||
description:
|
|
||||||
'Migrate workflowVersion.steps[].settings.input.stepFilters[].operand to use new operand enum',
|
|
||||||
})
|
|
||||||
export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
index,
|
|
||||||
total,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`[${index + 1}/${total}] Migrating workflow step filter operand values for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowVersionRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'workflowVersion',
|
|
||||||
{ shouldBypassPermissionChecks: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowVersionsToMigrate = await workflowVersionRepository.find({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
steps: Raw(
|
|
||||||
(_alias) => `"workflowVersion"."steps"::text LIKE :search`,
|
|
||||||
{
|
|
||||||
search: '%operand%',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Found ${workflowVersionsToMigrate.length} workflowVersions to migrate`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const workflowVersion of workflowVersionsToMigrate) {
|
|
||||||
let steps: WorkflowAction[] | null | undefined;
|
|
||||||
|
|
||||||
steps = workflowVersion.steps;
|
|
||||||
|
|
||||||
let hasChanged = false;
|
|
||||||
|
|
||||||
for (const step of steps ?? []) {
|
|
||||||
if (isWorkflowFilterAction(step)) {
|
|
||||||
for (const filter of step.settings.input.stepFilters ?? []) {
|
|
||||||
if (isDefined(filter.operand) && isString(filter.operand)) {
|
|
||||||
const newOperand = convertViewFilterOperandToCoreOperand(
|
|
||||||
filter.operand,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newOperand && newOperand !== filter.operand) {
|
|
||||||
filter.operand = newOperand;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isWorkflowFindRecordsAction(step)) {
|
|
||||||
for (const filter of step.settings.input.filter?.recordFilters ??
|
|
||||||
[]) {
|
|
||||||
if (isString(filter.operand)) {
|
|
||||||
const newOperand = convertViewFilterOperandToCoreOperand(
|
|
||||||
filter.operand,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newOperand && newOperand !== filter.operand) {
|
|
||||||
filter.operand = newOperand;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChanged) {
|
|
||||||
this.logger.log(
|
|
||||||
`${options.dryRun ? 'DRY RUN - Would be' : ''}Updating workflowVersion ${workflowVersion.id} in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!options.dryRun) {
|
|
||||||
await workflowVersionRepository.update(
|
|
||||||
{ id: workflowVersion.id },
|
|
||||||
{ steps },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`${options.dryRun ? 'DRY RUN - Would have' : ''} Updated workflowVersion ${workflowVersion.id} in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-174
@@ -1,174 +0,0 @@
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { DataSource, Repository, type QueryRunner } 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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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';
|
|
||||||
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
|
||||||
import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade:1-8:regenerate-person-search-vector-with-phones',
|
|
||||||
description:
|
|
||||||
'Regenerate person search vector to include phone number indexing for existing workspaces',
|
|
||||||
})
|
|
||||||
export class RegeneratePersonSearchVectorWithPhonesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(WorkspaceEntity)
|
|
||||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
||||||
@InjectDataSource()
|
|
||||||
private readonly coreDataSource: DataSource,
|
|
||||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
|
||||||
protected readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runOnWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
options,
|
|
||||||
}: RunOnWorkspaceArgs): Promise<void> {
|
|
||||||
await this.ensureUnaccentFunction();
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Regenerating person search vector for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
|
||||||
|
|
||||||
await queryRunner.connect();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
|
||||||
|
|
||||||
const newSearchVectorExpression = getTsVectorColumnExpressionFromFields(
|
|
||||||
SEARCH_FIELDS_FOR_PERSON,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isDryRun = Boolean(options.dryRun);
|
|
||||||
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
|
|
||||||
await this.applySearchVectorChanges(
|
|
||||||
schemaName,
|
|
||||||
workspaceId,
|
|
||||||
newSearchVectorExpression,
|
|
||||||
queryRunner,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDryRun) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`DRY RUN: Would regenerate person search vector for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Successfully regenerated person search vector for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (queryRunner.isTransactionActive) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to regenerate person search vector for workspace ${workspaceId}: ${error.message}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async applySearchVectorChanges(
|
|
||||||
schemaName: string,
|
|
||||||
workspaceId: string,
|
|
||||||
newSearchVectorExpression: string,
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Dropping existing searchVector index for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceSchemaManager.indexManager.dropIndex({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
indexName: 'IDX_person_searchVector',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Dropping existing searchVector column for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceSchemaManager.columnManager.dropColumns({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName: 'person',
|
|
||||||
columnNames: ['searchVector'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Creating new searchVector column with phone indexing for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceSchemaManager.columnManager.addColumns({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName: 'person',
|
|
||||||
columnDefinitions: [
|
|
||||||
{
|
|
||||||
name: 'searchVector',
|
|
||||||
type: 'tsvector',
|
|
||||||
isNullable: true,
|
|
||||||
asExpression: newSearchVectorExpression,
|
|
||||||
generatedType: 'STORED',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Recreating GIN index on searchVector for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceSchemaManager.indexManager.createIndex({
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName: 'person',
|
|
||||||
index: {
|
|
||||||
name: 'IDX_person_searchVector',
|
|
||||||
columns: ['searchVector'],
|
|
||||||
type: 'GIN',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureUnaccentFunction(): Promise<void> {
|
|
||||||
const result = await this.coreDataSource.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1 FROM pg_proc p
|
|
||||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
||||||
WHERE n.nspname = 'public'
|
|
||||||
AND p.proname = 'unaccent_immutable'
|
|
||||||
) as function_exists
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!result[0]?.function_exists) {
|
|
||||||
throw new Error(
|
|
||||||
'The public.unaccent_immutable() function is required but not found. Please run database migrations first.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
|
||||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
|
||||||
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
|
|
||||||
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';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
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 { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
|
||||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([
|
|
||||||
WorkspaceEntity,
|
|
||||||
FieldMetadataEntity,
|
|
||||||
ObjectMetadataEntity,
|
|
||||||
IndexMetadataEntity,
|
|
||||||
ServerlessFunctionEntity,
|
|
||||||
ServerlessFunctionLayerEntity,
|
|
||||||
]),
|
|
||||||
DataSourceModule,
|
|
||||||
IndexMetadataModule,
|
|
||||||
WorkspaceMigrationRunnerModule,
|
|
||||||
WorkspaceMigrationModule,
|
|
||||||
WorkspaceSchemaManagerModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
MigrateWorkflowStepFilterOperandValueCommand,
|
|
||||||
MigrateChannelSyncStagesCommand,
|
|
||||||
DeduplicateUniqueFieldsCommand,
|
|
||||||
FillNullServerlessFunctionLayerIdCommand,
|
|
||||||
RegeneratePersonSearchVectorWithPhonesCommand,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
MigrateChannelSyncStagesCommand,
|
|
||||||
MigrateWorkflowStepFilterOperandValueCommand,
|
|
||||||
DeduplicateUniqueFieldsCommand,
|
|
||||||
FillNullServerlessFunctionLayerIdCommand,
|
|
||||||
RegeneratePersonSearchVectorWithPhonesCommand,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class V1_8_UpgradeVersionCommandModule {}
|
|
||||||
-14
@@ -1,30 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
|
|
||||||
import { V1_11_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-11/1-11-upgrade-version-command.module';
|
|
||||||
import { V1_12_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module';
|
|
||||||
import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module';
|
import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module';
|
||||||
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
|
|
||||||
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
|
|
||||||
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 { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||||
V1_6_UpgradeVersionCommandModule,
|
|
||||||
V1_7_UpgradeVersionCommandModule,
|
|
||||||
V1_8_UpgradeVersionCommandModule,
|
|
||||||
V1_10_UpgradeVersionCommandModule,
|
|
||||||
V1_11_UpgradeVersionCommandModule,
|
|
||||||
V1_12_UpgradeVersionCommandModule,
|
|
||||||
V1_13_UpgradeVersionCommandModule,
|
V1_13_UpgradeVersionCommandModule,
|
||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
WorkspaceSyncMetadataModule,
|
|
||||||
],
|
],
|
||||||
providers: [UpgradeCommand],
|
providers: [UpgradeCommand],
|
||||||
})
|
})
|
||||||
|
|||||||
+18
-138
@@ -9,41 +9,18 @@ import {
|
|||||||
UpgradeCommandRunner,
|
UpgradeCommandRunner,
|
||||||
type VersionCommands,
|
type VersionCommands,
|
||||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||||
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
|
|
||||||
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
|
|
||||||
import { CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command';
|
|
||||||
import { FlushCacheCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-flush-cache.command';
|
|
||||||
import { MakeSureDashboardNamingAvailableCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command';
|
|
||||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
|
||||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
|
||||||
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
|
|
||||||
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
|
|
||||||
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 { CleanNullEquivalentValuesCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-clean-null-equivalent-values';
|
|
||||||
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 { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command';
|
import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command';
|
||||||
|
import { BackfillViewMainGroupByFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command';
|
||||||
|
import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command';
|
||||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||||
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
||||||
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
||||||
|
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-index.command';
|
||||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||||
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
|
|
||||||
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
|
|
||||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
|
||||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
|
||||||
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
|
|
||||||
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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'upgrade',
|
name: 'upgrade',
|
||||||
@@ -58,45 +35,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
|||||||
protected readonly twentyConfigService: TwentyConfigService,
|
protected readonly twentyConfigService: TwentyConfigService,
|
||||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||||
protected readonly dataSourceService: DataSourceService,
|
protected readonly dataSourceService: DataSourceService,
|
||||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
|
||||||
|
|
||||||
// 1.6 Commands
|
|
||||||
protected readonly fixLabelIdentifierPositionAndVisibilityCommand: FixLabelIdentifierPositionAndVisibilityCommand,
|
|
||||||
|
|
||||||
// 1.7 Commands
|
|
||||||
protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand,
|
|
||||||
|
|
||||||
// 1.8 Commands
|
|
||||||
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
|
|
||||||
protected readonly migrateWorkflowStepFilterOperandValueCommand: MigrateWorkflowStepFilterOperandValueCommand,
|
|
||||||
protected readonly deduplicateUniqueFieldsCommand: DeduplicateUniqueFieldsCommand,
|
|
||||||
protected readonly regeneratePersonSearchVectorWithPhonesCommand: RegeneratePersonSearchVectorWithPhonesCommand,
|
|
||||||
protected readonly migrateChannelSyncStagesCommand: MigrateChannelSyncStagesCommand,
|
|
||||||
|
|
||||||
// 1.10 Commands
|
|
||||||
protected readonly migrateAttachmentAuthorToCreatedByCommand: MigrateAttachmentAuthorToCreatedByCommand,
|
|
||||||
protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand,
|
|
||||||
protected readonly regenerateSearchVectorsCommand: RegenerateSearchVectorsCommand,
|
|
||||||
protected readonly addWorkflowRunStopStatusesCommand: AddWorkflowRunStopStatusesCommand,
|
|
||||||
protected readonly cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand: CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
|
|
||||||
protected readonly migrateChannelPartialFullSyncStagesCommand: MigrateChannelPartialFullSyncStagesCommand,
|
|
||||||
protected readonly makeSureDashboardNamingAvailableCommand: MakeSureDashboardNamingAvailableCommand,
|
|
||||||
protected readonly createViewKanbanFieldMetadataIdForeignKeyMigrationCommand: CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
|
|
||||||
protected readonly flushWorkspaceCacheCommand: FlushCacheCommand,
|
|
||||||
|
|
||||||
// 1.11 Commands
|
|
||||||
protected readonly cleanOrphanedUserWorkspacesCommand: CleanOrphanedUserWorkspacesCommand,
|
|
||||||
protected readonly cleanOrphanedRoleTargetsCommand: CleanOrphanedRoleTargetsCommand,
|
|
||||||
protected readonly seedStandardApplicationsCommand: CreateTwentyStandardApplicationCommand,
|
|
||||||
|
|
||||||
// 1.12 Commands
|
|
||||||
protected readonly setStandardApplicationNotUninstallableCommand: SetStandardApplicationNotUninstallableCommand,
|
|
||||||
protected readonly createTwentyStandardApplicationCommand: CreateTwentyStandardApplicationCommand,
|
|
||||||
protected readonly createWorkspaceCustomApplicationCommand: CreateWorkspaceCustomApplicationCommand,
|
|
||||||
protected readonly workspaceCustomApplicationIdNonNullableCommand: WorkspaceCustomApplicationIdNonNullableCommand,
|
|
||||||
protected readonly addMessagesImportScheduledSyncStageCommand: AddMessagesImportScheduledSyncStageCommand,
|
|
||||||
protected readonly addCalendarEventsImportScheduledSyncStageCommand: AddCalendarEventsImportScheduledSyncStageCommand,
|
|
||||||
protected readonly cleanNullEquivalentValuesCommand: CleanNullEquivalentValuesCommand,
|
|
||||||
|
|
||||||
// 1.13 Commands
|
// 1.13 Commands
|
||||||
protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand,
|
protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand,
|
||||||
@@ -104,90 +42,32 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
|||||||
protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||||
protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand,
|
protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand,
|
||||||
protected readonly migrateStandardInvalidEntitiesCommand: MigrateStandardInvalidEntitiesCommand,
|
protected readonly migrateStandardInvalidEntitiesCommand: MigrateStandardInvalidEntitiesCommand,
|
||||||
|
protected readonly backfillViewMainGroupByFieldMetadataIdCommand: BackfillViewMainGroupByFieldMetadataIdCommand,
|
||||||
|
protected readonly cleanEmptyStringNullInTextFieldsCommand: CleanEmptyStringNullInTextFieldsCommand,
|
||||||
|
protected readonly renameIndexNameCommand: RenameIndexNameCommand,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
workspaceRepository,
|
workspaceRepository,
|
||||||
twentyConfigService,
|
twentyConfigService,
|
||||||
globalWorkspaceOrmManager,
|
globalWorkspaceOrmManager,
|
||||||
dataSourceService,
|
dataSourceService,
|
||||||
syncWorkspaceMetadataCommand,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const commands_160: VersionCommands = {
|
// Note: Required empty commands array to allow retrieving previous version
|
||||||
beforeSyncMetadata: [this.fixLabelIdentifierPositionAndVisibilityCommand],
|
const commands_1120: VersionCommands = [];
|
||||||
afterSyncMetadata: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const commands_170: VersionCommands = {
|
const commands_1130: VersionCommands = [
|
||||||
beforeSyncMetadata: [
|
this.migrateTimelineActivityToMorphRelationsCommand,
|
||||||
this.backfillWorkflowManualTriggerAvailabilityCommand,
|
this.deduplicateRoleTargetsCommand,
|
||||||
],
|
this.updateRoleTargetsUniqueConstraintMigrationCommand,
|
||||||
afterSyncMetadata: [],
|
this.backfillPageLayoutUniversalIdentifiersCommand,
|
||||||
};
|
this.migrateStandardInvalidEntitiesCommand,
|
||||||
|
this.backfillViewMainGroupByFieldMetadataIdCommand,
|
||||||
const commands_180: VersionCommands = {
|
this.cleanEmptyStringNullInTextFieldsCommand,
|
||||||
beforeSyncMetadata: [
|
this.renameIndexNameCommand,
|
||||||
this.migrateWorkflowStepFilterOperandValueCommand,
|
];
|
||||||
this.deduplicateUniqueFieldsCommand,
|
|
||||||
this.regeneratePersonSearchVectorWithPhonesCommand,
|
|
||||||
this.migrateChannelSyncStagesCommand,
|
|
||||||
this.fillNullServerlessFunctionLayerIdCommand,
|
|
||||||
],
|
|
||||||
afterSyncMetadata: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const commands_1100: VersionCommands = {
|
|
||||||
beforeSyncMetadata: [
|
|
||||||
this.regenerateSearchVectorsCommand,
|
|
||||||
this.addWorkflowRunStopStatusesCommand,
|
|
||||||
this.cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
|
|
||||||
this.createViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
|
|
||||||
this.migrateChannelPartialFullSyncStagesCommand,
|
|
||||||
this.makeSureDashboardNamingAvailableCommand,
|
|
||||||
],
|
|
||||||
afterSyncMetadata: [
|
|
||||||
this.migrateAttachmentAuthorToCreatedByCommand,
|
|
||||||
this.migrateAttachmentTypeToFileCategoryCommand,
|
|
||||||
this.flushWorkspaceCacheCommand,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const commands_1110: VersionCommands = {
|
|
||||||
beforeSyncMetadata: [this.createTwentyStandardApplicationCommand],
|
|
||||||
afterSyncMetadata: [
|
|
||||||
this.cleanOrphanedUserWorkspacesCommand,
|
|
||||||
this.cleanOrphanedRoleTargetsCommand,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const commands_1120: VersionCommands = {
|
|
||||||
beforeSyncMetadata: [
|
|
||||||
this.createWorkspaceCustomApplicationCommand,
|
|
||||||
this.workspaceCustomApplicationIdNonNullableCommand,
|
|
||||||
this.addMessagesImportScheduledSyncStageCommand,
|
|
||||||
this.addCalendarEventsImportScheduledSyncStageCommand,
|
|
||||||
this.cleanNullEquivalentValuesCommand,
|
|
||||||
],
|
|
||||||
afterSyncMetadata: [this.setStandardApplicationNotUninstallableCommand],
|
|
||||||
};
|
|
||||||
|
|
||||||
const commands_1130: VersionCommands = {
|
|
||||||
beforeSyncMetadata: [
|
|
||||||
this.deduplicateRoleTargetsCommand,
|
|
||||||
this.updateRoleTargetsUniqueConstraintMigrationCommand,
|
|
||||||
this.migrateTimelineActivityToMorphRelationsCommand,
|
|
||||||
this.backfillPageLayoutUniversalIdentifiersCommand,
|
|
||||||
this.migrateStandardInvalidEntitiesCommand,
|
|
||||||
],
|
|
||||||
afterSyncMetadata: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.allCommands = {
|
this.allCommands = {
|
||||||
'1.6.0': commands_160,
|
|
||||||
'1.7.0': commands_170,
|
|
||||||
'1.8.0': commands_180,
|
|
||||||
'1.10.0': commands_1100,
|
|
||||||
'1.11.0': commands_1110,
|
|
||||||
'1.12.0': commands_1120,
|
'1.12.0': commands_1120,
|
||||||
'1.13.0': commands_1130,
|
'1.13.0': commands_1130,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user