mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 05:37:56 +00:00
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:
@@ -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'];
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -58,7 +58,7 @@ export type WorkspaceMigrationColumnAction = {
|
||||
|
||||
export type WorkspaceMigrationTableAction = {
|
||||
name: string;
|
||||
action: 'create' | 'alter';
|
||||
action: 'create' | 'alter' | 'drop';
|
||||
columns?: WorkspaceMigrationColumnAction[];
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user