Add deleteOneObject mutation (#3682)

* Add deleteOneObject mutation

* codegen

* move relationToDelete to dedicated file

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2024-01-30 09:47:58 +01:00
committed by GitHub
parent 49f33bbe2e
commit a9349f9fea
10 changed files with 210 additions and 51 deletions

View File

@@ -220,7 +220,7 @@ export type Mutation = {
createOneObject: Object;
createOneRelation: Relation;
deleteOneField: FieldDeleteResponse;
deleteOneObject: ObjectDeleteResponse;
deleteOneObject: Object;
deleteOneRelation: RelationDeleteResponse;
deleteUser: User;
updateOneField: Field;
@@ -297,25 +297,6 @@ export type ObjectConnection = {
totalCount: Scalars['Int']['output'];
};
export type ObjectDeleteResponse = {
__typename?: 'ObjectDeleteResponse';
createdAt?: Maybe<Scalars['DateTime']['output']>;
dataSourceId?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
icon?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']['output']>;
isActive?: Maybe<Scalars['Boolean']['output']>;
isCustom?: Maybe<Scalars['Boolean']['output']>;
isSystem?: Maybe<Scalars['Boolean']['output']>;
labelIdentifierFieldMetadataId?: Maybe<Scalars['String']['output']>;
labelPlural?: Maybe<Scalars['String']['output']>;
labelSingular?: Maybe<Scalars['String']['output']>;
namePlural?: Maybe<Scalars['String']['output']>;
nameSingular?: Maybe<Scalars['String']['output']>;
updatedAt?: Maybe<Scalars['DateTime']['output']>;
};
export type ObjectFieldsConnection = {
__typename?: 'ObjectFieldsConnection';
/** Array of edges. */
@@ -450,8 +431,8 @@ export type TimelineThreadParticipant = {
firstName: Scalars['String']['output'];
handle: Scalars['String']['output'];
lastName: Scalars['String']['output'];
personId?: Maybe<Scalars['String']['output']>;
workspaceMemberId?: Maybe<Scalars['String']['output']>;
personId?: Maybe<Scalars['ID']['output']>;
workspaceMemberId?: Maybe<Scalars['ID']['output']>;
};
export type UpdateFieldInput = {
@@ -696,7 +677,7 @@ export type DeleteOneObjectMetadataItemMutationVariables = Exact<{
}>;
export type DeleteOneObjectMetadataItemMutation = { __typename?: 'Mutation', deleteOneObject: { __typename?: 'ObjectDeleteResponse', id?: string | null, dataSourceId?: string | null, nameSingular?: string | null, namePlural?: string | null, labelSingular?: string | null, labelPlural?: string | null, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, createdAt?: any | null, updatedAt?: any | null, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null } };
export type DeleteOneObjectMetadataItemMutation = { __typename?: 'Mutation', deleteOneObject: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isActive: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null } };
export type DeleteOneFieldMetadataItemMutationVariables = Exact<{
idToDelete: Scalars['ID']['input'];

View File

@@ -97,6 +97,11 @@ export type CursorPaging = {
last?: InputMaybe<Scalars['Int']>;
};
export type DeleteOneObjectInput = {
/** The id of the record to delete. */
id: Scalars['ID'];
};
export type EmailPasswordResetLink = {
__typename?: 'EmailPasswordResetLink';
/** Boolean that confirms query was dispatched */
@@ -223,7 +228,7 @@ export type Mutation = {
createOneObject: Object;
createOneRefreshToken: RefreshToken;
deleteCurrentWorkspace: Workspace;
deleteOneObject: ObjectDeleteResponse;
deleteOneObject: Object;
deleteUser: User;
emailPasswordResetLink: EmailPasswordResetLink;
generateApiKeyToken: ApiKeyToken;
@@ -259,6 +264,11 @@ export type MutationCreateOneRefreshTokenArgs = {
};
export type MutationDeleteOneObjectArgs = {
input: DeleteOneObjectInput;
};
export type MutationGenerateApiKeyTokenArgs = {
apiKeyId: Scalars['String'];
expiresAt: Scalars['String'];
@@ -329,25 +339,6 @@ export type ObjectConnection = {
totalCount: Scalars['Int'];
};
export type ObjectDeleteResponse = {
__typename?: 'ObjectDeleteResponse';
createdAt?: Maybe<Scalars['DateTime']>;
dataSourceId?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['ID']>;
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
isActive?: Maybe<Scalars['Boolean']>;
isCustom?: Maybe<Scalars['Boolean']>;
isSystem?: Maybe<Scalars['Boolean']>;
labelIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
labelPlural?: Maybe<Scalars['String']>;
labelSingular?: Maybe<Scalars['String']>;
namePlural?: Maybe<Scalars['String']>;
nameSingular?: Maybe<Scalars['String']>;
updatedAt?: Maybe<Scalars['DateTime']>;
};
export type ObjectFieldsConnection = {
__typename?: 'ObjectFieldsConnection';
/** Array of edges. */

View File

@@ -0,0 +1,12 @@
import { ID, InputType } from '@nestjs/graphql';
import { BeforeDeleteOne, IDField } from '@ptc-org/nestjs-query-graphql';
import { BeforeDeleteOneObject } from 'src/metadata/object-metadata/hooks/before-delete-one-object.hook';
@InputType()
@BeforeDeleteOne(BeforeDeleteOneObject)
export class DeleteOneObjectInput {
@IDField(() => ID, { description: 'The id of the record to delete.' })
id!: string;
}

View File

@@ -14,6 +14,7 @@ import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { ObjectMetadataResolver } from 'src/metadata/object-metadata/object-metadata.resolver';
import { ObjectMetadataService } from './object-metadata.service';
import { ObjectMetadataEntity } from './object-metadata.entity';
@@ -54,13 +55,13 @@ import { ObjectMetadataDTO } from './dtos/object-metadata.dto';
update: {
many: { disabled: true },
},
delete: { many: { disabled: true } },
delete: { disabled: true },
guards: [JwtAuthGuard],
},
],
}),
],
providers: [ObjectMetadataService],
providers: [ObjectMetadataService, ObjectMetadataResolver],
exports: [ObjectMetadataService],
})
export class ObjectMetadataModule {}

View File

@@ -0,0 +1,23 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { ObjectMetadataDTO } from 'src/metadata/object-metadata/dtos/object-metadata.dto';
import { DeleteOneObjectInput } from 'src/metadata/object-metadata/dtos/delete-object.input';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => ObjectMetadataDTO)
export class ObjectMetadataResolver {
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
@Mutation(() => ObjectMetadataDTO)
deleteOneObject(
@Args('input') input: DeleteOneObjectInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.objectMetadataService.deleteOneObject(input, workspaceId);
}
}

View File

@@ -1,6 +1,12 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import console from 'console';
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
@@ -10,6 +16,7 @@ import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migrati
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationColumnDrop,
WorkspaceMigrationTableAction,
} from 'src/metadata/workspace-migration/workspace-migration.entity';
import {
@@ -22,7 +29,12 @@ import {
RelationMetadataEntity,
RelationMetadataType,
} from 'src/metadata/relation-metadata/relation-metadata.entity';
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
import {
computeCustomName,
computeObjectTargetTable,
} from 'src/workspace/utils/compute-object-target-table.util';
import { DeleteOneObjectInput } from 'src/metadata/object-metadata/dtos/delete-object.input';
import { RelationToDelete } from 'src/metadata/relation-metadata/types/relation-to-delete';
import { ObjectMetadataEntity } from './object-metadata.entity';
@@ -63,6 +75,123 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
return result;
}
public async deleteOneObject(
input: DeleteOneObjectInput,
workspaceId: string,
): Promise<ObjectMetadataEntity> {
const objectMetadata = await this.objectMetadataRepository.findOne({
relations: [
'fromRelations.fromFieldMetadata',
'fromRelations.toFieldMetadata',
'toRelations.fromFieldMetadata',
'toRelations.toFieldMetadata',
'fromRelations.fromObjectMetadata',
'fromRelations.toObjectMetadata',
'toRelations.fromObjectMetadata',
'toRelations.toObjectMetadata',
],
where: {
id: input.id,
workspaceId,
},
});
if (!objectMetadata) {
throw new NotFoundException('Object does not exist');
}
const relationsToDelete: RelationToDelete[] = [];
// TODO: Most of this logic should be moved to relation-metadata.service.ts
for (const relation of [
...objectMetadata.fromRelations,
...objectMetadata.toRelations,
]) {
relationsToDelete.push({
id: relation.id,
fromFieldMetadataId: relation.fromFieldMetadata.id,
toFieldMetadataId: relation.toFieldMetadata.id,
fromFieldMetadataName: relation.fromFieldMetadata.name,
toFieldMetadataName: relation.toFieldMetadata.name,
fromObjectMetadataId: relation.fromObjectMetadata.id,
toObjectMetadataId: relation.toObjectMetadata.id,
fromObjectName: relation.fromObjectMetadata.nameSingular,
toObjectName: relation.toObjectMetadata.nameSingular,
toFieldMetadataIsCustom: relation.toFieldMetadata.isCustom,
toObjectMetadataIsCustom: relation.toObjectMetadata.isCustom,
direction:
relation.fromObjectMetadata.nameSingular ===
objectMetadata.nameSingular
? 'from'
: 'to',
});
}
await this.relationMetadataRepository.delete(
relationsToDelete.map((relation) => relation.id),
);
for (const relationToDelete of relationsToDelete) {
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
where: {
name: `${relationToDelete.toFieldMetadataName}Id`,
objectMetadataId: relationToDelete.toObjectMetadataId,
workspaceId,
},
});
const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map(
(field) => field.id,
);
await this.fieldMetadataRepository.delete([
...foreignKeyFieldsToDeleteIds,
relationToDelete.fromFieldMetadataId,
relationToDelete.toFieldMetadataId,
]);
if (relationToDelete.direction === 'from') {
await this.workspaceMigrationService.createCustomMigration(
workspaceId,
[
{
name: computeCustomName(
relationToDelete.toObjectName,
relationToDelete.toObjectMetadataIsCustom,
),
action: 'alter',
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeCustomName(
`${relationToDelete.toFieldMetadataName}Id`,
relationToDelete.toFieldMetadataIsCustom,
),
} satisfies WorkspaceMigrationColumnDrop,
],
},
],
);
}
}
await this.objectMetadataRepository.delete(objectMetadata.id);
// DROP TABLE
await this.workspaceMigrationService.createCustomMigration(workspaceId, [
{
name: computeObjectTargetTable(objectMetadata),
action: 'drop',
},
]);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
return objectMetadata;
}
override async createOne(
objectMetadataInput: CreateObjectInput,
): Promise<ObjectMetadataEntity> {

View File

@@ -0,0 +1,14 @@
export type RelationToDelete = {
id: string;
fromFieldMetadataId: string;
toFieldMetadataId: string;
fromFieldMetadataName: string;
toFieldMetadataName: string;
fromObjectMetadataId: string;
toObjectMetadataId: string;
fromObjectName: string;
toObjectName: string;
toFieldMetadataIsCustom: boolean;
toObjectMetadataIsCustom: boolean;
direction: string;
};

View File

@@ -58,7 +58,7 @@ export type WorkspaceMigrationColumnAction = {
export type WorkspaceMigrationTableAction = {
name: string;
action: 'create' | 'alter';
action: 'create' | 'alter' | 'drop';
columns?: WorkspaceMigrationColumnAction[];
};

View File

@@ -3,7 +3,12 @@ import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/
export const computeObjectTargetTable = (
objectMetadata: ObjectMetadataInterface,
) => {
const prefix = objectMetadata.isCustom ? '_' : '';
return `${prefix}${objectMetadata.nameSingular}`;
return computeCustomName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
);
};
export const computeCustomName = (name: string, isCustom: boolean) => {
return isCustom ? `_${name}` : name;
};

View File

@@ -113,6 +113,9 @@ export class WorkspaceMigrationRunnerService {
tableMigration?.columns,
);
break;
case 'drop':
await queryRunner.dropTable(`${schemaName}.${tableMigration.name}`);
break;
default:
throw new Error(
`Migration table action ${tableMigration.action} not supported`,