Skip to content

Commit

Permalink
Merge branch 'main' into release-3.8
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas authored May 3, 2023
2 parents ffb179e + a625277 commit a8e555a
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-buttons-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Deprecate `useFragment` `returnPartialData` option
9 changes: 9 additions & 0 deletions .changeset/warm-zebras-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@apollo/client": patch
---

Fix type signature of `ServerError`.

In <3.7 `HttpLink` and `BatchHttpLink` would return a `ServerError.message` of e.g. `"Unexpected token 'E', \"Error! Foo bar\" is not valid JSON"` and a `ServerError.result` of `undefined` in the case where a server returned a >= 300 response code with a response body containing a string that could not be parsed as JSON.

In >=3.7, `message` became e.g. `Response not successful: Received status code 302` and `result` became the string from the response body, however the type in `ServerError.result` was not updated to include the `string` type, which is now properly reflected.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,22 @@
### Patch Changes

- [#10470](https://github.com/apollographql/apollo-client/pull/10470) [`47435e879`](https://github.com/apollographql/apollo-client/commit/47435e879ebc867d9fc3de5b6fd5785204b4dbd4) Thanks [@alessbell](https://github.com/alessbell)! - Bumps TypeScript to `4.9.4` (previously `4.7.4`) and updates types to account for changes in TypeScript 4.8 by [propagating contstraints on generic types](https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#unconstrained-generics-no-longer-assignable-to). Technically this makes some types stricter as attempting to pass `null|undefined` into certain functions is now disallowed by TypeScript, but these were never expected runtime values in the first place.
This should only affect you if you are wrapping functions provided by Apollo Client with your own abstractions that pass in their generics as type arguments, in which case you might get an error like `error TS2344: Type 'YourGenericType' does not satisfy the constraint 'OperationVariables'`. In that case, make sure that `YourGenericType` is restricted to a type that only accepts objects via `extends`, like `Record<string, any>` or `@apollo/client`'s `OperationVariables`:
```diff
import {
QueryHookOptions,
QueryResult,
useQuery,
+ OperationVariables,
} from '@apollo/client';
- export function useWrappedQuery<T, TVariables>(
+ export function useWrappedQuery<T, TVariables extends OperationVariables>(
query: DocumentNode,
queryOptions: QueryHookOptions<T, TVariables>
): QueryResult<T, TVariables> {
const [execute, result] = useQuery<T, TVariables>(query);
}
```

- [#10408](https://github.com/apollographql/apollo-client/pull/10408) [`55ffafc58`](https://github.com/apollographql/apollo-client/commit/55ffafc585e9eb66314755b4f40804b8b8affb13) Thanks [@zlrlo](https://github.com/zlrlo)! - fix: modify BatchHttpLink to have a separate timer for each different batch key

Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🔮 Apollo Client Roadmap

**Last updated: 2023-04-04**
**Last updated: 2023-05-02**

For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md).

Expand All @@ -15,7 +15,7 @@ For up to date release notes, refer to the project's [Changelog](https://github.

## [3.8.0](https://github.com/apollographql/apollo-client/milestone/30)

_Approximate Date: 2023-05-05 (Beta), GA TBD after user feedback_
_Approximate Date: 2023-05-12 (Beta), GA TBD after user feedback_

Currently in active development and being shipped in a series alpha releases. React 18 users will get a lot out of this release since it introduces support for Suspense and (for you server-side rendering enthusiasts) `renderToPipeableStream`. There are also new features added to the core as well. Here's a brief overview:

Expand Down
4 changes: 2 additions & 2 deletions docs/source/data/mutations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,15 @@ If you know that your app usually needs to refetch certain queries after a parti
// Refetches two queries after mutation completes
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
refetchQueries: [
{query: GET_POST}, // DocumentNode object parsed with gql
GET_POST, // DocumentNode object parsed with gql
'GetComments' // Query name
],
});
```

Each element in the `refetchQueries` array is one of the following:

* An object referencing `query` (a `DocumentNode` object parsed with the `gql` function) and `variables`
* A `DocumentNode` object parsed with the `gql` function
* The name of a query you've previously executed, as a string (e.g., `GetComments`)
* To refer to queries by name, make sure each of your app's queries have a _unique_ name.

Expand Down
20 changes: 19 additions & 1 deletion src/link/http/__tests__/HttpLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,11 @@ describe('HttpLink', () => {
responseBody = JSON.parse(responseBodyText);
return Promise.resolve(responseBodyText);
});
const textWithStringError = jest.fn(() => {
const responseBodyText = 'Error! Foo bar';
responseBody = responseBodyText;
return Promise.resolve(responseBodyText);
});
const textWithData = jest.fn(() => {
responseBody = {
data: { stub: { id: 1 } },
Expand Down Expand Up @@ -1477,6 +1482,20 @@ describe('HttpLink', () => {
}),
);
});
itAsync('throws an error if response code is > 300 and handles string response body', (resolve, reject) => {
fetch.mockReturnValueOnce(Promise.resolve({ status: 302, text: textWithStringError }));
const link = createHttpLink({ uri: 'data', fetch: fetch as any });
execute(link, { query: sampleQuery }).subscribe(
result => {
reject('next should have been thrown from the network');
},
makeCallback(resolve, reject, (e: ServerError) => {
expect(e.message).toMatch(/Received status code 302/);
expect(e.statusCode).toBe(302);
expect(e.result).toEqual(responseBody);
}),
);
});
itAsync('throws an error if response code is > 300 and returns data', (resolve, reject) => {
fetch.mockReturnValueOnce(
Promise.resolve({ status: 400, text: textWithData }),
Expand Down Expand Up @@ -1506,7 +1525,6 @@ describe('HttpLink', () => {
);

const link = createHttpLink({ uri: 'data', fetch: fetch as any });

execute(link, { query: sampleQuery }).subscribe(
result => {
reject('should not have called result because we have no data');
Expand Down
2 changes: 1 addition & 1 deletion src/link/http/parseAndCheckHttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function parseHeaders(headerText: string): Record<string, string> {
export function parseJsonBody<T>(response: Response, bodyText: string): T {
if (response.status >= 300) {
// Network error
const getResult = () => {
const getResult = (): Record<string, unknown> | string => {
try {
return JSON.parse(bodyText);
} catch (err) {
Expand Down
11 changes: 7 additions & 4 deletions src/link/persisted-queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,13 @@ export const createPersistedQueryLink = (
}

// Network errors can return GraphQL errors on for example a 403
const networkErrors =
networkError &&
networkError.result &&
networkError.result.errors as GraphQLError[];
let networkErrors;
if (typeof networkError?.result !== 'string') {
networkErrors =
networkError &&
networkError.result &&
networkError.result.errors as GraphQLError[];
}
if (isNonEmptyArray(networkErrors)) {
graphQLErrors.push(...networkErrors);
}
Expand Down
2 changes: 1 addition & 1 deletion src/link/utils/throwServerError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ServerError = Error & {
response: Response;
result: Record<string, any>;
result: Record<string, any> | string;
statusCode: number;
};

Expand Down
90 changes: 90 additions & 0 deletions src/react/hooks/__tests__/useFragment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,96 @@ describe("useFragment", () => {
]);
});
});

describe("tests with incomplete data", () => {
let cache: InMemoryCache, wrapper: React.FunctionComponent;
const ItemFragment = gql`
fragment ItemFragment on Item {
id
text
}
`;

beforeEach(() => {
cache = new InMemoryCache();

wrapper = ({ children }: any) => <MockedProvider cache={cache}>{children}</MockedProvider>;

// silence the console for the incomplete fragment write
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
cache.writeFragment({
fragment: ItemFragment,
data: {
__typename: "Item",
id: 5,
},
});
spy.mockRestore();
});

it("assumes `returnPartialData: true` per default", () => {
const { result } = renderHook(
() =>
useFragment({
fragment: ItemFragment,
from: { __typename: "Item", id: 5 },
}),
{ wrapper }
);

expect(result.current.data).toEqual({ __typename: "Item", id: 5 });
expect(result.current.complete).toBe(false);
});

it("throws an exception with `returnPartialData: false` if only partial data is available", () => {
// this is actually not intended behavior, but it is the current behavior
// let's document it in a test until we remove `returnPartialData` in 3.8

let error: Error;

renderHook(
() => {
// we can't just `expect(() => renderHook(...)).toThrow(...)` because it will render a second time, resulting in an uncaught exception
try {
useFragment({
fragment: ItemFragment,
from: { __typename: "Item", id: 5 },
returnPartialData: false,
});
} catch (e) {
error = e;
}
},
{ wrapper }
);

expect(error!.toString()).toMatch(`Error: Can't find field 'text' on Item:5 object`);
});

it("throws an exception with `returnPartialData: false` if no data is available", () => {
// this is actually not intended behavior, but it is the current behavior
// let's document it in a test until we remove `returnPartialData` in 3.8
let error: Error;

renderHook(
() => {
// we can't just `expect(() => renderHook(...)).toThrow(...)` because it will render a second time, resulting in an uncaught exception
try {
useFragment({
fragment: ItemFragment,
from: { __typename: "Item", id: 6 },
returnPartialData: false,
});
} catch (e) {
error = e;
}
},
{ wrapper }
);

expect(error!.toString()).toMatch(`Error: Dangling reference to missing Item:6 object`);
});
});
});

describe.skip("Type Tests", () => {
Expand Down
13 changes: 11 additions & 2 deletions src/react/hooks/useFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,23 @@ extends Omit<
| "query"
| "optimistic"
| "previousResult"
>, Omit<
Cache.ReadFragmentOptions<TData, TVars>,
| "returnPartialData"
>, Omit<Cache.ReadFragmentOptions<TData, TVars>,
| "id"
| "variables"
| "returnPartialData"
> {
from: StoreObject | Reference | string;
// Override this field to make it optional (default: true).
optimistic?: boolean;

/**
* Whether to return incomplete data rather than null.
* Defaults to `true`.
* @deprecated This option will be removed in Apollo Client 3.8.
* Please check `result.missing` instead.
*/
returnPartialData?: boolean;
}

// Since the above definition of UseFragmentOptions can be hard to parse without
Expand Down

0 comments on commit a8e555a

Please sign in to comment.