Skip to content

Commit ee47a77

Browse files
Fixed Apollo cache bug (twentyhq#16523)
Fixes twentyhq#16520 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
1 parent f0af947 commit ee47a77

22 files changed

+826
-104
lines changed

packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { type ApolloCache, type StoreObject } from '@apollo/client';
22

33
import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect';
44
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
5+
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
56
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
67
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
8+
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
79
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
810
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
11+
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
912
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
1013
import { type ObjectPermissions } from 'twenty-shared/types';
1114
import { isDefined } from 'twenty-shared/utils';
15+
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
1216

1317
export const triggerDestroyRecordsOptimisticEffect = ({
1418
cache,
@@ -32,18 +36,52 @@ export const triggerDestroyRecordsOptimisticEffect = ({
3236
fields: {
3337
[objectMetadataItem.namePlural]: (
3438
rootQueryCachedResponse,
35-
{ readField },
39+
{ readField, storeFieldName },
3640
) => {
37-
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
38-
!isObjectRecordConnectionWithRefs(
39-
objectMetadataItem.nameSingular,
40-
rootQueryCachedResponse,
41+
const { fieldVariables: rootQueryVariables } =
42+
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
43+
storeFieldName,
4144
);
4245

43-
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
46+
if (
47+
!isObjectRecordConnection(
48+
objectMetadataItem.nameSingular,
49+
rootQueryCachedResponse,
50+
)
51+
) {
4452
return rootQueryCachedResponse;
4553
}
4654

55+
const totalCount = readField<number | undefined>(
56+
'totalCount',
57+
rootQueryCachedResponse,
58+
);
59+
60+
const recordsMatchingRootQueryFilter = recordsToDestroy.filter(
61+
(record) =>
62+
isRecordMatchingFilter({
63+
record,
64+
filter: rootQueryVariables?.filter ?? {},
65+
objectMetadataItem,
66+
}),
67+
);
68+
69+
const newTotalCount = isDefined(totalCount)
70+
? Math.max(totalCount - recordsMatchingRootQueryFilter.length, 0)
71+
: undefined;
72+
73+
if (
74+
!isObjectRecordConnectionWithRefs(
75+
objectMetadataItem.nameSingular,
76+
rootQueryCachedResponse,
77+
)
78+
) {
79+
return {
80+
...rootQueryCachedResponse,
81+
totalCount: newTotalCount,
82+
};
83+
}
84+
4785
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
4886

4987
const recordIdsToDestroy = recordsToDestroy.map(({ id }) => id);
@@ -52,11 +90,6 @@ export const triggerDestroyRecordsOptimisticEffect = ({
5290
rootQueryCachedObjectRecordConnection,
5391
);
5492

55-
const totalCount = readField<number | undefined>(
56-
'totalCount',
57-
rootQueryCachedObjectRecordConnection,
58-
);
59-
6093
const nextCachedEdges =
6194
cachedEdges?.filter((cachedEdge) => {
6295
const nodeId = readField<string>('id', cachedEdge.node);
@@ -65,14 +98,15 @@ export const triggerDestroyRecordsOptimisticEffect = ({
6598
}) || [];
6699

67100
if (nextCachedEdges.length === cachedEdges?.length)
68-
return rootQueryCachedObjectRecordConnection;
101+
return {
102+
...rootQueryCachedObjectRecordConnection,
103+
totalCount: newTotalCount,
104+
};
69105

70106
return {
71107
...rootQueryCachedObjectRecordConnection,
72108
edges: nextCachedEdges,
73-
totalCount: isDefined(totalCount)
74-
? totalCount - recordIdsToDestroy.length
75-
: undefined,
109+
totalCount: newTotalCount,
76110
};
77111
},
78112
},

packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ export const triggerUpdateRecordOptimisticEffect = ({
8282
objectMetadataItem,
8383
});
8484

85+
const currentRecordIndexInRootQueryEdges = isRecordMatchingFilter({
86+
record: currentRecord,
87+
filter: rootQueryFilter ?? {},
88+
objectMetadataItem,
89+
});
90+
91+
const totalCount = readField<number | undefined>(
92+
'totalCount',
93+
rootQueryConnection,
94+
);
95+
96+
const newTotalCount = isDefined(totalCount)
97+
? Math.max(
98+
totalCount +
99+
(updatedRecordMatchesThisRootQueryFilter ? 1 : 0) +
100+
(currentRecordIndexInRootQueryEdges ? -1 : 0),
101+
0,
102+
)
103+
: undefined;
104+
85105
const updatedRecordIndexInRootQueryEdges =
86106
rootQueryCurrentEdges.findIndex(
87107
(cachedEdge) =>
@@ -131,6 +151,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
131151
return {
132152
...rootQueryConnection,
133153
edges: rootQueryNextEdges,
154+
totalCount: newTotalCount,
134155
};
135156
},
136157
},

packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
77
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
88
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
99
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
10-
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
11-
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
10+
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
1211
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
1312
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
1413
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
1514
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
1615
import { type ApolloCache } from '@apollo/client';
1716
import { isArray } from '@sniptt/guards';
18-
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
17+
import {
18+
FieldMetadataType,
19+
RelationType,
20+
type ObjectPermissions,
21+
} from 'twenty-shared/types';
1922
import {
2023
computeMorphRelationFieldName,
2124
CustomError,
@@ -147,12 +150,12 @@ const triggerUpdateRelationOptimisticEffect = ({
147150
}
148151

149152
const currentFieldValueOnSourceRecord:
150-
| RecordGqlConnection
153+
| RecordGqlConnectionEdgesRequired
151154
| RecordGqlNode
152155
| null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
153156

154157
const updatedFieldValueOnSourceRecord:
155-
| RecordGqlConnection
158+
| RecordGqlConnectionEdgesRequired
156159
| RecordGqlNode
157160
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
158161

@@ -179,6 +182,7 @@ const triggerUpdateRelationOptimisticEffect = ({
179182
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
180183
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
181184
);
185+
182186
const gqlFieldNameOnTargetRecord =
183187
targetFieldMetadataFullObject.type === FieldMetadataType.RELATION
184188
? targetFieldMetadataFullObject.name
@@ -189,7 +193,10 @@ const triggerUpdateRelationOptimisticEffect = ({
189193
sourceObjectMetadataItem.nameSingular,
190194
targetObjectMetadataNamePlural: sourceObjectMetadataItem.namePlural,
191195
});
192-
if (shouldCascadeDeleteTargetRecords) {
196+
if (
197+
shouldCascadeDeleteTargetRecords &&
198+
targetRecordsToDetachFrom.length > 0
199+
) {
193200
triggerDestroyRecordsOptimisticEffect({
194201
cache,
195202
objectMetadataItem: fullTargetObjectMetadataItem,
@@ -198,7 +205,10 @@ const triggerUpdateRelationOptimisticEffect = ({
198205
upsertRecordsInStore,
199206
objectPermissionsByObjectMetadataId,
200207
});
201-
} else if (isDefined(currentSourceRecord)) {
208+
} else if (
209+
isDefined(currentSourceRecord) &&
210+
targetRecordsToDetachFrom.length > 0
211+
) {
202212
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
203213
triggerDetachRelationOptimisticEffect({
204214
cache,
@@ -306,12 +316,12 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
306316
}
307317

308318
const currentFieldValueOnSourceRecord:
309-
| RecordGqlConnection
319+
| RecordGqlConnectionEdgesRequired
310320
| RecordGqlNode
311321
| null = currentSourceRecord?.[gqlFieldMorphRelation];
312322

313323
const updatedFieldValueOnSourceRecord:
314-
| RecordGqlConnection
324+
| RecordGqlConnectionEdgesRequired
315325
| RecordGqlNode
316326
| null = updatedSourceRecord?.[gqlFieldMorphRelation];
317327

@@ -338,7 +348,10 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
338348
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
339349
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
340350
);
341-
if (shouldCascadeDeleteTargetRecords) {
351+
if (
352+
shouldCascadeDeleteTargetRecords &&
353+
targetRecordsToDetachFrom.length > 0
354+
) {
342355
triggerDestroyRecordsOptimisticEffect({
343356
cache,
344357
objectMetadataItem: fullTargetObjectMetadataItem,
@@ -347,7 +360,10 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
347360
objectPermissionsByObjectMetadataId,
348361
upsertRecordsInStore,
349362
});
350-
} else if (isDefined(currentSourceRecord)) {
363+
} else if (
364+
isDefined(currentSourceRecord) &&
365+
targetRecordsToDetachFrom.length > 0
366+
) {
351367
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
352368
triggerDetachRelationOptimisticEffect({
353369
cache,
@@ -387,7 +403,7 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
387403
};
388404

389405
const extractTargetRecordsFromRelation = (
390-
value: RecordGqlConnection | RecordGqlNode | null,
406+
value: RecordGqlConnectionEdgesRequired | RecordGqlNode | null,
391407
relation: FieldMetadataItemRelation,
392408
): RecordGqlNode[] => {
393409
// TODO investigate on the root cause of array injection here, should never occurs
@@ -399,9 +415,9 @@ const extractTargetRecordsFromRelation = (
399415
if (!isDefined(relation)) {
400416
throw new Error('Relation found is undefined');
401417
}
402-
if (isObjectRecordConnection(relation, value)) {
403-
return value.edges.map(({ node }) => node);
418+
if (relation.type === RelationType.ONE_TO_MANY) {
419+
return value.edges.map(({ node }: { node: RecordGqlNode }) => node);
404420
}
405421

406-
return [value];
422+
return [value as RecordGqlNode];
407423
};
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
2-
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
2+
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
33

4-
export type RecordGqlRefConnection = Omit<RecordGqlConnection, 'edges'> & {
4+
export type RecordGqlRefConnection = Omit<
5+
RecordGqlConnectionEdgesRequired,
6+
'edges'
7+
> & {
58
edges: RecordGqlRefEdge[];
69
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`getRecordFromRecordNode should convert a simple record node 1`] = `
4+
{
5+
"__typename": "Person",
6+
"email": "john@example.com",
7+
"id": "123",
8+
"name": "John Doe",
9+
}
10+
`;
11+
12+
exports[`getRecordFromRecordNode should handle array values 1`] = `
13+
{
14+
"__typename": "Person",
15+
"id": "123",
16+
"scores": [
17+
100,
18+
200,
19+
300,
20+
],
21+
"tags": [
22+
"developer",
23+
"designer",
24+
],
25+
}
26+
`;
27+
28+
exports[`getRecordFromRecordNode should handle connection fields with edges 1`] = `
29+
{
30+
"__typename": "Company",
31+
"id": "123",
32+
"name": "Acme Inc",
33+
"people": [
34+
{
35+
"__typename": "Person",
36+
"id": "456",
37+
"name": "John Doe",
38+
},
39+
{
40+
"__typename": "Person",
41+
"id": "789",
42+
"name": "Jane Smith",
43+
},
44+
],
45+
}
46+
`;
47+
48+
exports[`getRecordFromRecordNode should handle deeply nested structures 1`] = `
49+
{
50+
"__typename": "Workspace",
51+
"id": "123",
52+
"settings": {
53+
"__typename": "WorkspaceSettings",
54+
"display": {
55+
"__typename": "DisplaySettings",
56+
"layout": "compact",
57+
"theme": "dark",
58+
},
59+
},
60+
}
61+
`;
62+
63+
exports[`getRecordFromRecordNode should handle mixed nested objects and connections 1`] = `
64+
{
65+
"__typename": "Company",
66+
"address": {
67+
"__typename": "Address",
68+
"city": "New York",
69+
"country": "USA",
70+
},
71+
"employees": [
72+
{
73+
"__typename": "Person",
74+
"id": "456",
75+
"name": {
76+
"__typename": "FullName",
77+
"firstName": "John",
78+
"lastName": "Doe",
79+
},
80+
},
81+
],
82+
"id": "123",
83+
"name": "Acme Inc",
84+
}
85+
`;
86+
87+
exports[`getRecordFromRecordNode should handle nested object fields 1`] = `
88+
{
89+
"__typename": "Person",
90+
"id": "123",
91+
"name": {
92+
"__typename": "FullName",
93+
"firstName": "John",
94+
"lastName": "Doe",
95+
},
96+
}
97+
`;
98+
99+
exports[`getRecordFromRecordNode should handle null and undefined values 1`] = `
100+
{
101+
"__typename": "Person",
102+
"email": null,
103+
"id": "123",
104+
"name": "John Doe",
105+
"phone": undefined,
106+
}
107+
`;

0 commit comments

Comments
 (0)