Fixed Apollo cache bug (#16523)
Fixes https://github.com/twentyhq/twenty/issues/16520 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+49
-15
@@ -2,13 +2,17 @@ import { type ApolloCache, type StoreObject } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
|
||||
export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
cache,
|
||||
@@ -32,16 +36,50 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
rootQueryCachedResponse,
|
||||
{ readField },
|
||||
{ readField, storeFieldName },
|
||||
) => {
|
||||
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
|
||||
const { fieldVariables: rootQueryVariables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
);
|
||||
|
||||
if (
|
||||
!isObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
)
|
||||
) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
const recordsMatchingRootQueryFilter = recordsToDestroy.filter(
|
||||
(record) =>
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: rootQueryVariables?.filter ?? {},
|
||||
objectMetadataItem,
|
||||
}),
|
||||
);
|
||||
|
||||
const newTotalCount = isDefined(totalCount)
|
||||
? Math.max(totalCount - recordsMatchingRootQueryFilter.length, 0)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
!isObjectRecordConnectionWithRefs(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
|
||||
return rootQueryCachedResponse;
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...rootQueryCachedResponse,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
}
|
||||
|
||||
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
|
||||
@@ -52,11 +90,6 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
);
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
);
|
||||
|
||||
const nextCachedEdges =
|
||||
cachedEdges?.filter((cachedEdge) => {
|
||||
const nodeId = readField<string>('id', cachedEdge.node);
|
||||
@@ -65,14 +98,15 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
}) || [];
|
||||
|
||||
if (nextCachedEdges.length === cachedEdges?.length)
|
||||
return rootQueryCachedObjectRecordConnection;
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
edges: nextCachedEdges,
|
||||
totalCount: isDefined(totalCount)
|
||||
? totalCount - recordIdsToDestroy.length
|
||||
: undefined,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
+21
@@ -82,6 +82,26 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const currentRecordIndexInRootQueryEdges = isRecordMatchingFilter({
|
||||
record: currentRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
rootQueryConnection,
|
||||
);
|
||||
|
||||
const newTotalCount = isDefined(totalCount)
|
||||
? Math.max(
|
||||
totalCount +
|
||||
(updatedRecordMatchesThisRootQueryFilter ? 1 : 0) +
|
||||
(currentRecordIndexInRootQueryEdges ? -1 : 0),
|
||||
0,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const updatedRecordIndexInRootQueryEdges =
|
||||
rootQueryCurrentEdges.findIndex(
|
||||
(cachedEdge) =>
|
||||
@@ -131,6 +151,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
return {
|
||||
...rootQueryConnection,
|
||||
edges: rootQueryNextEdges,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
+31
-15
@@ -7,15 +7,18 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ApolloCache } from '@apollo/client';
|
||||
import { isArray } from '@sniptt/guards';
|
||||
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationType,
|
||||
type ObjectPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
computeMorphRelationFieldName,
|
||||
CustomError,
|
||||
@@ -147,12 +150,12 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
@@ -179,6 +182,7 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
|
||||
const gqlFieldNameOnTargetRecord =
|
||||
targetFieldMetadataFullObject.type === FieldMetadataType.RELATION
|
||||
? targetFieldMetadataFullObject.name
|
||||
@@ -189,7 +193,10 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
sourceObjectMetadataItem.nameSingular,
|
||||
targetObjectMetadataNamePlural: sourceObjectMetadataItem.namePlural,
|
||||
});
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
if (
|
||||
shouldCascadeDeleteTargetRecords &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
@@ -198,7 +205,10 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
upsertRecordsInStore,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
} else if (
|
||||
isDefined(currentSourceRecord) &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
@@ -306,12 +316,12 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[gqlFieldMorphRelation];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[gqlFieldMorphRelation];
|
||||
|
||||
@@ -338,7 +348,10 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
if (
|
||||
shouldCascadeDeleteTargetRecords &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
@@ -347,7 +360,10 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
upsertRecordsInStore,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
} else if (
|
||||
isDefined(currentSourceRecord) &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
@@ -387,7 +403,7 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
};
|
||||
|
||||
const extractTargetRecordsFromRelation = (
|
||||
value: RecordGqlConnection | RecordGqlNode | null,
|
||||
value: RecordGqlConnectionEdgesRequired | RecordGqlNode | null,
|
||||
relation: FieldMetadataItemRelation,
|
||||
): RecordGqlNode[] => {
|
||||
// TODO investigate on the root cause of array injection here, should never occurs
|
||||
@@ -399,9 +415,9 @@ const extractTargetRecordsFromRelation = (
|
||||
if (!isDefined(relation)) {
|
||||
throw new Error('Relation found is undefined');
|
||||
}
|
||||
if (isObjectRecordConnection(relation, value)) {
|
||||
return value.edges.map(({ node }) => node);
|
||||
if (relation.type === RelationType.ONE_TO_MANY) {
|
||||
return value.edges.map(({ node }: { node: RecordGqlNode }) => node);
|
||||
}
|
||||
|
||||
return [value];
|
||||
return [value as RecordGqlNode];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user