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" +} diff --git a/packages/apollo-forest-run/src/__tests__/regression.test.ts b/packages/apollo-forest-run/src/__tests__/regression.test.ts index c123027d3..e5a0c7756 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` { 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);