[feat] Enable deletion of custom fields in workspace (#4780)
**Context** cf. feature request [#4597](https://github.com/twentyhq/twenty/issues/4597) Enables deletion of custom fields that aren't active nor of type relation Also 1. renamed a misnamed file 2. deleted redundant hook BeforeDeleteOneField as it seemed best to move the logic to the resolver instead **How was it tested?** Did not write unit tests as code is to be migrated (discussed with @Weiko). Locally tested. --------- Co-authored-by: Marie Stoppa <mariestoppa@MacBook-Pro-de-Marie.local>
This commit is contained in:
@ -0,0 +1,9 @@
|
|||||||
|
import { InputType, ID } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class DeleteOneFieldInput {
|
||||||
|
@IDField(() => ID, { description: 'The id of the field to delete.' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
@ -9,7 +9,6 @@ import {
|
|||||||
import { GraphQLJSON } from 'graphql-type-json';
|
import { GraphQLJSON } from 'graphql-type-json';
|
||||||
import {
|
import {
|
||||||
Authorize,
|
Authorize,
|
||||||
BeforeDeleteOne,
|
|
||||||
FilterableField,
|
FilterableField,
|
||||||
IDField,
|
IDField,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
@ -31,7 +30,6 @@ import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-met
|
|||||||
|
|
||||||
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
|
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { BeforeDeleteOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-delete-one-field.hook';
|
|
||||||
import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator';
|
import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator';
|
||||||
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
|
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
|
||||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||||
@ -52,7 +50,6 @@ registerEnumType(FieldMetadataType, {
|
|||||||
disableSort: true,
|
disableSort: true,
|
||||||
maxResultsSize: 1000,
|
maxResultsSize: 1000,
|
||||||
})
|
})
|
||||||
@BeforeDeleteOne(BeforeDeleteOneField)
|
|
||||||
@Relation('toRelationMetadata', () => RelationMetadataDTO, {
|
@Relation('toRelationMetadata', () => RelationMetadataDTO, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -61,7 +61,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
|||||||
one: { disabled: true },
|
one: { disabled: true },
|
||||||
many: { disabled: true },
|
many: { disabled: true },
|
||||||
},
|
},
|
||||||
delete: { many: { disabled: true } },
|
delete: { disabled: true },
|
||||||
guards: [JwtAuthGuard],
|
guards: [JwtAuthGuard],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
UnauthorizedException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
Mutation,
|
Mutation,
|
||||||
@ -11,9 +15,11 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||||
|
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
||||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||||
import { RelationDefinitionDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto';
|
import { RelationDefinitionDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto';
|
||||||
import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ -43,6 +49,43 @@ export class FieldMetadataResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => FieldMetadataDTO)
|
||||||
|
async deleteOneField(
|
||||||
|
@Args('input') input: DeleteOneFieldInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
if (!workspaceId) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMetadata =
|
||||||
|
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||||
|
where: {
|
||||||
|
id: input.id.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fieldMetadata) {
|
||||||
|
throw new BadRequestException('Field does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fieldMetadata.isCustom) {
|
||||||
|
throw new BadRequestException("Standard Fields can't be deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldMetadata.isActive) {
|
||||||
|
throw new BadRequestException("Active fields can't be deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Relation fields can't be deleted, you need to delete the RelationMetadata instead",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fieldMetadataService.deleteOneField(input, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
@ResolveField(() => RelationDefinitionDTO, { nullable: true })
|
@ResolveField(() => RelationDefinitionDTO, { nullable: true })
|
||||||
async relationDefinition(
|
async relationDefinition(
|
||||||
@Parent() fieldMetadata: FieldMetadataDTO,
|
@Parent() fieldMetadata: FieldMetadataDTO,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metada
|
|||||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||||
import {
|
import {
|
||||||
WorkspaceMigrationColumnActionType,
|
WorkspaceMigrationColumnActionType,
|
||||||
|
WorkspaceMigrationColumnDrop,
|
||||||
WorkspaceMigrationTableAction,
|
WorkspaceMigrationTableAction,
|
||||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||||
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
|
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
|
||||||
@ -35,6 +36,8 @@ import {
|
|||||||
RelationMetadataEntity,
|
RelationMetadataEntity,
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
||||||
|
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FieldMetadataEntity,
|
FieldMetadataEntity,
|
||||||
@ -358,6 +361,80 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteOneField(
|
||||||
|
input: DeleteOneFieldInput,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<FieldMetadataEntity> {
|
||||||
|
const queryRunner = this.metadataDataSource.createQueryRunner();
|
||||||
|
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction(); // transaction not safe as a different queryRunner is used within workspaceMigrationRunnerService
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fieldMetadataRepository =
|
||||||
|
queryRunner.manager.getRepository<FieldMetadataEntity<'default'>>(
|
||||||
|
FieldMetadataEntity,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldMetadata = await fieldMetadataRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: input.id,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fieldMetadata) {
|
||||||
|
throw new NotFoundException('Field does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectMetadata =
|
||||||
|
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||||
|
where: {
|
||||||
|
id: fieldMetadata?.objectMetadataId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!objectMetadata) {
|
||||||
|
throw new NotFoundException('Object does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fieldMetadataRepository.delete(fieldMetadata.id);
|
||||||
|
|
||||||
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
|
generateMigrationName(`delete-${fieldMetadata.name}`),
|
||||||
|
workspaceId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: computeObjectTargetTable(objectMetadata),
|
||||||
|
action: 'alter',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
action: WorkspaceMigrationColumnActionType.DROP,
|
||||||
|
columnName: computeCustomName(
|
||||||
|
fieldMetadata.name,
|
||||||
|
fieldMetadata.isCustom,
|
||||||
|
),
|
||||||
|
} satisfies WorkspaceMigrationColumnDrop,
|
||||||
|
],
|
||||||
|
} satisfies WorkspaceMigrationTableAction,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return fieldMetadata;
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async findOneOrFail(
|
public async findOneOrFail(
|
||||||
id: string,
|
id: string,
|
||||||
options?: FindOneOptions<FieldMetadataEntity>,
|
options?: FindOneOptions<FieldMetadataEntity>,
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BeforeDeleteOneHook,
|
|
||||||
DeleteOneInputType,
|
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
|
||||||
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BeforeDeleteOneField implements BeforeDeleteOneHook<any> {
|
|
||||||
constructor(readonly fieldMetadataService: FieldMetadataService) {}
|
|
||||||
|
|
||||||
async run(
|
|
||||||
instance: DeleteOneInputType,
|
|
||||||
context: any,
|
|
||||||
): Promise<DeleteOneInputType> {
|
|
||||||
const workspaceId = context?.req?.user?.workspace?.id;
|
|
||||||
|
|
||||||
if (!workspaceId) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldMetadata =
|
|
||||||
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
|
|
||||||
where: {
|
|
||||||
id: instance.id.toString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fieldMetadata) {
|
|
||||||
throw new BadRequestException('Field does not exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fieldMetadata.isCustom) {
|
|
||||||
throw new BadRequestException("Standard Fields can't be deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldMetadata.isActive) {
|
|
||||||
throw new BadRequestException("Active fields can't be deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Relation fields can't be deleted, you need to delete the RelationMetadata instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,7 +17,7 @@ import {
|
|||||||
|
|
||||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BeforeDeleteOneRelation } from 'src/engine/metadata-modules/relation-metadata/hooks/before-delete-one-field.hook';
|
import { BeforeDeleteOneRelation } from 'src/engine/metadata-modules/relation-metadata/hooks/before-delete-one-relation.hook';
|
||||||
|
|
||||||
registerEnumType(RelationMetadataType, {
|
registerEnumType(RelationMetadataType, {
|
||||||
name: 'RelationMetadataType',
|
name: 'RelationMetadataType',
|
||||||
|
|||||||
Reference in New Issue
Block a user