diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 0bc809390..ab20e1b28 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -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; - dataSourceId?: Maybe; - description?: Maybe; - icon?: Maybe; - id?: Maybe; - imageIdentifierFieldMetadataId?: Maybe; - isActive?: Maybe; - isCustom?: Maybe; - isSystem?: Maybe; - labelIdentifierFieldMetadataId?: Maybe; - labelPlural?: Maybe; - labelSingular?: Maybe; - namePlural?: Maybe; - nameSingular?: Maybe; - updatedAt?: Maybe; -}; - 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; - workspaceMemberId?: Maybe; + personId?: Maybe; + workspaceMemberId?: Maybe; }; 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']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index de5efda19..785e6d88c 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -97,6 +97,11 @@ export type CursorPaging = { last?: InputMaybe; }; +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; - dataSourceId?: Maybe; - description?: Maybe; - icon?: Maybe; - id?: Maybe; - imageIdentifierFieldMetadataId?: Maybe; - isActive?: Maybe; - isCustom?: Maybe; - isSystem?: Maybe; - labelIdentifierFieldMetadataId?: Maybe; - labelPlural?: Maybe; - labelSingular?: Maybe; - namePlural?: Maybe; - nameSingular?: Maybe; - updatedAt?: Maybe; -}; - export type ObjectFieldsConnection = { __typename?: 'ObjectFieldsConnection'; /** Array of edges. */ diff --git a/packages/twenty-server/src/metadata/object-metadata/dtos/delete-object.input.ts b/packages/twenty-server/src/metadata/object-metadata/dtos/delete-object.input.ts new file mode 100644 index 000000000..9fea1518d --- /dev/null +++ b/packages/twenty-server/src/metadata/object-metadata/dtos/delete-object.input.ts @@ -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; +} diff --git a/packages/twenty-server/src/metadata/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/metadata/object-metadata/object-metadata.module.ts index e43b20be6..abf401338 100644 --- a/packages/twenty-server/src/metadata/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/metadata/object-metadata/object-metadata.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/metadata/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/metadata/object-metadata/object-metadata.resolver.ts new file mode 100644 index 000000000..54f72ce3a --- /dev/null +++ b/packages/twenty-server/src/metadata/object-metadata/object-metadata.resolver.ts @@ -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); + } +} diff --git a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts index 44e69c6dc..0fe97a590 100644 --- a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts @@ -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 { + 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 { diff --git a/packages/twenty-server/src/metadata/relation-metadata/types/relation-to-delete.ts b/packages/twenty-server/src/metadata/relation-metadata/types/relation-to-delete.ts new file mode 100644 index 000000000..1ce72f60b --- /dev/null +++ b/packages/twenty-server/src/metadata/relation-metadata/types/relation-to-delete.ts @@ -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; +}; \ No newline at end of file diff --git a/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.entity.ts index 42ccb3130..5f68939c8 100644 --- a/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.entity.ts @@ -58,7 +58,7 @@ export type WorkspaceMigrationColumnAction = { export type WorkspaceMigrationTableAction = { name: string; - action: 'create' | 'alter'; + action: 'create' | 'alter' | 'drop'; columns?: WorkspaceMigrationColumnAction[]; }; diff --git a/packages/twenty-server/src/workspace/utils/compute-object-target-table.util.ts b/packages/twenty-server/src/workspace/utils/compute-object-target-table.util.ts index dc547f32c..fd241f152 100644 --- a/packages/twenty-server/src/workspace/utils/compute-object-target-table.util.ts +++ b/packages/twenty-server/src/workspace/utils/compute-object-target-table.util.ts @@ -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; }; diff --git a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts index 462581251..9fe8fcf6d 100644 --- a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts @@ -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`,