7092 destroy connected account instead of soft deleting it (#7099)

- Create `destroyOne` endpoint
- Call `destroyOne` when removing a `connectedAccount`
This commit is contained in:
Raphaël Bosi
2024-09-17 18:30:40 +02:00
committed by GitHub
parent c42ea57b97
commit 7cdf2dc4ec
15 changed files with 241 additions and 5 deletions

View File

@@ -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';

View File

@@ -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,
};
};

View File

@@ -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,
};
};

View File

@@ -0,0 +1,5 @@
import { capitalize } from '~/utils/string/capitalize';
export const getDestroyOneRecordMutationResponseField = (
objectNameSingular: string,
) => `destroy${capitalize(objectNameSingular)}`;

View File

@@ -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();
}}
/>

View File

@@ -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<ObjectRecord extends IRecord = IRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const graphqlQueryDestroyOneResolverService =
new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager);
return graphqlQueryDestroyOneResolverService.destroyOne(args, options);
}
}

View File

@@ -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<ObjectRecord extends IRecord = IRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
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;
}
}

View File

@@ -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<DestroyOneResolverArgs> {
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);
}
};
}
}

View File

@@ -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,
],

View File

@@ -88,6 +88,10 @@ export interface RestoreManyResolverArgs<Filter = any> {
filter: Filter;
}
export interface DestroyOneResolverArgs {
id: string;
}
export interface DestroyManyResolverArgs<Filter = any> {
filter: Filter;
}

View File

@@ -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],
})

View File

@@ -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],

View File

@@ -105,6 +105,13 @@ export const getResolverArgs = (
isNullable: false,
},
};
case 'destroyOne':
return {
id: {
type: GraphQLID,
isNullable: false,
},
};
case 'updateMany':
return {
data: {

View File

@@ -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':