From 17ee80e0c10a9ad8bc235bc7d6b046a49f1ca20c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 26 Aug 2024 15:31:06 -0600 Subject: [PATCH 1/7] Handle when top-level data is null or undefined --- src/core/__tests__/masking.test.ts | 34 ++++++++++++++++++++++++++++++ src/core/masking.ts | 5 +++++ 2 files changed, 39 insertions(+) diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index 3eaa1bdb5cb..b2d5b88e970 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -56,6 +56,40 @@ describe("maskOperation", () => { ); }); + test("returns null when data is null", () => { + const query = gql` + query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskOperation(null, query, new InMemoryCache()); + + expect(data).toBe(null); + }); + + test("returns undefined when data is undefined", () => { + const query = gql` + query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskOperation(undefined, query, new InMemoryCache()); + + expect(data).toBe(undefined); + }); + test("strips top-level fragment data from query", () => { const query = gql` query { diff --git a/src/core/masking.ts b/src/core/masking.ts index ee82723e6b3..fc86af67dd7 100644 --- a/src/core/masking.ts +++ b/src/core/masking.ts @@ -40,6 +40,11 @@ export function maskOperation( "Expected a parsed GraphQL document with a query, mutation, or subscription." ); + if (data == null) { + // Maintain the original `null` or `undefined` value + return data; + } + const context: MaskingContext = { operationType: definition.operation, operationName: definition.name?.value, From 7c68c121136d1a92ccb202f7d5cf2b110b767249 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 26 Aug 2024 15:43:46 -0600 Subject: [PATCH 2/7] Ensure nulls returned for data with child selection sets don't fail --- src/core/__tests__/masking.test.ts | 35 ++++++++++++++++++++++++++++++ src/core/masking.ts | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index b2d5b88e970..1cde14a10cb 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -211,6 +211,41 @@ describe("maskOperation", () => { }); }); + test("handles nulls in child selection sets", () => { + const query = gql` + query { + user { + profile { + id + } + ...UserFields + } + } + fragment UserFields on User { + profile { + id + fullName + } + } + `; + + const nullUser = maskOperation( + deepFreeze({ user: null }), + query, + new InMemoryCache() + ); + const nullProfile = maskOperation( + deepFreeze({ user: { __typename: "User", profile: null } }), + query, + new InMemoryCache() + ); + + expect(nullUser).toEqual({ user: null }); + expect(nullProfile).toEqual({ + user: { __typename: "User", profile: null }, + }); + }); + test("deep freezes the masked result if the original data is frozen", () => { const query = gql` query { diff --git a/src/core/masking.ts b/src/core/masking.ts index fc86af67dd7..7bffcb266d8 100644 --- a/src/core/masking.ts +++ b/src/core/masking.ts @@ -160,7 +160,7 @@ function maskSelectionSet( memo[keyName] = data[keyName]; - if (childSelectionSet) { + if (childSelectionSet && data[keyName] !== null) { const [masked, childChanged] = maskSelectionSet( data[keyName], childSelectionSet, From e25407abf73aef0ca2e5df64d44f14aad06e40cd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 26 Aug 2024 15:44:05 -0600 Subject: [PATCH 3/7] Ensure nulls in array values are handled --- src/core/__tests__/masking.test.ts | 59 ++++++++++++++++++++++++++++++ src/core/masking.ts | 4 ++ 2 files changed, 63 insertions(+) diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index 1cde14a10cb..614321f3307 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -246,6 +246,65 @@ describe("maskOperation", () => { }); }); + test("handles nulls in arrays", () => { + const query = gql` + query { + users { + profile { + id + } + ...UserFields + } + } + fragment UserFields on User { + profile { + id + fullName + } + } + `; + + const nullUsers = maskOperation( + deepFreeze({ + users: [ + null, + { + __typename: "User", + profile: { __typename: "Profile", id: "1", fullName: "Test User" }, + }, + ], + }), + query, + new InMemoryCache() + ); + const nullProfiles = maskOperation( + deepFreeze({ + users: [ + { __typename: "User", profile: null }, + { + __typename: "User", + profile: { __typename: "Profile", id: "1", fullName: "Test User" }, + }, + ], + }), + query, + new InMemoryCache() + ); + + expect(nullUsers).toEqual({ + users: [ + null, + { __typename: "User", profile: { __typename: "Profile", id: "1" } }, + ], + }); + expect(nullProfiles).toEqual({ + users: [ + { __typename: "User", profile: null }, + { __typename: "User", profile: { __typename: "Profile", id: "1" } }, + ], + }); + }); + test("deep freezes the masked result if the original data is frozen", () => { const query = gql` query { diff --git a/src/core/masking.ts b/src/core/masking.ts index 7bffcb266d8..0a9a37d28c0 100644 --- a/src/core/masking.ts +++ b/src/core/masking.ts @@ -137,6 +137,10 @@ function maskSelectionSet( let changed = false; const masked = data.map((item, index) => { + if (item === null) { + return null; + } + const [masked, itemChanged] = maskSelectionSet( item, selectionSet, From 20a76470bed2ac8a480dd12768762de9149e1afa Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 26 Aug 2024 16:04:13 -0600 Subject: [PATCH 4/7] Combine two checks together into single array --- src/core/__tests__/masking.test.ts | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index 614321f3307..84060f6edaf 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -264,22 +264,10 @@ describe("maskOperation", () => { } `; - const nullUsers = maskOperation( + const data = maskOperation( deepFreeze({ users: [ null, - { - __typename: "User", - profile: { __typename: "Profile", id: "1", fullName: "Test User" }, - }, - ], - }), - query, - new InMemoryCache() - ); - const nullProfiles = maskOperation( - deepFreeze({ - users: [ { __typename: "User", profile: null }, { __typename: "User", @@ -291,14 +279,9 @@ describe("maskOperation", () => { new InMemoryCache() ); - expect(nullUsers).toEqual({ + expect(data).toEqual({ users: [ null, - { __typename: "User", profile: { __typename: "Profile", id: "1" } }, - ], - }); - expect(nullProfiles).toEqual({ - users: [ { __typename: "User", profile: null }, { __typename: "User", profile: { __typename: "Profile", id: "1" } }, ], From 5547c4c94f2a5eb23a53a75321a1184a60314bf9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 26 Aug 2024 16:08:24 -0600 Subject: [PATCH 5/7] Add tests for handling null in maskFragment --- src/core/__tests__/masking.test.ts | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index 84060f6edaf..3c884ec4a3f 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -1875,6 +1875,75 @@ describe("maskFragment", () => { }); }); + test("handles nulls in child selection sets", () => { + const fragment = gql` + fragment UserFields on User { + profile { + id + } + ...ProfileFields + } + fragment ProfileFields on User { + profile { + id + fullName + } + } + `; + + const data = maskFragment( + deepFreeze({ __typename: "User", profile: null }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ __typename: "User", profile: null }); + }); + + test("handles nulls in arrays", () => { + const fragment = gql` + fragment UserFields on Query { + users { + profile { + id + } + ...ProfileFields + } + } + fragment ProfileFields on User { + profile { + id + fullName + } + } + `; + + const data = maskFragment( + deepFreeze({ + users: [ + null, + { __typename: "User", profile: null }, + { + __typename: "User", + profile: { __typename: "Profile", id: "1", fullName: "Test User" }, + }, + ], + }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ + users: [ + null, + { __typename: "User", profile: null }, + { __typename: "User", profile: { __typename: "Profile", id: "1" } }, + ], + }); + }); + test("deep freezes the masked result if the original data is frozen", () => { const fragment = gql` fragment UserFields on User { From 00d1df14f8ded3430f27b474e3c7b0f521328667 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 26 Aug 2024 16:10:53 -0600 Subject: [PATCH 6/7] Ensure useFragment handles top-level null/undefined --- src/core/__tests__/masking.test.ts | 33 ++++++++++++++++++++++++++++++ src/core/masking.ts | 5 +++++ 2 files changed, 38 insertions(+) diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index 3c884ec4a3f..27063e7be1d 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -1821,6 +1821,39 @@ describe("maskOperation", () => { }); describe("maskFragment", () => { + test("returns null when data is null", () => { + const fragment = gql` + fragment Foo on Query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskFragment(null, fragment, new InMemoryCache(), "Foo"); + + expect(data).toBe(null); + }); + + test("returns undefined when data is undefined", () => { + const fragment = gql` + fragment Foo on Query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskFragment(undefined, fragment, new InMemoryCache(), "Foo"); + + expect(data).toBe(undefined); + }); test("masks named fragments in fragment documents", () => { const fragment = gql` fragment UserFields on User { diff --git a/src/core/masking.ts b/src/core/masking.ts index 0a9a37d28c0..226750cceb8 100644 --- a/src/core/masking.ts +++ b/src/core/masking.ts @@ -105,6 +105,11 @@ export function maskFragment( fragmentName ); + if (data == null) { + // Maintain the original `null` or `undefined` value + return data; + } + const context: MaskingContext = { operationType: "fragment", operationName: fragment.name.value, From dc21c6336b7075d61122d46ff771a53a1b0a5810 Mon Sep 17 00:00:00 2001 From: jerelmiller Date: Wed, 28 Aug 2024 15:03:45 +0000 Subject: [PATCH 7/7] Clean up Prettier, Size-limit, and Api-Extractor --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index b2800c3acfe..7eaebd5dffd 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41255, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33943 + "dist/apollo-client.min.cjs": 41282, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33969 }