From 07bf2903296c537213d3b9e3d09c3afc38c7fda5 Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Thu, 29 Jan 2026 17:39:51 +0100 Subject: [PATCH 1/3] fix(apollo-forest-run): apollo-compatible behavior for corrupt writes --- .../src/__tests__/regression.test.ts | 192 ++++++++++++++++++ .../apollo-forest-run/src/diff/diffObject.ts | 9 + 2 files changed, 201 insertions(+) diff --git a/packages/apollo-forest-run/src/__tests__/regression.test.ts b/packages/apollo-forest-run/src/__tests__/regression.test.ts index c123027d3..70cf8d611 100644 --- a/packages/apollo-forest-run/src/__tests__/regression.test.ts +++ b/packages/apollo-forest-run/src/__tests__/regression.test.ts @@ -830,6 +830,167 @@ test("treats incorrect list items as empty objects", () => { expect(data.complete).toBe(false); }); +test("does not crash when object diff is applied to list field with key collision", () => { + // Schema (valid): + // type ParentInfo { id: ID! items: [ChildItem!] note: String } + // type ChildItem { id: ID! details: ParentInfo } + // type Query { parentInfo: ParentInfo childItem: ChildItem } + const listQuery = gql` + { + parentInfo { + id + items { + id + } + } + } + `; + const objectQuery = gql` + { + childItem { + id + details { + # id + note + } + } + } + `; + + const cache = new ForestRun({ + dataIdFromObject: (object: any) => object.id, + }); + + cache.write({ + query: listQuery, + result: { + parentInfo: { + __typename: "ParentInfo", + id: "1", + items: [ + { + __typename: "ChildItem", + id: "1", + }, + ], + }, + }, + }); + + cache.write({ + query: objectQuery, + result: { + childItem: { + __typename: "ChildItem", + id: "1", + details: { + // id: "2", + note: "old", + }, + }, + }, + }); + + // This currently throws inside updateObject (assert on non-object base). + const run = () => + cache.write({ + query: objectQuery, + result: { + childItem: { + __typename: "ChildItem", + id: "1", + details: { + // id: "2", + note: "new", + }, + }, + }, + }); + + expect(run).not.toThrow(); +}); + +test("matches apollo InMemoryCache behavior on incorrect cache overwrites", () => { + const listQuery = gql` + query ListQuery { + container { + __typename + id + entries { + __typename + note + } + } + } + `; + const objectQuery = gql` + query ObjectQuery { + container { + __typename + id + entries { + __typename + note + } + } + } + `; + + const forestRun = new ForestRun(); + + const op1 = { + container: { + __typename: "Container", + id: "1", + entries: [ + { + __typename: "Entry", + note: "old", + }, + ], + }, + }; + const op2 = { + container: { + __typename: "Container", + id: "1", + entries: { + __typename: "Entry", + note: "old", + }, + }, + }; + const op3 = { + container: { + __typename: "Container", + id: "1", + entries: { + __typename: "Entry", + note: "new", + }, + }, + }; + + forestRun.write({ query: listQuery, result: op1 }); + forestRun.write({ query: objectQuery, result: op2 }); + forestRun.write({ query: objectQuery, result: op3 }); + + const forestList = forestRun.read({ query: listQuery, optimistic: true }); + const forestObj = forestRun.read({ query: objectQuery, optimistic: true }); + + // This is what InMemoryCache produces for both queries + const apolloCompatibleResult = { + container: { + __typename: "Container", + id: "1", + entries: { __typename: "Entry", note: "new" }, + }, + }; + + expect(forestList).toEqual(apolloCompatibleResult); + expect(forestObj).toEqual(apolloCompatibleResult); +}); + test("consistent root-level __typename in optimistic response 1", () => { const query = gql` { @@ -1096,3 +1257,34 @@ test("correctly handles optimistic fragment write for deeply nested node", () => { items: [item1, item2] }, ]); }); + +test.skip("cache redirects to missing nodes result in missing fields", () => { + const cache = new ForestRun({ + typePolicies: { + Query: { + fields: { + pet(existingData, { args, toReference }) { + return ( + existingData || toReference({ __typename: "Pet", id: args?.id }) + ); + }, + }, + }, + }, + }); + + const query = gql` + { + pet(id: "missing") { + id + name + } + } + `; + const result = cache.diff({ query, optimistic: true }); + expect(result).toEqual({ + complete: false, + result: { pet: null }, + missing: [], + }); +}); diff --git a/packages/apollo-forest-run/src/diff/diffObject.ts b/packages/apollo-forest-run/src/diff/diffObject.ts index ce4b3a337..d9dddec99 100644 --- a/packages/apollo-forest-run/src/diff/diffObject.ts +++ b/packages/apollo-forest-run/src/diff/diffObject.ts @@ -336,6 +336,15 @@ function diffValueInternal( ? undefined : Difference.createReplacement(base, model); } + // ApolloCompat: wrong structural writes are still acceptable by Apollo client (instead of throwing errors) + if (Value.isCompositeValue(base) || Value.isCompositeValue(model)) { + if (!Value.isCompositeValue(base) || !Value.isCompositeValue(model)) { + return Difference.createReplacement(base, model); + } + if (base.kind !== model.kind) { + return Difference.createReplacement(base, model); + } + } switch (base.kind) { case ValueKind.CompositeNull: { assert(!currentDiff); From 445dfda50d2610314d5cb472a77cdf8fac44382b Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Thu, 29 Jan 2026 17:41:01 +0100 Subject: [PATCH 2/3] remove leftover code --- .../src/__tests__/regression.test.ts | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/apollo-forest-run/src/__tests__/regression.test.ts b/packages/apollo-forest-run/src/__tests__/regression.test.ts index 70cf8d611..e5a0c7756 100644 --- a/packages/apollo-forest-run/src/__tests__/regression.test.ts +++ b/packages/apollo-forest-run/src/__tests__/regression.test.ts @@ -1257,34 +1257,3 @@ test("correctly handles optimistic fragment write for deeply nested node", () => { items: [item1, item2] }, ]); }); - -test.skip("cache redirects to missing nodes result in missing fields", () => { - const cache = new ForestRun({ - typePolicies: { - Query: { - fields: { - pet(existingData, { args, toReference }) { - return ( - existingData || toReference({ __typename: "Pet", id: args?.id }) - ); - }, - }, - }, - }, - }); - - const query = gql` - { - pet(id: "missing") { - id - name - } - } - `; - const result = cache.diff({ query, optimistic: true }); - expect(result).toEqual({ - complete: false, - result: { pet: null }, - missing: [], - }); -}); From 68f5549e997ebbf6161ed35bfdac34ea49f429c2 Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Thu, 29 Jan 2026 17:45:01 +0100 Subject: [PATCH 3/3] Change files --- ...lo-forest-run-d7de5893-1745-4330-b576-212741802d65.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-apollo-forest-run-d7de5893-1745-4330-b576-212741802d65.json diff --git a/change/@graphitation-apollo-forest-run-d7de5893-1745-4330-b576-212741802d65.json b/change/@graphitation-apollo-forest-run-d7de5893-1745-4330-b576-212741802d65.json new file mode 100644 index 000000000..b31843962 --- /dev/null +++ b/change/@graphitation-apollo-forest-run-d7de5893-1745-4330-b576-212741802d65.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(apollo-forest-run): apollo-compatible behavior for corrupt writes", + "packageName": "@graphitation/apollo-forest-run", + "email": "vrazuvaev@microsoft.com_msteamsmdb", + "dependentChangeType": "patch" +}