diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts index 6524374c5..08f4d092a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts @@ -6,7 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; -import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordMutation'; +import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation'; import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecordsMutation.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecordMutation.ts rename to packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecordsMutation.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts new file mode 100644 index 000000000..fc5d75d0a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts @@ -0,0 +1,84 @@ +import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; + +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; +import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; +import { capitalize } from '~/utils/string/capitalize'; + +type useDestroyOneRecordProps = { + objectNameSingular: string; + refetchFindManyQuery?: boolean; +}; + +export const useDestroyOneRecord = ({ + objectNameSingular, +}: useDestroyOneRecordProps) => { + const apolloClient = useApolloClient(); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const getRecordFromCache = useGetRecordFromCache({ + objectNameSingular, + }); + + const { destroyOneRecordMutation } = useDestroyOneRecordMutation({ + objectNameSingular, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const mutationResponseField = + getDestroyOneRecordMutationResponseField(objectNameSingular); + + const destroyOneRecord = useCallback( + async (idToDestroy: string) => { + const deletedRecord = await apolloClient.mutate({ + mutation: destroyOneRecordMutation, + variables: { idToDestroy }, + optimisticResponse: { + [mutationResponseField]: { + __typename: capitalize(objectNameSingular), + id: idToDestroy, + }, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + + if (!record) return; + + const cachedRecord = getRecordFromCache(record.id, cache); + + if (!cachedRecord) return; + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: [cachedRecord], + objectMetadataItems, + }); + }, + }); + + return deletedRecord.data?.[mutationResponseField] ?? null; + }, + [ + apolloClient, + destroyOneRecordMutation, + getRecordFromCache, + mutationResponseField, + objectMetadataItem, + objectNameSingular, + objectMetadataItems, + ], + ); + + return { + destroyOneRecord, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts new file mode 100644 index 000000000..c6a6fb680 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts @@ -0,0 +1,39 @@ +import gql from 'graphql-tag'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; +import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useDestroyOneRecordMutation = ({ + objectNameSingular, +}: { + objectNameSingular: string; +}) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + if (isUndefinedOrNull(objectMetadataItem)) { + return { destroyOneRecordMutation: EMPTY_MUTATION }; + } + + const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); + + const mutationResponseField = getDestroyOneRecordMutationResponseField( + objectMetadataItem.nameSingular, + ); + + const destroyOneRecordMutation = gql` + mutation DestroyOne${capitalizedObjectName}($idToDestroy: ID!) { + ${mutationResponseField}(id: $idToDestroy) { + id + } + } + `; + + return { + destroyOneRecordMutation, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/utils/getDestroyOneRecordMutationResponseField.ts b/packages/twenty-front/src/modules/object-record/utils/getDestroyOneRecordMutationResponseField.ts new file mode 100644 index 000000000..f93c64915 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getDestroyOneRecordMutationResponseField.ts @@ -0,0 +1,5 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getDestroyOneRecordMutationResponseField = ( + objectNameSingular: string, +) => `destroy${capitalize(objectNameSingular)}`; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx index 515dce50a..beba37f8b 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx @@ -9,7 +9,7 @@ import { import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -32,7 +32,7 @@ export const SettingsAccountsRowDropdownMenu = ({ const navigate = useNavigate(); const { closeDropdown } = useDropdown(dropdownId); - const { deleteOneRecord } = useDeleteOneRecord({ + const { destroyOneRecord } = useDestroyOneRecord({ objectNameSingular: CoreObjectNameSingular.ConnectedAccount, }); @@ -81,7 +81,7 @@ export const SettingsAccountsRowDropdownMenu = ({ LeftIcon={IconTrash} text="Remove account" onClick={() => { - deleteOneRecord(account.id); + destroyOneRecord(account.id); closeDropdown(); }} /> diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index c709a711e..40ea3b4b4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -9,11 +9,13 @@ import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/inter import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { CreateManyResolverArgs, + DestroyOneResolverArgs, FindManyResolverArgs, FindOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; +import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; @@ -64,4 +66,15 @@ export class GraphqlQueryRunnerService { return graphqlQueryCreateManyResolverService.createMany(args, options); } + + @LogExecutionTime() + async destroyOne( + args: DestroyOneResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise { + const graphqlQueryDestroyOneResolverService = + new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager); + + return graphqlQueryDestroyOneResolverService.destroyOne(args, options); + } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts new file mode 100644 index 000000000..53dad3edd --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -0,0 +1,33 @@ +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +export class GraphqlQueryDestroyOneResolverService { + private twentyORMGlobalManager: TwentyORMGlobalManager; + + constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { + this.twentyORMGlobalManager = twentyORMGlobalManager; + } + + async destroyOne( + args: DestroyOneResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise { + const { authContext, objectMetadataItem } = options; + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + authContext.workspace.id, + objectMetadataItem.nameSingular, + ); + + const record = await repository.findOne({ + where: { id: args.id }, + }); + + await repository.delete(args.id); + + return record as ObjectRecord; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts new file mode 100644 index 000000000..4c204d6e8 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { + DestroyOneResolverArgs, + Resolver, +} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; + +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; + +@Injectable() +export class DestroyOneResolverFactory + implements WorkspaceResolverBuilderFactoryInterface +{ + public static methodName = 'destroyOne' as const; + + constructor( + private readonly graphQLQueryRunnerService: GraphqlQueryRunnerService, + ) {} + + create( + context: WorkspaceSchemaBuilderContext, + ): Resolver { + const internalContext = context; + + return async (_source, args, context, info) => { + try { + return await this.graphQLQueryRunnerService.destroyOne(args, { + authContext: internalContext.authContext, + objectMetadataItem: internalContext.objectMetadataItem, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + }; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts index 99d9c5419..1724242e8 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts @@ -1,4 +1,5 @@ import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory'; +import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory'; import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory'; import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; @@ -21,6 +22,7 @@ export const workspaceResolverBuilderFactories = [ DeleteOneResolverFactory, UpdateManyResolverFactory, DeleteManyResolverFactory, + DestroyOneResolverFactory, DestroyManyResolverFactory, RestoreManyResolverFactory, ]; @@ -38,6 +40,7 @@ export const workspaceResolverBuilderMethodNames = { DeleteOneResolverFactory.methodName, UpdateManyResolverFactory.methodName, DeleteManyResolverFactory.methodName, + DestroyOneResolverFactory.methodName, DestroyManyResolverFactory.methodName, RestoreManyResolverFactory.methodName, ], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts index cbe5b1fa2..4e2a0af85 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts @@ -88,6 +88,10 @@ export interface RestoreManyResolverArgs { filter: Filter; } +export interface DestroyOneResolverArgs { + id: string; +} + export interface DestroyManyResolverArgs { filter: Filter; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts index 7b87e49a8..93cf9e920 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module'; import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module'; import { WorkspaceResolverFactory } from './workspace-resolver.factory'; @@ -7,7 +8,7 @@ import { WorkspaceResolverFactory } from './workspace-resolver.factory'; import { workspaceResolverBuilderFactories } from './factories/factories'; @Module({ - imports: [WorkspaceQueryRunnerModule], + imports: [WorkspaceQueryRunnerModule, GraphqlQueryRunnerModule], providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory], exports: [WorkspaceResolverFactory], }) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index fc405bfff..6c06b85d9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -6,6 +6,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory'; import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory'; +import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory'; import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory'; import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -36,6 +37,7 @@ export class WorkspaceResolverFactory { private readonly createOneResolverFactory: CreateOneResolverFactory, private readonly updateOneResolverFactory: UpdateOneResolverFactory, private readonly deleteOneResolverFactory: DeleteOneResolverFactory, + private readonly destroyOneResolverFactory: DestroyOneResolverFactory, private readonly updateManyResolverFactory: UpdateManyResolverFactory, private readonly deleteManyResolverFactory: DeleteManyResolverFactory, private readonly restoreManyResolverFactory: RestoreManyResolverFactory, @@ -58,6 +60,7 @@ export class WorkspaceResolverFactory { ['createOne', this.createOneResolverFactory], ['updateOne', this.updateOneResolverFactory], ['deleteOne', this.deleteOneResolverFactory], + ['destroyOne', this.destroyOneResolverFactory], ['updateMany', this.updateManyResolverFactory], ['deleteMany', this.deleteManyResolverFactory], ['restoreMany', this.restoreManyResolverFactory], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts index f42377b89..c829e0e40 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts @@ -105,6 +105,13 @@ export const getResolverArgs = ( isNullable: false, }, }; + case 'destroyOne': + return { + id: { + type: GraphQLID, + isNullable: false, + }, + }; case 'updateMany': return { data: { diff --git a/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts b/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts index f5462b87f..77a30372a 100644 --- a/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts +++ b/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts @@ -23,6 +23,8 @@ export const getResolverName = ( return `update${pascalCase(objectMetadata.nameSingular)}`; case 'deleteOne': return `delete${pascalCase(objectMetadata.nameSingular)}`; + case 'destroyOne': + return `destroy${pascalCase(objectMetadata.nameSingular)}`; case 'updateMany': return `update${pascalCase(objectMetadata.namePlural)}`; case 'restoreMany':