Revert optimistic rendering on negative response (#7541)

Fixes #7299

The changes primarily focus on ensuring that records are correctly
handled in the cache and optimistic effects are reverted appropriately
when mutations fail.
This commit is contained in:
Félix Malfait
2024-10-09 16:18:55 +02:00
committed by GitHub
parent f901512a4f
commit be49d4fe5d
8 changed files with 351 additions and 166 deletions

View File

@ -2,9 +2,11 @@ import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
@ -67,7 +69,7 @@ export const useCreateManyRecords = <
}, },
); );
const recordsCreatedInCache = []; const recordsCreatedInCache: ObjectRecord[] = [];
for (const recordToCreate of sanitizedCreateManyRecordsInput) { for (const recordToCreate of sanitizedCreateManyRecordsInput) {
if (recordToCreate.id === null) { if (recordToCreate.id === null) {
@ -98,7 +100,8 @@ export const useCreateManyRecords = <
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
const createdObjects = await apolloClient.mutate({ const createdObjects = await apolloClient
.mutate({
mutation: createManyRecordsMutation, mutation: createManyRecordsMutation,
variables: { variables: {
data: sanitizedCreateManyRecordsInput, data: sanitizedCreateManyRecordsInput,
@ -117,6 +120,25 @@ export const useCreateManyRecords = <
shouldMatchRootQueryFilter, shouldMatchRootQueryFilter,
}); });
}, },
})
.catch((error: Error) => {
recordsCreatedInCache.forEach((recordToDelete) => {
deleteRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
recordToDelete,
});
});
triggerDeleteRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToDelete: recordsCreatedInCache,
objectMetadataItems,
});
throw error;
}); });
return createdObjects.data?.[mutationResponseField] ?? []; return createdObjects.data?.[mutationResponseField] ?? [];

View File

@ -3,9 +3,11 @@ import { useState } from 'react';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
@ -85,7 +87,8 @@ export const useCreateOneRecord = <
const mutationResponseField = const mutationResponseField =
getCreateOneRecordMutationResponseField(objectNameSingular); getCreateOneRecordMutationResponseField(objectNameSingular);
const createdObject = await apolloClient.mutate({ const createdObject = await apolloClient
.mutate({
mutation: createOneRecordMutation, mutation: createOneRecordMutation,
variables: { variables: {
input: sanitizedInput, input: sanitizedInput,
@ -105,6 +108,27 @@ export const useCreateOneRecord = <
setLoading(false); setLoading(false);
}, },
})
.catch((error: Error) => {
if (!recordCreatedInCache) {
throw error;
}
deleteRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
recordToDelete: recordCreatedInCache,
});
triggerDeleteRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToDelete: [recordCreatedInCache],
objectMetadataItems,
});
throw error;
}); });
return createdObject.data?.[mutationResponseField] ?? null; return createdObject.data?.[mutationResponseField] ?? null;

View File

@ -1,10 +1,12 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { apiConfigState } from '@/client-config/states/apiConfigState'; import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
@ -65,7 +67,8 @@ export const useDeleteManyRecords = ({
(batchIndex + 1) * mutationPageSize, (batchIndex + 1) * mutationPageSize,
); );
const deletedRecordsResponse = await apolloClient.mutate({ const deletedRecordsResponse = await apolloClient
.mutate({
mutation: deleteManyRecordsMutation, mutation: deleteManyRecordsMutation,
variables: { variables: {
filter: { id: { in: batchIds } }, filter: { id: { in: batchIds } },
@ -96,6 +99,41 @@ export const useDeleteManyRecords = ({
objectMetadataItems, objectMetadataItems,
}); });
}, },
})
.catch((error: Error) => {
const cachedRecords = batchIds.map((idToDelete) =>
getRecordFromCache(idToDelete, apolloClient.cache),
);
cachedRecords.forEach((cachedRecord) => {
if (!cachedRecord) {
return;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: {
...cachedRecord,
deletedAt: null,
},
});
});
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
objectMetadataItems,
recordsToCreate: cachedRecords
.filter(isDefined)
.map((cachedRecord) => ({
...cachedRecord,
deletedAt: null,
})),
});
throw error;
}); });
const deletedRecordsForThisBatch = const deletedRecordsForThisBatch =

View File

@ -1,10 +1,12 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -39,7 +41,8 @@ export const useDeleteOneRecord = ({
async (idToDelete: string) => { async (idToDelete: string) => {
const currentTimestamp = new Date().toISOString(); const currentTimestamp = new Date().toISOString();
const deletedRecord = await apolloClient.mutate({ const deletedRecord = await apolloClient
.mutate({
mutation: deleteOneRecordMutation, mutation: deleteOneRecordMutation,
variables: { variables: {
idToDelete: idToDelete, idToDelete: idToDelete,
@ -67,6 +70,40 @@ export const useDeleteOneRecord = ({
objectMetadataItems, objectMetadataItems,
}); });
}, },
})
.catch((error: Error) => {
const cachedRecord = getRecordFromCache(
idToDelete,
apolloClient.cache,
);
if (!cachedRecord) {
throw error;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: {
...cachedRecord,
deletedAt: null,
},
});
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
objectMetadataItems,
recordsToCreate: [
{
...cachedRecord,
deletedAt: null,
},
],
});
throw error;
}); });
return deletedRecord.data?.[mutationResponseField] ?? null; return deletedRecord.data?.[mutationResponseField] ?? null;

View File

@ -1,5 +1,6 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { apiConfigState } from '@/client-config/states/apiConfigState'; import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
@ -65,7 +66,12 @@ export const useDestroyManyRecords = ({
(batchIndex + 1) * mutationPageSize, (batchIndex + 1) * mutationPageSize,
); );
const destroyedRecordsResponse = await apolloClient.mutate({ const originalRecords = idsToDestroy
.map((recordId) => getRecordFromCache(recordId, apolloClient.cache))
.filter(isDefined);
const destroyedRecordsResponse = await apolloClient
.mutate({
mutation: destroyManyRecordsMutation, mutation: destroyManyRecordsMutation,
variables: { variables: {
filter: { id: { in: batchIds } }, filter: { id: { in: batchIds } },
@ -96,6 +102,17 @@ export const useDestroyManyRecords = ({
objectMetadataItems, objectMetadataItems,
}); });
}, },
})
.catch((error: Error) => {
if (originalRecords.length > 0) {
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToCreate: originalRecords,
objectMetadataItems,
});
}
throw error;
}); });
const destroyedRecordsForThisBatch = const destroyedRecordsForThisBatch =

View File

@ -1,12 +1,15 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
type useDestroyOneRecordProps = { type useDestroyOneRecordProps = {
@ -38,7 +41,13 @@ export const useDestroyOneRecord = ({
const destroyOneRecord = useCallback( const destroyOneRecord = useCallback(
async (idToDestroy: string) => { async (idToDestroy: string) => {
const deletedRecord = await apolloClient.mutate({ const originalRecord: ObjectRecord | null = getRecordFromCache(
idToDestroy,
apolloClient.cache,
);
const deletedRecord = await apolloClient
.mutate({
mutation: destroyOneRecordMutation, mutation: destroyOneRecordMutation,
variables: { idToDestroy }, variables: { idToDestroy },
optimisticResponse: { optimisticResponse: {
@ -63,6 +72,17 @@ export const useDestroyOneRecord = ({
objectMetadataItems, objectMetadataItems,
}); });
}, },
})
.catch((error: Error) => {
if (!isUndefinedOrNull(originalRecord)) {
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToCreate: [originalRecord],
objectMetadataItems,
});
}
throw error;
}); });
return deletedRecord.data?.[mutationResponseField] ?? null; return deletedRecord.data?.[mutationResponseField] ?? null;

View File

@ -62,7 +62,8 @@ export const useRestoreManyRecords = ({
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
)}`; )}`;
const restoredRecordsResponse = await apolloClient.mutate({ const restoredRecordsResponse = await apolloClient
.mutate({
mutation: restoreManyRecordsMutation, mutation: restoreManyRecordsMutation,
refetchQueries: [findOneQueryName, findManyQueryName], refetchQueries: [findOneQueryName, findManyQueryName],
variables: { variables: {
@ -77,6 +78,10 @@ export const useRestoreManyRecords = ({
deletedAt: null, deletedAt: null,
})), })),
}, },
})
.catch((error: Error) => {
// TODO: revert optimistic effect (once optimistic effect is fixed)
throw error;
}); });
const restoredRecordsForThisBatch = const restoredRecordsForThisBatch =

View File

@ -108,7 +108,8 @@ export const useUpdateOneRecord = <
const mutationResponseField = const mutationResponseField =
getUpdateOneRecordMutationResponseField(objectNameSingular); getUpdateOneRecordMutationResponseField(objectNameSingular);
const updatedRecord = await apolloClient.mutate({ const updatedRecord = await apolloClient
.mutate({
mutation: updateOneRecordMutation, mutation: updateOneRecordMutation,
variables: { variables: {
idToUpdate, idToUpdate,
@ -127,6 +128,27 @@ export const useUpdateOneRecord = <
objectMetadataItems, objectMetadataItems,
}); });
}, },
})
.catch((error: Error) => {
if (!cachedRecord) {
throw error;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: cachedRecord,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems,
});
throw error;
}); });
return updatedRecord?.data?.[mutationResponseField] ?? null; return updatedRecord?.data?.[mutationResponseField] ?? null;