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 { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
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 { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
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) {
if (recordToCreate.id === null) {
@ -98,26 +100,46 @@ export const useCreateManyRecords = <
objectMetadataItem.namePlural,
);
const createdObjects = await apolloClient.mutate({
mutation: createManyRecordsMutation,
variables: {
data: sanitizedCreateManyRecordsInput,
upsert: upsert,
},
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
const createdObjects = await apolloClient
.mutate({
mutation: createManyRecordsMutation,
variables: {
data: sanitizedCreateManyRecordsInput,
upsert: upsert,
},
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!records?.length || skipPostOptmisticEffect) return;
if (!records?.length || skipPostOptmisticEffect) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: records,
objectMetadataItems,
shouldMatchRootQueryFilter,
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: records,
objectMetadataItems,
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] ?? [];
};

View File

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

View File

@ -1,10 +1,12 @@
import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
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 { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
@ -65,38 +67,74 @@ export const useDeleteManyRecords = ({
(batchIndex + 1) * mutationPageSize,
);
const deletedRecordsResponse = await apolloClient.mutate({
mutation: deleteManyRecordsMutation,
variables: {
filter: { id: { in: batchIds } },
},
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: batchIds.map((idToDelete) => ({
__typename: capitalize(objectNameSingular),
id: idToDelete,
const deletedRecordsResponse = await apolloClient
.mutate({
mutation: deleteManyRecordsMutation,
variables: {
filter: { id: { in: batchIds } },
},
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: batchIds.map((idToDelete) => ({
__typename: capitalize(objectNameSingular),
id: idToDelete,
})),
},
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,
});
},
})
.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,
})),
},
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,
});
},
});
throw error;
});
const deletedRecordsForThisBatch =
deletedRecordsResponse.data?.[mutationResponseField] ?? [];

View File

@ -1,10 +1,12 @@
import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
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 { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { capitalize } from '~/utils/string/capitalize';
@ -39,35 +41,70 @@ export const useDeleteOneRecord = ({
async (idToDelete: string) => {
const currentTimestamp = new Date().toISOString();
const deletedRecord = await apolloClient.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete: idToDelete,
},
optimisticResponse: {
[mutationResponseField]: {
__typename: capitalize(objectNameSingular),
id: idToDelete,
deletedAt: currentTimestamp,
const deletedRecord = await apolloClient
.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete: idToDelete,
},
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
optimisticResponse: {
[mutationResponseField]: {
__typename: capitalize(objectNameSingular),
id: idToDelete,
deletedAt: currentTimestamp,
},
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!record) return;
if (!record) return;
const cachedRecord = getRecordFromCache(record.id, cache);
const cachedRecord = getRecordFromCache(record.id, cache);
if (!cachedRecord) return;
if (!cachedRecord) return;
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: [cachedRecord],
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: [cachedRecord],
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;
},

View File

@ -1,5 +1,6 @@
import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
@ -65,38 +66,54 @@ export const useDestroyManyRecords = ({
(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];
const originalRecords = idsToDestroy
.map((recordId) => getRecordFromCache(recordId, apolloClient.cache))
.filter(isDefined);
if (!records?.length) return;
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];
const cachedRecords = records
.map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined);
if (!records?.length) return;
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: cachedRecords,
objectMetadataItems,
});
},
});
const cachedRecords = records
.map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined);
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: cachedRecords,
objectMetadataItems,
});
},
})
.catch((error: Error) => {
if (originalRecords.length > 0) {
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToCreate: originalRecords,
objectMetadataItems,
});
}
throw error;
});
const destroyedRecordsForThisBatch =
destroyedRecordsResponse.data?.[mutationResponseField] ?? [];

View File

@ -1,12 +1,15 @@
import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from '~/utils/string/capitalize';
type useDestroyOneRecordProps = {
@ -38,32 +41,49 @@ export const useDestroyOneRecord = ({
const destroyOneRecord = useCallback(
async (idToDestroy: string) => {
const deletedRecord = await apolloClient.mutate({
mutation: destroyOneRecordMutation,
variables: { idToDestroy },
optimisticResponse: {
[mutationResponseField]: {
__typename: capitalize(objectNameSingular),
id: idToDestroy,
const originalRecord: ObjectRecord | null = getRecordFromCache(
idToDestroy,
apolloClient.cache,
);
const deletedRecord = await apolloClient
.mutate({
mutation: destroyOneRecordMutation,
variables: { idToDestroy },
optimisticResponse: {
[mutationResponseField]: {
__typename: capitalize(objectNameSingular),
id: idToDestroy,
},
},
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!record) return;
if (!record) return;
const cachedRecord = getRecordFromCache(record.id, cache);
const cachedRecord = getRecordFromCache(record.id, cache);
if (!cachedRecord) return;
if (!cachedRecord) return;
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: [cachedRecord],
objectMetadataItems,
});
},
});
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: [cachedRecord],
objectMetadataItems,
});
},
})
.catch((error: Error) => {
if (!isUndefinedOrNull(originalRecord)) {
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToCreate: [originalRecord],
objectMetadataItems,
});
}
throw error;
});
return deletedRecord.data?.[mutationResponseField] ?? null;
},

View File

@ -62,22 +62,27 @@ export const useRestoreManyRecords = ({
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 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,
})),
},
})
.catch((error: Error) => {
// TODO: revert optimistic effect (once optimistic effect is fixed)
throw error;
});
const restoredRecordsForThisBatch =
restoredRecordsResponse.data?.[mutationResponseField] ?? [];

View File

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