add delete view fields without views command (#8728)

## Context
We recently added a command to ensure uniqueness on the viewId column in
the viewField table. This created some issues for some old workspaces
that had viewFields with an empty viewId.
This command should get rid of those and set the column as non-nullable.
Also updating the onDelete action accordingly and set one missing for
FavoriteFolder
This commit is contained in:
Weiko
2024-11-26 10:40:17 +01:00
committed by GitHub
parent 9f2e774113
commit a16b0d233e
8 changed files with 141 additions and 19 deletions

View File

@ -6,6 +6,7 @@ import { CommandRunner, Option } from 'nest-commander';
export type BaseCommandOptions = {
workspaceId?: string;
dryRun?: boolean;
verbose?: boolean;
};
export abstract class BaseCommandRunner extends CommandRunner {
@ -25,6 +26,14 @@ export abstract class BaseCommandRunner extends CommandRunner {
return true;
}
@Option({
flags: '--verbose',
description: 'Verbose output',
})
parseVerbose() {
return true;
}
override async run(
passedParams: string[],
options: BaseCommandOptions,

View File

@ -0,0 +1,95 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { IsNull, Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
interface DeleteViewFieldsWithoutViewsCommandOptions
extends ActiveWorkspacesCommandOptions {}
@Command({
name: 'upgrade-0.33:delete-view-fields-without-views',
description: 'Delete ViewFields that do not have a View',
})
export class DeleteViewFieldsWithoutViewsCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: DeleteViewFieldsWithoutViewsCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to delete ViewFields that do not have a View',
);
for (const workspaceId of workspaceIds) {
this.logger.log(`Running command for workspace ${workspaceId}`);
try {
await this.deleteViewFieldsWithoutViewsForWorkspace(
workspaceId,
options,
);
} catch (error) {
this.logger.log(
chalk.red(
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
),
);
continue;
} finally {
this.logger.log(
chalk.green(`Finished running command for workspace ${workspaceId}.`),
);
}
}
this.logger.log(chalk.green(`Command completed!`));
}
private async deleteViewFieldsWithoutViewsForWorkspace(
workspaceId: string,
options: DeleteViewFieldsWithoutViewsCommandOptions,
): Promise<void> {
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'viewField',
false,
);
const viewFieldsWithoutViews = await viewFieldRepository.find({
where: {
viewId: IsNull(),
},
});
const viewFieldIds = viewFieldsWithoutViews.map((vf) => vf.id);
if (!options.dryRun && viewFieldIds.length > 0) {
await viewFieldRepository.delete(viewFieldIds);
}
if (options.verbose) {
this.logger.log(
chalk.yellow(
`Deleted ${viewFieldsWithoutViews.length} ViewFields that do not have a View`,
),
);
}
}
}

View File

@ -17,7 +17,6 @@ interface EnforceUniqueConstraintsCommandOptions
company?: boolean;
viewField?: boolean;
viewSort?: boolean;
verbose?: boolean;
}
@Command({
@ -42,14 +41,6 @@ export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunn
return true;
}
@Option({
flags: '--verbose',
description: 'Verbose output',
})
parseVerbose() {
return true;
}
@Option({
flags: '--company',
description: 'Enforce unique constraints on company domainName',
@ -250,9 +241,16 @@ export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunn
.getRawMany();
for (const duplicate of duplicates) {
const { fieldMetadataId, viewId } = duplicate;
const {
viewField_fieldMetadataId: fieldMetadataId,
viewField_viewId: viewId,
} = duplicate;
const viewFields = await viewFieldRepository.find({
where: { fieldMetadataId, viewId, deletedAt: IsNull() },
where: {
fieldMetadataId,
viewId,
deletedAt: IsNull(),
},
order: { createdAt: 'DESC' },
});
@ -292,9 +290,16 @@ export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunn
.getRawMany();
for (const duplicate of duplicates) {
const { fieldMetadataId, viewId } = duplicate;
const {
viewSort_fieldMetadataId: fieldMetadataId,
viewSort_viewId: viewId,
} = duplicate;
const viewSorts = await viewSortRepository.find({
where: { fieldMetadataId, viewId, deletedAt: IsNull() },
where: {
fieldMetadataId,
viewId,
deletedAt: IsNull(),
},
order: { createdAt: 'DESC' },
});

View File

@ -4,6 +4,7 @@ import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { DeleteViewFieldsWithoutViewsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-delete-view-fields-without-views.command';
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-enforce-unique-constraints.command';
import { UpdateRichTextSearchVectorCommand } from 'src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -23,6 +24,7 @@ export class UpgradeTo0_33Command extends ActiveWorkspacesCommandRunner {
protected readonly workspaceRepository: Repository<Workspace>,
private readonly updateRichTextSearchVectorCommand: UpdateRichTextSearchVectorCommand,
private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand,
private readonly deleteViewFieldsWithoutViewsCommand: DeleteViewFieldsWithoutViewsCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
) {
super(workspaceRepository);
@ -33,6 +35,11 @@ export class UpgradeTo0_33Command extends ActiveWorkspacesCommandRunner {
options: UpdateTo0_33CommandOptions,
workspaceIds: string[],
): Promise<void> {
await this.deleteViewFieldsWithoutViewsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.enforceUniqueConstraintsCommand.executeActiveWorkspacesCommand(
passedParam,
{

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeleteViewFieldsWithoutViewsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-delete-view-fields-without-views.command';
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-enforce-unique-constraints.command';
import { UpdateRichTextSearchVectorCommand } from 'src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression';
import { UpgradeTo0_33Command } from 'src/database/commands/upgrade-version/0-33/0-33-upgrade-version.command';
@ -26,6 +27,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
UpgradeTo0_33Command,
UpdateRichTextSearchVectorCommand,
EnforceUniqueConstraintsCommand,
DeleteViewFieldsWithoutViewsCommand,
],
})
export class UpgradeTo0_33CommandModule {}

View File

@ -2,7 +2,10 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
@ -53,6 +56,7 @@ export class FavoriteFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconHeart',
inverseSideFieldKey: 'favoriteFolder',
inverseSideTarget: () => FavoriteWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL,
})
favorites: Relation<FavoriteWorkspaceEntity[]>;
}

View File

@ -1,3 +1,5 @@
import { Relation } from 'typeorm';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
@ -5,7 +7,6 @@ import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-enti
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@ -77,9 +78,8 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => ViewWorkspaceEntity,
inverseSideFieldKey: 'viewFields',
})
@WorkspaceIsNullable()
view?: ViewWorkspaceEntity | null;
view: Relation<ViewWorkspaceEntity>;
@WorkspaceJoinColumn('view')
viewId: string | null;
viewId: string;
}

View File

@ -111,7 +111,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
description: 'View Fields',
icon: 'IconTag',
inverseSideTarget: () => ViewFieldWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
viewFields: Relation<ViewFieldWorkspaceEntity[]>;