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 { 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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
class BasicUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||
allCommands = {
|
||||
'1.0.0': {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
},
|
||||
'2.0.0': {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
},
|
||||
'1.0.0': [],
|
||||
'2.0.0': [],
|
||||
};
|
||||
}
|
||||
|
||||
class InvalidUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||
allCommands = {
|
||||
invalid: {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
},
|
||||
'2.0.0': {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
},
|
||||
invalid: [],
|
||||
'2.0.0': [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,14 +73,12 @@ const buildUpgradeCommandModule = async ({
|
||||
twentyConfigService: TwentyConfigService,
|
||||
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
dataSourceService: DataSourceService,
|
||||
syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
) => {
|
||||
return new commandRunner(
|
||||
workspaceRepository,
|
||||
twentyConfigService,
|
||||
globalWorkspaceOrmManager,
|
||||
dataSourceService,
|
||||
syncWorkspaceMetadataCommand,
|
||||
);
|
||||
},
|
||||
inject: [
|
||||
@@ -101,7 +86,6 @@ const buildUpgradeCommandModule = async ({
|
||||
TwentyConfigService,
|
||||
GlobalWorkspaceOrmManager,
|
||||
DataSourceService,
|
||||
SyncWorkspaceMetadataCommand,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -146,12 +130,6 @@ const buildUpgradeCommandModule = async ({
|
||||
provide: DataSourceService,
|
||||
useValue: mockDataSourceService,
|
||||
},
|
||||
{
|
||||
provide: SyncWorkspaceMetadataCommand,
|
||||
useValue: {
|
||||
runOnWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -161,9 +139,6 @@ const buildUpgradeCommandModule = async ({
|
||||
describe('UpgradeCommandRunner', () => {
|
||||
let upgradeCommandRunner: BasicUpgradeCommandRunner;
|
||||
let workspaceRepository: Repository<WorkspaceEntity>;
|
||||
let syncWorkspaceMetadataCommand: jest.Mocked<SyncWorkspaceMetadataCommand>;
|
||||
let runAfterSyncMetadataSpy: jest.SpyInstance;
|
||||
let runBeforeSyncMetadataSpy: jest.SpyInstance;
|
||||
let runCoreMigrationsSpy: jest.SpyInstance;
|
||||
let globalWorkspaceOrmManagerSpy: GlobalWorkspaceOrmManager;
|
||||
|
||||
@@ -201,14 +176,6 @@ describe('UpgradeCommandRunner', () => {
|
||||
jest.spyOn(upgradeCommandRunner['logger'], 'error').mockImplementation();
|
||||
jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation();
|
||||
|
||||
runBeforeSyncMetadataSpy = jest.spyOn(
|
||||
upgradeCommandRunner,
|
||||
'runBeforeSyncMetadata',
|
||||
);
|
||||
runAfterSyncMetadataSpy = jest.spyOn(
|
||||
upgradeCommandRunner,
|
||||
'runAfterSyncMetadata',
|
||||
);
|
||||
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
|
||||
runCoreMigrationsSpy = jest
|
||||
.spyOn(upgradeCommandRunner, 'runCoreMigrations')
|
||||
@@ -217,7 +184,6 @@ describe('UpgradeCommandRunner', () => {
|
||||
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
|
||||
getRepositoryToken(WorkspaceEntity),
|
||||
);
|
||||
syncWorkspaceMetadataCommand = module.get(SyncWorkspaceMetadataCommand);
|
||||
globalWorkspaceOrmManagerSpy = module.get<GlobalWorkspaceOrmManager>(
|
||||
GlobalWorkspaceOrmManager,
|
||||
);
|
||||
@@ -253,12 +219,9 @@ describe('UpgradeCommandRunner', () => {
|
||||
upgradeCommandRunner.runOnWorkspace,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
||||
|
||||
[
|
||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||
upgradeCommandRunner.runAfterSyncMetadata,
|
||||
workspaceRepository.update,
|
||||
].forEach((fn) => expect(fn).not.toHaveBeenCalled());
|
||||
[workspaceRepository.update].forEach((fn) =>
|
||||
expect(fn).not.toHaveBeenCalled(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should run upgrade over several workspaces', async () => {
|
||||
@@ -278,9 +241,6 @@ describe('UpgradeCommandRunner', () => {
|
||||
|
||||
[
|
||||
upgradeCommandRunner.runOnWorkspace,
|
||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||
upgradeCommandRunner.runAfterSyncMetadata,
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace));
|
||||
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
|
||||
@@ -292,35 +252,6 @@ describe('UpgradeCommandRunner', () => {
|
||||
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 ', () => {
|
||||
const successfulTestUseCases: EachTestingContext<{
|
||||
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
|
||||
@@ -379,8 +310,6 @@ describe('UpgradeCommandRunner', () => {
|
||||
expect(failReport.length).toBe(0);
|
||||
expect(successReport.length).toBe(1);
|
||||
expect(runCoreMigrationsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(runAfterSyncMetadataSpy).toHaveBeenCalledTimes(1);
|
||||
expect(runBeforeSyncMetadataSpy).toHaveBeenCalledTimes(1);
|
||||
const { workspaceId } = successReport[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 { 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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
import {
|
||||
type CompareVersionMajorAndMinorReturnType,
|
||||
compareVersionMajorAndMinor,
|
||||
} from 'src/utils/version/compare-version-minor-and-major';
|
||||
import { getPreviousVersion } from 'src/utils/version/get-previous-version';
|
||||
|
||||
export type VersionCommands = {
|
||||
beforeSyncMetadata: (
|
||||
| WorkspacesMigrationCommandRunner
|
||||
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
||||
)[];
|
||||
afterSyncMetadata: (
|
||||
| WorkspacesMigrationCommandRunner
|
||||
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
||||
)[];
|
||||
};
|
||||
export type VersionCommands = (
|
||||
| WorkspacesMigrationCommandRunner
|
||||
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
|
||||
)[];
|
||||
export type AllCommands = Record<string, VersionCommands>;
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
@@ -54,7 +47,6 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
|
||||
protected readonly twentyConfigService: TwentyConfigService,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
@@ -184,7 +176,7 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
|
||||
'Initialized upgrade context with:',
|
||||
`- currentVersion (migrating to): ${currentAppVersion}`,
|
||||
`- fromWorkspaceVersion: ${previousVersion}`,
|
||||
`- ${this.commands.beforeSyncMetadata.length + this.commands.afterSyncMetadata.length} commands`,
|
||||
`- ${this.commands.length} commands`,
|
||||
];
|
||||
|
||||
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': {
|
||||
await this.runBeforeSyncMetadata(args);
|
||||
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
||||
await this.runAfterSyncMetadata(args);
|
||||
for (const command of this.commands) {
|
||||
await command.runOnWorkspace(args);
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
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() {
|
||||
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 { 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 { 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 { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
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 { 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_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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
V1_6_UpgradeVersionCommandModule,
|
||||
V1_7_UpgradeVersionCommandModule,
|
||||
V1_8_UpgradeVersionCommandModule,
|
||||
V1_10_UpgradeVersionCommandModule,
|
||||
V1_11_UpgradeVersionCommandModule,
|
||||
V1_12_UpgradeVersionCommandModule,
|
||||
V1_13_UpgradeVersionCommandModule,
|
||||
DataSourceModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
],
|
||||
providers: [UpgradeCommand],
|
||||
})
|
||||
|
||||
+18
-138
@@ -9,41 +9,18 @@ import {
|
||||
UpgradeCommandRunner,
|
||||
type VersionCommands,
|
||||
} 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 { 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 { 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 { 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 { 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 { 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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade',
|
||||
@@ -58,45 +35,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly twentyConfigService: TwentyConfigService,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
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
|
||||
protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand,
|
||||
@@ -104,90 +42,32 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
protected readonly migrateStandardInvalidEntitiesCommand: MigrateStandardInvalidEntitiesCommand,
|
||||
protected readonly backfillViewMainGroupByFieldMetadataIdCommand: BackfillViewMainGroupByFieldMetadataIdCommand,
|
||||
protected readonly cleanEmptyStringNullInTextFieldsCommand: CleanEmptyStringNullInTextFieldsCommand,
|
||||
protected readonly renameIndexNameCommand: RenameIndexNameCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
twentyConfigService,
|
||||
globalWorkspaceOrmManager,
|
||||
dataSourceService,
|
||||
syncWorkspaceMetadataCommand,
|
||||
);
|
||||
|
||||
const commands_160: VersionCommands = {
|
||||
beforeSyncMetadata: [this.fixLabelIdentifierPositionAndVisibilityCommand],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
// Note: Required empty commands array to allow retrieving previous version
|
||||
const commands_1120: VersionCommands = [];
|
||||
|
||||
const commands_170: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.backfillWorkflowManualTriggerAvailabilityCommand,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_180: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
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: [],
|
||||
};
|
||||
const commands_1130: VersionCommands = [
|
||||
this.migrateTimelineActivityToMorphRelationsCommand,
|
||||
this.deduplicateRoleTargetsCommand,
|
||||
this.updateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
this.backfillPageLayoutUniversalIdentifiersCommand,
|
||||
this.migrateStandardInvalidEntitiesCommand,
|
||||
this.backfillViewMainGroupByFieldMetadataIdCommand,
|
||||
this.cleanEmptyStringNullInTextFieldsCommand,
|
||||
this.renameIndexNameCommand,
|
||||
];
|
||||
|
||||
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.13.0': commands_1130,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user