From 51c0110aa76ecbd2f2a419141d7d267f739e1384 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 23 Aug 2024 13:56:37 -0600 Subject: [PATCH] Fix issue where nonreactive with cache update to child field would trigger update --- src/__tests__/dataMasking.ts | 121 +++++++++++++++++++++++++++++++++++ src/core/ApolloClient.ts | 11 +++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 1f1d047f596..c61529d4fd0 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -1996,6 +1996,127 @@ test("does not trigger update to watched fragments when updating field in named }); }); +test("does not trigger update to watched fragments when updating parent field with @nonreactive and child field", async () => { + interface UserFieldsFragment { + __typename: "User"; + id: number; + lastUpdatedAt: string; + } + + interface ProfileFieldsFragment { + __typename: "User"; + lastUpdatedAt: string; + } + + const profileFieldsFragment: TypedDocumentNode = + gql` + fragment ProfileFields on User { + age + lastUpdatedAt @nonreactive + } + `; + + const userFieldsFragment: TypedDocumentNode = gql` + fragment UserFields on User { + id + lastUpdatedAt @nonreactive + ...ProfileFields + } + + ${profileFieldsFragment} + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-01", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 30, + }, + }); + + const userFieldsObservable = client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const profileFieldsObservable = client.watchFragment({ + fragment: profileFieldsFragment, + from: { __typename: "User", id: 1 }, + }); + + const userFieldsStream = new ObservableStream(userFieldsObservable); + const profileFieldsStream = new ObservableStream(profileFieldsObservable); + + { + const { data } = await userFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-01", + }); + } + + { + const { data } = await profileFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 30, + lastUpdatedAt: "2024-01-01", + }); + } + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-02", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 31, + }, + }); + + { + const { data } = await profileFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 31, + lastUpdatedAt: "2024-01-02", + }); + } + + await expect(userFieldsStream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + + expect( + client.readFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + id: "User:1", + }) + ).toEqual({ + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-02", + age: 31, + }); +}); + test("warns when accessing an unmasked field on a watched fragment while using @unmask with mode: 'migrate'", async () => { using consoleSpy = spyOnConsole("warn"); diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 1c82a2bcf3e..fd6127e930b 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -164,7 +164,7 @@ import type { WatchFragmentOptions, WatchFragmentResult, } from "../cache/core/cache.js"; -import { equal } from "@wry/equality"; +import { equalByQuery } from "./equalByQuery.js"; export { mergeOptions }; /** @@ -562,7 +562,14 @@ export class ApolloClient implements DataProxy { data: result.data, }); - if (equal(result, latestResult)) { + if ( + latestResult && + equalByQuery( + this.cache["getFragmentDoc"](fragment, fragmentName), + { data: latestResult.data }, + { data: result.data } + ) + ) { return; }