[BUG] Fix record relation optimistic mutation (#9881)
# Introduction It seems like optimistic caching isn't working as expected for any record relation mutation, CREATE UPDATE DELETE. It should not have an impact on the destroy We included a new `computeOptimisticRecordInput` that will calculate if a relation is added or detach. Updated the `triggerCreateRecordsOptimisticEffect` signature we should have a look to each of its call to determine if it should be checking cache or not Related to #9580 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -57,7 +57,6 @@ const connectedObjects = {
|
||||
export const variables = {
|
||||
idToUpdate: '36abbb63-34ed-4a16-89f5-f549ac55d0f9',
|
||||
input: {
|
||||
...basePerson,
|
||||
name: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ import { expect } from '@storybook/test';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
|
||||
const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' };
|
||||
const update = {
|
||||
const updateInput = {
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
@ -20,7 +20,7 @@ const update = {
|
||||
const updatePerson = {
|
||||
...person,
|
||||
...responseData,
|
||||
...update,
|
||||
...updateInput,
|
||||
};
|
||||
|
||||
const mocks = [
|
||||
@ -64,11 +64,11 @@ describe('useUpdateOneRecord', () => {
|
||||
await act(async () => {
|
||||
const res = await result.current.updateOneRecord({
|
||||
idToUpdate,
|
||||
updateOneRecordInput: updatePerson,
|
||||
updateOneRecordInput: updateInput,
|
||||
});
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toHaveProperty('id', person.id);
|
||||
expect(res).toHaveProperty('name', update.name);
|
||||
expect(res).toHaveProperty('name', updateInput.name);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
|
||||
@ -8,15 +8,21 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
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 { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type PartialObjectRecordWithId = Partial<ObjectRecord> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type useCreateManyRecordsProps = {
|
||||
objectNameSingular: string;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
@ -60,42 +66,56 @@ export const useCreateManyRecords = <
|
||||
recordsToCreate: Partial<CreatedObjectRecord>[],
|
||||
upsert?: boolean,
|
||||
) => {
|
||||
const sanitizedCreateManyRecordsInput = recordsToCreate.map(
|
||||
(recordToCreate) => {
|
||||
const idForCreation = recordToCreate?.id ?? (upsert ? undefined : v4());
|
||||
const sanitizedCreateManyRecordsInput: PartialObjectRecordWithId[] = [];
|
||||
const recordOptimisticRecordsInput: PartialObjectRecordWithId[] = [];
|
||||
recordsToCreate.forEach((recordToCreate) => {
|
||||
const idForCreation = recordToCreate?.id ?? v4();
|
||||
const sanitizedRecord = {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: recordToCreate,
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
const optimisticRecordInput = {
|
||||
...computeOptimisticRecordFromInput({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordInput: recordToCreate,
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
|
||||
return {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: recordToCreate,
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
},
|
||||
);
|
||||
sanitizedCreateManyRecordsInput.push(sanitizedRecord);
|
||||
recordOptimisticRecordsInput.push(optimisticRecordInput);
|
||||
});
|
||||
|
||||
const recordsCreatedInCache: ObjectRecord[] = [];
|
||||
|
||||
for (const recordToCreate of sanitizedCreateManyRecordsInput) {
|
||||
if (recordToCreate.id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordCreatedInCache = createOneRecordInCache({
|
||||
...(recordToCreate as { id: string }),
|
||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||
});
|
||||
|
||||
if (isDefined(recordCreatedInCache)) {
|
||||
recordsCreatedInCache.push(recordCreatedInCache);
|
||||
}
|
||||
}
|
||||
const recordsCreatedInCache = recordOptimisticRecordsInput
|
||||
.map((recordToCreate) =>
|
||||
createOneRecordInCache({
|
||||
...recordToCreate,
|
||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (recordsCreatedInCache.length > 0) {
|
||||
const recordNodeCreatedInCache = recordsCreatedInCache
|
||||
.map((record) =>
|
||||
getRecordNodeFromRecord({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
record: record,
|
||||
computeReferences: false,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: recordsCreatedInCache,
|
||||
recordsToCreate: recordNodeCreatedInCache,
|
||||
objectMetadataItems,
|
||||
shouldMatchRootQueryFilter,
|
||||
});
|
||||
@ -123,6 +143,7 @@ export const useCreateManyRecords = <
|
||||
recordsToCreate: records,
|
||||
objectMetadataItems,
|
||||
shouldMatchRootQueryFilter,
|
||||
checkForRecordInCache: true,
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
@ -9,11 +9,13 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
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 { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -60,33 +62,48 @@ export const useCreateOneRecord = <
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||
const createOneRecord = async (recordInput: Partial<CreatedObjectRecord>) => {
|
||||
setLoading(true);
|
||||
|
||||
const idForCreation = input.id ?? v4();
|
||||
const idForCreation = recordInput.id ?? v4();
|
||||
|
||||
const sanitizedInput = {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: input,
|
||||
recordInput,
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
|
||||
const optimisticRecordInput = computeOptimisticRecordFromInput({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordInput: { ...recordInput, id: idForCreation },
|
||||
});
|
||||
const recordCreatedInCache = createOneRecordInCache({
|
||||
...input,
|
||||
...optimisticRecordInput,
|
||||
id: idForCreation,
|
||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||
});
|
||||
|
||||
if (isDefined(recordCreatedInCache)) {
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
const optimisticRecordNode = getRecordNodeFromRecord({
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [recordCreatedInCache],
|
||||
objectMetadataItems,
|
||||
shouldMatchRootQueryFilter,
|
||||
record: recordCreatedInCache,
|
||||
computeReferences: false,
|
||||
});
|
||||
|
||||
if (optimisticRecordNode !== null) {
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [optimisticRecordNode],
|
||||
objectMetadataItems,
|
||||
shouldMatchRootQueryFilter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mutationResponseField =
|
||||
@ -100,16 +117,16 @@ export const useCreateOneRecord = <
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!record || skipPostOptmisticEffect) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
shouldMatchRootQueryFilter,
|
||||
});
|
||||
if (skipPostOptmisticEffect === false && isDefined(record)) {
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
shouldMatchRootQueryFilter,
|
||||
checkForRecordInCache: true,
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
|
||||
@ -10,6 +10,7 @@ import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/g
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
@ -59,12 +60,12 @@ export const useUpdateOneRecord = <
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
optimisticRecord?: Partial<ObjectRecord>;
|
||||
}) => {
|
||||
const sanitizedInput = {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: updateOneRecordInput,
|
||||
}),
|
||||
};
|
||||
const optimisticRecordInput = computeOptimisticRecordFromInput({
|
||||
objectMetadataItem,
|
||||
recordInput: updateOneRecordInput,
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const cachedRecord = getRecordFromCache<ObjectRecord>(idToUpdate);
|
||||
|
||||
@ -73,12 +74,12 @@ export const useUpdateOneRecord = <
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
computeReferences: true,
|
||||
computeReferences: false,
|
||||
});
|
||||
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...(optimisticRecord ?? sanitizedInput),
|
||||
...(optimisticRecord ?? optimisticRecordInput),
|
||||
...{ id: idToUpdate },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
@ -89,9 +90,8 @@ export const useUpdateOneRecord = <
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
computeReferences: true,
|
||||
computeReferences: false,
|
||||
});
|
||||
|
||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
||||
return null;
|
||||
}
|
||||
@ -114,6 +114,12 @@ export const useUpdateOneRecord = <
|
||||
const mutationResponseField =
|
||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
const sanitizedInput = {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: updateOneRecordInput,
|
||||
}),
|
||||
};
|
||||
const updatedRecord = await apolloClient
|
||||
.mutate({
|
||||
mutation: updateOneRecordMutation,
|
||||
|
||||
Reference in New Issue
Block a user