Skip to content

Commit

Permalink
[Data masking] Allow null as a valid from value (#12131)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller authored Nov 15, 2024
1 parent 1e7d009 commit 21c3f08
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .api-reports/api-report-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2251,7 +2251,7 @@ export interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptio
// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts
//
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_hooks.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2075,7 +2075,7 @@ export interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptio
// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts
//
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2138,7 +2138,7 @@ interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoIn
// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts
//
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2924,7 +2924,7 @@ export function useFragment<TData = any, TVars = OperationVariables>(options: Us
export interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoInfer_2<TData>, NoInfer_2<TVars>>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit<Cache_2.ReadFragmentOptions<TData, TVars>, "id" | "variables" | "returnPartialData"> {
client?: ApolloClient<any>;
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}
Expand Down
5 changes: 5 additions & 0 deletions .changeset/long-zoos-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": minor
---

Allow `null` as a valid `from` value in `useFragment`.
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": 41573,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34361
"dist/apollo-client.min.cjs": 41601,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34359
}
91 changes: 90 additions & 1 deletion src/react/hooks/__tests__/useFragment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,95 @@ describe("useFragment", () => {
}
});

it("allows `null` as valid `from` value without warning", async () => {
using _ = spyOnConsole("warn");

interface Fragment {
age: number;
}

const fragment: TypedDocumentNode<Fragment, never> = gql`
fragment UserFields on User {
age
}
`;

const client = new ApolloClient({ cache: new InMemoryCache() });

const { takeSnapshot } = renderHookToSnapshotStream(
() => useFragment({ fragment, from: null }),
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
}
);

{
const { data, complete } = await takeSnapshot();

expect(data).toEqual({});
expect(complete).toBe(false);
}

expect(console.warn).not.toHaveBeenCalled();
});

it("properly handles changing from null to valid from value", async () => {
using _ = spyOnConsole("warn");

interface Fragment {
__typename: "User";
id: string;
age: number;
}

const fragment: TypedDocumentNode<Fragment, never> = gql`
fragment UserFields on User {
__typename
id
age
}
`;

const client = new ApolloClient({ cache: new InMemoryCache() });

client.writeFragment({
fragment,
data: {
__typename: "User",
id: "1",
age: 30,
},
});

const { takeSnapshot, rerender } = renderHookToSnapshotStream(
({ from }) => useFragment({ fragment, from }),
{
initialProps: { from: null as UseFragmentOptions<any, never>["from"] },
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
}
);

{
const { data, complete } = await takeSnapshot();

expect(data).toEqual({});
expect(complete).toBe(false);
}

rerender({ from: { __typename: "User", id: "1" } });

{
const { data, complete } = await takeSnapshot();

expect(data).toEqual({ __typename: "User", id: "1", age: 30 });
expect(complete).toBe(true);
}
});

describe("tests with incomplete data", () => {
let cache: InMemoryCache, wrapper: React.FunctionComponent;
const ItemFragment = gql`
Expand Down Expand Up @@ -2327,7 +2416,7 @@ describe.skip("Type Tests", () => {

test("UseFragmentOptions interface shape", <TData, TVars>() => {
expectTypeOf<UseFragmentOptions<TData, TVars>>().branded.toEqualTypeOf<{
from: string | StoreObject | Reference | FragmentType<TData>;
from: string | StoreObject | Reference | FragmentType<TData> | null;
fragment: DocumentNode | TypedDocumentNode<TData, TVars>;
fragmentName?: string;
optimistic?: boolean;
Expand Down
55 changes: 36 additions & 19 deletions src/react/hooks/useFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface UseFragmentOptions<TData, TVars>
Cache.ReadFragmentOptions<TData, TVars>,
"id" | "variables" | "returnPartialData"
> {
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string;
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string | null;
// Override this field to make it optional (default: true).
optimistic?: boolean;
/**
Expand Down Expand Up @@ -73,7 +73,10 @@ function _useFragment<TData = any, TVars = OperationVariables>(
// `stableOptions` and retrigger our subscription. If the cache identifier
// stays the same between renders, we want to reuse the existing subscription.
const id = React.useMemo(
() => (typeof from === "string" ? from : cache.identify(from)),
() =>
typeof from === "string" ? from
: from === null ? null
: cache.identify(from),
[cache, from]
);

Expand All @@ -83,6 +86,16 @@ function _useFragment<TData = any, TVars = OperationVariables>(
// get the correct diff on the next render given new diffOptions
const diff = React.useMemo(() => {
const { fragment, fragmentName, from, optimistic = true } = stableOptions;

if (from === null) {
return {
result: diffToResult({
result: {} as TData,
complete: false,
}),
};
}

const { cache } = client;
const diff = cache.diff<TData>({
...stableOptions,
Expand Down Expand Up @@ -111,24 +124,28 @@ function _useFragment<TData = any, TVars = OperationVariables>(
React.useCallback(
(forceUpdate) => {
let lastTimeout = 0;
const subscription = client.watchFragment(stableOptions).subscribe({
next: (result) => {
// Since `next` is called async by zen-observable, we want to avoid
// unnecessarily rerendering this hook for the initial result
// emitted from watchFragment which should be equal to
// `diff.result`.
if (equal(result, diff.result)) return;
diff.result = result;
// If we get another update before we've re-rendered, bail out of
// the update and try again. This ensures that the relative timing
// between useQuery and useFragment stays roughly the same as
// fixed in https://github.com/apollographql/apollo-client/pull/11083
clearTimeout(lastTimeout);
lastTimeout = setTimeout(forceUpdate) as any;
},
});

const subscription =
stableOptions.from === null ?
null
: client.watchFragment(stableOptions).subscribe({
next: (result) => {
// Since `next` is called async by zen-observable, we want to avoid
// unnecessarily rerendering this hook for the initial result
// emitted from watchFragment which should be equal to
// `diff.result`.
if (equal(result, diff.result)) return;
diff.result = result;
// If we get another update before we've re-rendered, bail out of
// the update and try again. This ensures that the relative timing
// between useQuery and useFragment stays roughly the same as
// fixed in https://github.com/apollographql/apollo-client/pull/11083
clearTimeout(lastTimeout);
lastTimeout = setTimeout(forceUpdate) as any;
},
});
return () => {
subscription.unsubscribe();
subscription?.unsubscribe();
clearTimeout(lastTimeout);
};
},
Expand Down

0 comments on commit 21c3f08

Please sign in to comment.