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:
Paul Rastoin
2025-12-11 14:50:56 +01:00
committed by GitHub
parent 9c1197c0b2
commit f06a5ccacd
38 changed files with 34 additions and 7855 deletions
@@ -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');
@@ -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');
@@ -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}`,
);
}
}
}
}
@@ -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`,
);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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}...`,
);
}
}
@@ -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}`,
);
}
}
@@ -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}`,
);
}
}
@@ -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}`,
);
}
}
@@ -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;
}
@@ -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(', ')}`,
);
}
}
@@ -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 {}
@@ -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;
}
}
@@ -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}`,
);
}
}
@@ -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);
}
}
}
@@ -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 {}
@@ -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}`,
);
}
}
@@ -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}`,
);
}
}
@@ -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 });
}
}
@@ -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`);
}
}
@@ -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);
}
}
}
@@ -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 {}
@@ -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();
}
}
}
}
@@ -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';
@@ -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`,
);
}
}
}
}
@@ -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 {}
@@ -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],
);
}
}
}
@@ -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 {}
@@ -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}`;
}
}
@@ -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}`,
);
}
}
@@ -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}`,
);
}
}
}
}
@@ -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.',
);
}
}
}
@@ -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 {}
@@ -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],
})
@@ -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,
};