From 930d118c65dc381199d8e78b5f9044cad97b6b60 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:02:13 -0600 Subject: [PATCH 01/11] Add base tests for client.subscribe --- src/__tests__/dataMasking.ts | 155 ++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index eeddd834379..2dcbc3705ab 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -14,7 +14,7 @@ import { Reference, TypedDocumentNode, } from "../core"; -import { MockLink } from "../testing"; +import { MockLink, MockSubscriptionLink } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { invariant } from "../utilities/globals"; import { createFragmentRegistry } from "../cache/inmemory/fragmentRegistry"; @@ -2617,6 +2617,159 @@ describe("client.query", () => { }); }); +describe("client.subscribe", () => { + test("masks data returned from subscriptions when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + }); + + test("does not mask data returned from subscriptions by default", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + }); +}); + class TestCache extends ApolloCache { public diff(query: Cache.DiffOptions): DataProxy.DiffResult { return {}; From 54e63a91b48f2e4371926fc23ecee3fbb22ca94e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:05:02 -0600 Subject: [PATCH 02/11] Mask results returned in subscriptions --- src/core/QueryManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 23d1ddd78c8..47649ef6553 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1046,6 +1046,11 @@ export class QueryManager { delete result.errors; } + result.data = this.maskOperation({ + document: query, + data: result.data, + }); + return result; } ); From 5d6ce3fd985458d85aff18a4656abe432b893114 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:16:13 -0600 Subject: [PATCH 03/11] Remove explicit passing of dataMasking for test that checks its default behavior --- src/__tests__/dataMasking.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 2dcbc3705ab..df4a35ebfab 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -2736,7 +2736,6 @@ describe("client.subscribe", () => { const link = new MockSubscriptionLink(); const client = new ApolloClient({ - dataMasking: false, cache: new InMemoryCache(), link, }); From 0f7c517c9c97cae4a6823c0eb1bc40c63fcf7af1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:16:40 -0600 Subject: [PATCH 04/11] Add tests for errors returned from subscriptions --- src/__tests__/dataMasking.ts | 138 +++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index df4a35ebfab..1cd1d52380e 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -2767,6 +2767,144 @@ describe("client.subscribe", () => { }, }); }); + + test("handles errors returned from the subscription when errorPolicy is `none`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "none", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: null, + }, + errors: [{ message: "Something went wrong" }], + }, + }); + + const error = await stream.takeError(); + + expect(error).toEqual( + new ApolloError({ graphQLErrors: [{ message: "Something went wrong" }] }) + ); + }); + + test("handles errors returned from the subscription when errorPolicy is `all`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: null, + }, + errors: [{ message: "Something went wrong" }], + }, + }); + + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ addedComment: null }); + expect(errors).toEqual([{ message: "Something went wrong" }]); + }); + + test("masks partial data for errors returned from the subscription when errorPolicy is `all`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: null, + }, + }, + errors: [{ message: "Could not get author" }], + }, + }); + + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ addedComment: { __typename: "Comment", id: 1 } }); + expect(errors).toEqual([{ message: "Could not get author" }]); + }); }); class TestCache extends ApolloCache { From b08efb0235c1057618b099f0098579a2d28e3498 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:46:54 -0600 Subject: [PATCH 05/11] Don't mask data passed to updateQuery for subscribeToMore --- src/__tests__/dataMasking.ts | 130 ++++++++++++++++++++++++++++++++++- src/core/ObservableQuery.ts | 1 + src/core/QueryManager.ts | 31 +++++---- 3 files changed, 149 insertions(+), 13 deletions(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 1cd1d52380e..e8611fadcb7 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -14,10 +14,16 @@ import { Reference, TypedDocumentNode, } from "../core"; -import { MockLink, MockSubscriptionLink } from "../testing"; +import { + MockedResponse, + MockLink, + MockSubscriptionLink, + wait, +} from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { invariant } from "../utilities/globals"; import { createFragmentRegistry } from "../cache/inmemory/fragmentRegistry"; +import { isSubscriptionOperation } from "../utilities"; describe("client.watchQuery", () => { test("masks queries when dataMasking is `true`", async () => { @@ -2907,6 +2913,128 @@ describe("client.subscribe", () => { }); }); +describe("observable.subscribeToMore", () => { + test("masks query data, does not mask updateQuery callback when dataMasking is `true`", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ recentComment: { __typename: "Comment", id: 1 } }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ recentComment: { __typename: "Comment", id: 2 } }); + } + }); +}); + class TestCache extends ApolloCache { public diff(query: Cache.DiffOptions): DataProxy.DiffResult { return {}; diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 68cfade9b7c..998286a1929 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -607,6 +607,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, query: options.document, variables: options.variables, context: options.context, + [Symbol.for("apollo.dataMasking")]: false, }) .subscribe({ next: (subscriptionData: { data: TSubscriptionData }) => { diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 47649ef6553..36948a4c13f 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -994,16 +994,22 @@ export class QueryManager { this.getQuery(observableQuery.queryId).setObservableQuery(observableQuery); } - public startGraphQLSubscription({ - query, - fetchPolicy, - errorPolicy = "none", - variables, - context = {}, - extensions = {}, - }: SubscriptionOptions): Observable> { + public startGraphQLSubscription( + options: SubscriptionOptions + ): Observable> { + let { query, variables } = options; + const { + fetchPolicy, + errorPolicy = "none", + context = {}, + extensions = {}, + } = options; + query = this.transform(query); variables = this.getVariables(query, variables); + const dataMasking: boolean | undefined = (options as any)[ + Symbol.for("apollo.dataMasking") + ]; const makeObservable = (variables: OperationVariables) => this.getObservableFromLink(query, context, variables, extensions).map( @@ -1046,10 +1052,11 @@ export class QueryManager { delete result.errors; } - result.data = this.maskOperation({ - document: query, - data: result.data, - }); + if (dataMasking !== false) + result.data = this.maskOperation({ + document: query, + data: result.data, + }); return result; } From 6b036fc2cbe41588a3242fbd1817d4c1bbf651c4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:49:32 -0600 Subject: [PATCH 06/11] Add additional tests for data masking disabled --- src/__tests__/dataMasking.ts | 263 +++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index e8611fadcb7..4ad302699f5 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -3033,6 +3033,269 @@ describe("observable.subscribeToMore", () => { expect(data).toEqual({ recentComment: { __typename: "Comment", id: 2 } }); } }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }); + } + }); + + test("does not mask data by default", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }); + } + }); }); class TestCache extends ApolloCache { From 1be091e43016f2907f932be4a90277cd424b9b93 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:50:00 -0600 Subject: [PATCH 07/11] Tweak test description --- src/__tests__/dataMasking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 4ad302699f5..e09350b43f8 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -2913,7 +2913,7 @@ describe("client.subscribe", () => { }); }); -describe("observable.subscribeToMore", () => { +describe("observableQuery.subscribeToMore", () => { test("masks query data, does not mask updateQuery callback when dataMasking is `true`", async () => { const fragment = gql` fragment CommentFields on Comment { From d12881894d90e609abf5602d72c8220aebb2b0ca Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 16:58:00 -0600 Subject: [PATCH 08/11] Add data masking tests to useSubscription --- .../hooks/__tests__/useSubscription.test.tsx | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index 0c9002638d1..f143bc8561e 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -2060,6 +2060,144 @@ describe("ignoreResults", () => { }); }); +describe("data masking", () => { + test("masks data returned when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const ProfiledHook = profileHook(() => useSubscription(subscription)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + expect(error).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: false, + cache: new Cache(), + link, + }); + + const ProfiledHook = profileHook(() => useSubscription(subscription)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + expect(error).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); +}); + describe.skip("Type Tests", () => { test("NoInfer prevents adding arbitrary additional variables", () => { const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>; From cfa57efafd9201244443682be6e05c2fd1f7b419 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 17:02:19 -0600 Subject: [PATCH 09/11] Add test to check value passed to onData --- .../hooks/__tests__/useSubscription.test.tsx | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index f143bc8561e..3da19628b3f 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -2196,6 +2196,87 @@ describe("data masking", () => { await expect(ProfiledHook).not.toRerender(); }); + + test("masks data passed to onData callback when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const onData = jest.fn(); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { onData }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + expect(error).toBeUndefined(); + + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { addedComment: { __typename: "Comment", id: 1 } }, + loading: false, + error: undefined, + variables: undefined, + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); }); describe.skip("Type Tests", () => { From 31f23fc43d1d7dbf54bb3136813065275cd240b4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 27 Aug 2024 17:04:02 -0600 Subject: [PATCH 10/11] Add tests for @unmask --- .../hooks/__tests__/useSubscription.test.tsx | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index 3da19628b3f..ec9d33473f1 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -2277,6 +2277,96 @@ describe("data masking", () => { await expect(ProfiledHook).not.toRerender(); }); + + test("uses unmasked data when using the @unmask directive", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields @unmask + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const onData = jest.fn(); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { onData }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + expect(error).toBeUndefined(); + + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + loading: false, + error: undefined, + variables: undefined, + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); }); describe.skip("Type Tests", () => { From c288798eefbc913e24b339d8db516ad24c4f3134 Mon Sep 17 00:00:00 2001 From: jerelmiller Date: Wed, 28 Aug 2024 17:01:44 +0000 Subject: [PATCH 11/11] Clean up Prettier, Size-limit, and Api-Extractor --- .api-reports/api-report-core.api.md | 2 +- .api-reports/api-report-react.api.md | 2 +- .api-reports/api-report-react_components.api.md | 2 +- .api-reports/api-report-react_context.api.md | 2 +- .api-reports/api-report-react_hoc.api.md | 2 +- .api-reports/api-report-react_hooks.api.md | 2 +- .api-reports/api-report-react_internal.api.md | 2 +- .api-reports/api-report-react_ssr.api.md | 2 +- .api-reports/api-report-testing.api.md | 2 +- .api-reports/api-report-testing_core.api.md | 2 +- .api-reports/api-report-utilities.api.md | 2 +- .api-reports/api-report.api.md | 2 +- .size-limits.json | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index c905b809684..50206fff96f 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1895,7 +1895,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 3833078e887..e9fa0920390 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1674,7 +1674,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index b6865076e53..f78cbd59419 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1488,7 +1488,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 3e841319c74..3ae10667777 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1416,7 +1416,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 58044d8fcd9..370f0719a66 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -1461,7 +1461,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 0bdfbf7be7c..1ed5749cac2 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -1543,7 +1543,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 89eb10890d9..073b56afbcf 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -1594,7 +1594,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 15c9ebcf378..1f470c15801 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1401,7 +1401,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index d8be3b90f9b..ccc35eb16f8 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -1482,7 +1482,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 65672566046..98adfa454f3 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1439,7 +1439,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 9b9718a06f6..30fa559078e 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2209,7 +2209,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index c1b395ccdf1..f64cfc16482 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2246,7 +2246,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.size-limits.json b/.size-limits.json index b2be55833cd..1b40dd3f3b0 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41301, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33987 + "dist/apollo-client.min.cjs": 41335, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34036 }