feat: soft delete (#6576)
Implement soft delete on standards and custom objects. This is a temporary solution, when we drop `pg_graphql` we should rely on the `softDelete` functions of TypeORM. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -13,7 +13,7 @@ import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDeleteOneRecordProps = {
|
||||
type useDeleteManyRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
@ -25,7 +25,7 @@ type DeleteManyRecordsOptions = {
|
||||
|
||||
export const useDeleteManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
}: useDeleteManyRecordProps) => {
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useDestroyManyRecordsMutation = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return { destroyManyRecordsMutation: EMPTY_MUTATION };
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||
|
||||
const mutationResponseField = getDestroyManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const destroyManyRecordsMutation = gql`
|
||||
mutation DestroyMany${capitalizedObjectName}($filter: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput!) {
|
||||
${mutationResponseField}(filter: $filter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
destroyManyRecordsMutation,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,115 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
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 { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDestroyManyRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
type DestroyManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
delayInMsBetweenRequests?: number;
|
||||
};
|
||||
|
||||
export const useDestroyManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDestroyManyRecordProps) => {
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
apiConfig?.mutationMaximumAffectedRecords ?? DEFAULT_MUTATION_BATCH_SIZE;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { destroyManyRecordsMutation } = useDestroyManyRecordsMutation({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const mutationResponseField = getDestroyManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const destroyManyRecords = async (
|
||||
idsToDestroy: string[],
|
||||
options?: DestroyManyRecordsOptions,
|
||||
) => {
|
||||
const numberOfBatches = Math.ceil(idsToDestroy.length / mutationPageSize);
|
||||
|
||||
const destroyedRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToDestroy.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
const destroyedRecordsResponse = await apolloClient.mutate({
|
||||
mutation: destroyManyRecordsMutation,
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: batchIds.map((idToDestroy) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDestroy,
|
||||
})),
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
|
||||
const cachedRecords = records
|
||||
.map((record) => getRecordFromCache(record.id, cache))
|
||||
.filter(isDefined);
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: cachedRecords,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const destroyedRecordsForThisBatch =
|
||||
destroyedRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||
|
||||
destroyedRecords.push(...destroyedRecordsForThisBatch);
|
||||
|
||||
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||
await sleep(options.delayInMsBetweenRequests);
|
||||
}
|
||||
}
|
||||
|
||||
return destroyedRecords;
|
||||
};
|
||||
|
||||
return { destroyManyRecords };
|
||||
};
|
||||
@ -17,11 +17,13 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
recordGqlFields,
|
||||
onCompleted,
|
||||
skip,
|
||||
withSoftDeleted = false,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
objectRecordId: string | undefined;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
onCompleted?: (data: T) => void;
|
||||
skip?: boolean;
|
||||
withSoftDeleted?: boolean;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -33,6 +35,7 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
const { findOneRecordQuery } = useFindOneRecordQuery({
|
||||
objectNameSingular,
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
withSoftDeleted,
|
||||
});
|
||||
|
||||
const { data, loading, error } = useQuery<{
|
||||
|
||||
@ -10,9 +10,11 @@ import { capitalize } from '~/utils/string/capitalize';
|
||||
export const useFindOneRecordQuery = ({
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
withSoftDeleted = false,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
withSoftDeleted?: boolean;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -25,6 +27,16 @@ export const useFindOneRecordQuery = ({
|
||||
objectMetadataItem.nameSingular,
|
||||
)}($objectRecordId: ID!) {
|
||||
${objectMetadataItem.nameSingular}(filter: {
|
||||
${
|
||||
withSoftDeleted
|
||||
? `
|
||||
or: [
|
||||
{ deletedAt: { is: NULL } },
|
||||
{ deletedAt: { is: NOT_NULL } }
|
||||
],
|
||||
`
|
||||
: ''
|
||||
}
|
||||
id: {
|
||||
eq: $objectRecordId
|
||||
}
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useRestoreManyRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
type RestoreManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
delayInMsBetweenRequests?: number;
|
||||
};
|
||||
|
||||
export const useRestoreManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useRestoreManyRecordProps) => {
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
apiConfig?.mutationMaximumAffectedRecords ?? DEFAULT_MUTATION_BATCH_SIZE;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { restoreManyRecordsMutation } = useRestoreManyRecordsMutation({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const restoreManyRecords = async (
|
||||
idsToRestore: string[],
|
||||
options?: RestoreManyRecordsOptions,
|
||||
) => {
|
||||
const numberOfBatches = Math.ceil(idsToRestore.length / mutationPageSize);
|
||||
|
||||
const restoredRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToRestore.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
// TODO: fix optimistic effect
|
||||
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
||||
const findManyQueryName = `FindMany${capitalize(objectMetadataItem.namePlural)}`;
|
||||
|
||||
const restoredRecordsResponse = await apolloClient.mutate({
|
||||
mutation: restoreManyRecordsMutation,
|
||||
refetchQueries: [findOneQueryName, findManyQueryName],
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: batchIds.map((idToRestore) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToRestore,
|
||||
deletedAt: null,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const restoredRecordsForThisBatch =
|
||||
restoredRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||
|
||||
restoredRecords.push(...restoredRecordsForThisBatch);
|
||||
|
||||
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||
await sleep(options.delayInMsBetweenRequests);
|
||||
}
|
||||
}
|
||||
|
||||
return restoredRecords;
|
||||
};
|
||||
|
||||
return { restoreManyRecords };
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useRestoreManyRecordsMutation = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return { restoreManyRecordsMutation: EMPTY_MUTATION };
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||
|
||||
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const restoreManyRecordsMutation = gql`
|
||||
mutation RestoreMany${capitalizedObjectName}($filter: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput!) {
|
||||
${mutationResponseField}(filter: $filter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
restoreManyRecordsMutation,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user