Skip to content

Commit

Permalink
[Data masking] Ensure masking algorithm properly handles nulls (#12034)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller committed Sep 24, 2024
1 parent f6f8e95 commit 04e51b7
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .size-limits.json
Original file line number Diff line number Diff line change
@@ -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
}
213 changes: 213 additions & 0 deletions src/core/__tests__/masking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -177,6 +211,83 @@ 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("handles nulls in arrays", () => {
const query = gql`
query {
users {
profile {
id
}
...UserFields
}
}
fragment UserFields on User {
profile {
id
fullName
}
}
`;

const data = maskOperation(
deepFreeze({
users: [
null,
{ __typename: "User", profile: null },
{
__typename: "User",
profile: { __typename: "Profile", id: "1", fullName: "Test User" },
},
],
}),
query,
new InMemoryCache()
);

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 query = gql`
query {
Expand Down Expand Up @@ -1710,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 {
Expand Down Expand Up @@ -1764,6 +1908,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 {
Expand Down
16 changes: 15 additions & 1 deletion src/core/masking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export function maskOperation<TData = unknown>(
"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,
Expand Down Expand Up @@ -100,6 +105,11 @@ export function maskFragment<TData = unknown>(
fragmentName
);

if (data == null) {
// Maintain the original `null` or `undefined` value
return data;
}

const context: MaskingContext = {
operationType: "fragment",
operationName: fragment.name.value,
Expand Down Expand Up @@ -132,6 +142,10 @@ function maskSelectionSet(
let changed = false;

const masked = data.map((item, index) => {
if (item === null) {
return null;
}

const [masked, itemChanged] = maskSelectionSet(
item,
selectionSet,
Expand All @@ -155,7 +169,7 @@ function maskSelectionSet(

memo[keyName] = data[keyName];

if (childSelectionSet) {
if (childSelectionSet && data[keyName] !== null) {
const [masked, childChanged] = maskSelectionSet(
data[keyName],
childSelectionSet,
Expand Down

0 comments on commit 04e51b7

Please sign in to comment.