Skip to content

Commit

Permalink
Merge pull request #6774 from apollographql/release-3.2
Browse files Browse the repository at this point in the history
Release 3.2.0
  • Loading branch information
benjamn authored Sep 14, 2020
2 parents e23b3b6 + 062b76b commit a975320
Show file tree
Hide file tree
Showing 69 changed files with 3,541 additions and 507 deletions.
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
## Apollo Client 3.2.0

## Bug Fixes

- Use `options.nextFetchPolicy` internally to restore original `FetchPolicy` after polling with `fetchPolicy: "network-only"`, so that polling does not interfere with normal query watching. <br/>
[@benjamn](https://github.com/benjamn) in [#6893](https://github.com/apollographql/apollo-client/pull/6893)

- Initialize `ObservableQuery` in `updateObservableQuery` even if `skip` is `true`. <br/>
[@mu29](https://github.com/mu29) in [#6999](https://github.com/apollographql/apollo-client/pull/6999)

- Prevent full reobservation of queries affected by optimistic mutation updates, while still delivering results from the cache. <br/>
[@benjamn](https://github.com/benjamn) in [#6854](https://github.com/apollographql/apollo-client/pull/6854)

## Improvements

- In TypeScript, all APIs that take `DocumentNode` parameters now may alternatively take `TypeDocumentNode<Data, Variables>`. This type has the same JavaScript representation but allows the APIs to infer the data and variable types instead of requiring you to specify types explicitly at the call site. <br/>
[@dotansimha](https://github.com/dotansimha) in [#6720](https://github.com/apollographql/apollo-client/pull/6720)

- Bring back an improved form of heuristic fragment matching, by allowing `possibleTypes` to specify subtype regular expression strings, which count as matches if the written result object has all the fields expected for the fragment. <br/>
[@benjamn](https://github.com/benjamn) in [#6901](https://github.com/apollographql/apollo-client/pull/6901)

- Allow `options.nextFetchPolicy` to be a function that takes the current `FetchPolicy` and returns a new (or the same) `FetchPolicy`, making `nextFetchPolicy` more suitable for global use in `defaultOptions.watchQuery`. <br/>
[@benjamn](https://github.com/benjamn) in [#6893](https://github.com/apollographql/apollo-client/pull/6893)

- Implement `useReactiveVar` hook for consuming reactive variables in React components. <br/>
[@benjamn](https://github.com/benjamn) in [#6867](https://github.com/apollographql/apollo-client/pull/6867)

- Move `apollo-link-persisted-queries` implementation to `@apollo/client/link/persisted-queries`. Try running our [automated imports transform](https://github.com/apollographql/apollo-client/tree/main/codemods/ac2-to-ac3) to handle this conversion, if you're using `apollo-link-persisted-queries`. <br/>
[@hwillson](https://github.com/hwillson) in [#6837](https://github.com/apollographql/apollo-client/pull/6837)

- Disable feud-stopping logic after any `cache.evict` or `cache.modify` operation. <br/>
[@benjamn](https://github.com/benjamn) in
[#6817](https://github.com/apollographql/apollo-client/pull/6817) and
[#6898](https://github.com/apollographql/apollo-client/pull/6898)

- Throw if `writeFragment` cannot identify `options.data` when no `options.id` provided. <br/>
[@jcreighton](https://github.com/jcreighton) in [#6859](https://github.com/apollographql/apollo-client/pull/6859)

- Provide `options.storage` object to `cache.modify` functions, as provided to `read` and `merge` functions. <br/>
[@benjamn](https://github.com/benjamn) in [#6991](https://github.com/apollographql/apollo-client/pull/6991)

- Allow `cache.modify` functions to return `details.INVALIDATE` (similar to `details.DELETE`) to invalidate the current field, causing affected queries to rerun, even if the field's value is unchanged. <br/>
[@benjamn](https://github.com/benjamn) in [#6991](https://github.com/apollographql/apollo-client/pull/6991)

- Support non-default `ErrorPolicy` values (that is, `"ignore"` and `"all"`, in addition to the default value `"none"`) for mutations and subscriptions, like we do for queries. <br/>
[@benjamn](https://github.com/benjamn) in [#7003](https://github.com/apollographql/apollo-client/pull/7003)

- Remove invariant forbidding a `FetchPolicy` of `cache-only` in `ObservableQuery#refetch`. <br/>
[@benjamn](https://github.com/benjamn) in [ccb0a79a](https://github.com/apollographql/apollo-client/pull/6774/commits/ccb0a79a588721f08bf87a131c31bf37fa3238e5), fixing [#6702](https://github.com/apollographql/apollo-client/issues/6702)

## Apollo Client 3.1.5

## Bug Fixes
Expand Down
4 changes: 4 additions & 0 deletions codemods/ac2-to-ac3/examples/link-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { BatchLink } from 'apollo-link-batch';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { setContext } from 'apollo-link-context';
import { ErrorLink } from 'apollo-link-error';
import {
VERSION,
createPersistedQueryLink,
} from 'apollo-link-persisted-queries';
import { RetryLink } from 'apollo-link-retry';
import { WebSocketLink } from 'apollo-link-ws';
// This package was unusual for having a default export.
Expand Down
1 change: 1 addition & 0 deletions codemods/ac2-to-ac3/imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default function transformer(file, api) {
'batch-http',
'context',
'error',
'persisted-queries',
'retry',
'schema',
'ws',
Expand Down
1 change: 1 addition & 0 deletions config/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const entryPoints = [
{ dirs: ['link', 'core'] },
{ dirs: ['link', 'error'] },
{ dirs: ['link', 'http'] },
{ dirs: ['link', 'persisted-queries'] },
{ dirs: ['link', 'retry'] },
{ dirs: ['link', 'schema'] },
{ dirs: ['link', 'utils'] },
Expand Down
3 changes: 2 additions & 1 deletion docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ module.exports = {
'api/link/apollo-link-rest',
'api/link/apollo-link-retry',
'api/link/apollo-link-schema',
'api/link/apollo-link-ws'
'api/link/apollo-link-ws',
'api/link/persisted-queries'
],
},
},
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/cache/InMemoryCache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ The first parameter of a modifier function is the current value of the field bei
| `canRead` | `CanReadFunction` | Returns `true` for non-normalized `StoreObjects` and non-dangling `Reference`s, indicating that `readField(name, objOrRef)` has a chance of working. Useful for filtering out dangling references from lists. |
| `isReference` | `boolean` | Utility to check if an object is a `{ __ref }` object. |
| `DELETE` | `any` | Sentinel object that can be returned from a modifier function to delete the field being modified. |
| `INVALIDATE` | `any` | Sentinel object that can be returned from a modifier function to invalidate the field, causing affected queries to rerun, without changing or deleting the field value. |

`Modifier` functions should return the value that is to be written into the cache for the field being modified, or a `DELETE` sentinel to remove the field.

Expand Down
164 changes: 164 additions & 0 deletions docs/source/api/link/persisted-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
title: Persisted Queries Link
description: Replace full queries with generated ID's to reduce bandwidth.
---

## Problem to solve

Unlike REST APIs that use a fixed URL to load data, GraphQL provides a rich query language that can be used to express the shape of application data requirements. This is a marvelous advancement in technology, but it comes at a cost: GraphQL query strings are often much longer than REST URLS — in some cases by many kilobytes.

In practice we've seen GraphQL query sizes ranging well above 10 KB *just for the query text*. This is significant overhead when compared with a simple URL of 50-100 characters. When paired with the fact that the uplink speed from the client is typically the most bandwidth-constrained part of the chain, large queries can become bottlenecks for client performance.

Automatic Persisted Queries solves this problem by sending a generated ID instead of the query text as the request.

For more information about this solution, read [this article announcing Automatic Persisted Queries](https://www.apollographql.com/blog/improve-graphql-performance-with-automatic-persisted-queries-c31d27b8e6ea/).

## How it works

1. When the client makes a query, it will optimistically send a short (64-byte) cryptographic hash instead of the full query text.
2. If the backend recognizes the hash, it will retrieve the full text of the query and execute it.
3. If the backend doesn't recognize the hash, it will ask the client to send the hash and the query text so it can store them mapped together for future lookups. During this request, the backend will also fulfill the data request.

This library is a client implementation for use with Apollo Client by using custom Apollo Link.

## Installation

This link is included in the `@apollo/client` package:

`npm install @apollo/client`

If you do not already have a SHA-256 based hashing function available in your application, you will need to install one separately. For example:

`npm install crypto-hash`

This link doesn't include a SHA-256 hash function by default, to avoid forcing one as a dependency. Developers should pick the most appropriate SHA-256 function (sync or async) for their needs/environment.

## Usage

The persisted query link requires using the `HttpLink`. The easiest way to use them together is to `concat` them into a single link.

```js
import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { sha256 } from 'crypto-hash';

const httpLink = new HttpLink({ uri: "/graphql" });
const persistedQueriesLink = createPersistedQueryLink({ sha256 });
const client = new ApolloClient({
cache: new InMemoryCache(),
link: persistedQueriesLink.concat(httpLink);
});
```

Thats it! Now your client will start sending query signatures instead of the full text resulting in improved network performance!

#### Options

The `createPersistedQueryLink` function takes a configuration object:

- `sha256`: a SHA-256 hashing function. Can be sync or async. Providing a SHA-256 hashing function is required, unless you're defining a fully custom hashing approach via `generateHash`.
- `generateHash`: an optional function that takes the query document and returns the hash. If provided this custom function will override the default hashing approach that uses the supplied `sha256` function. If not provided, the persisted queries link will use a fallback hashing approach leveraging the `sha256` function.
- `useGETForHashedQueries`: set to `true` to use the HTTP `GET` method when sending the hashed version of queries (but not for mutations). `GET` requests are not compatible with `@apollo/client/link/batch-http`.
> If you want to use `GET` for non-mutation queries whether or not they are hashed, pass `useGETForQueries: true` option to `HttpLink` instead. If you want to use `GET` for all requests, pass `fetchOptions: {method: 'GET'}` to `HttpLink`.
- `disable`: a function which takes an `ErrorResponse` (see below) and returns a boolean to disable any future persisted queries for that session. This defaults to disabling on `PersistedQueryNotSupported` or a 400 or 500 http error.

**ErrorResponse**

The argument that the optional `disable` function is given is an object with the following keys:

- `operation`: The Operation that encountered an error (contains `query`, `variables`, `operationName`, and `context`).
- `response`: The Execution of the response (contains `data` and `errors` as well `extensions` if sent from the server).
- `graphQLErrors`: An array of errors from the GraphQL endpoint.
- `networkError`: Any error during the link execution or server response.

*Note*: `networkError` is the value from the downlink's `error` callback. In most cases, `graphQLErrors` is the `errors` field of the result from the last `next` call. A `networkError` can contain additional fields, such as a GraphQL object in the case of a failing HTTP status code from `@apollo/link/http`. In this situation, `graphQLErrors` is an alias for `networkError.result.errors` if the property exists.

## Apollo Studio

Apollo Studio supports receiving and fulfilling Automatic Persisted Queries. Simply adding this link into your client app will improve your network response times when using Apollo Studio.

### Protocol

Automatic Persisted Queries are made up of three parts: the query signature, error responses, and the negotiation protocol.

**Query Signature**

The query signature for Automatic Persisted Queries is sent through the `extensions` field of a request from the client. This is a transport independent way to send extra information along with the operation.

```js
{
operationName: 'MyQuery',
variables: null,
extensions: {
persistedQuery: {
version: 1,
sha256Hash: hashOfQuery
}
}
}
```

When sending an Automatic Persisted Query, the client omits the `query` field normally present, and instead sends an extension field with a `persistedQuery` object as shown above. The hash algorithm defaults to a `sha256` hash of the query string.

If the client needs to register the hash, the query signature will be the same but include the full query text like so:

```js
{
operationName: 'MyQuery',
variables: null,
query: `query MyQuery { id }`,
extensions: {
persistedQuery: {
version: 1,
sha256Hash: hashOfQuery
}
}
}
```

This should only happen once across all clients when a new query is introduced into your application.

**Error Responses**

When the initial query signature is received by a backend, if it is unable to find the hash previously stored, it will send back the following response signature:

```js
{
errors: [
{ message: 'PersistedQueryNotFound' }
]
}
```

If the backend doesn't support Automatic Persisted Queries, or does not want to support it for that particular client, it can send back the following which will tell the client to stop trying to send hashes:

```
{
errors: [
{ message: 'PersistedQueryNotSupported' }
]
}
```

**Negotiation Protocol**

In order to support Automatic Persisted Queries, the client and server must follow the negotiation steps as outlined here:

*Happy Path*
1. Client sends query signature with no `query` field
2. Server looks up query based on hash, if found, it resolves the data
3. Client receives data and completes request

*Missing hash path*
1. Client sends query signature with no `query` field
2. Server looks up query based on hash, none is found
3. Server responds with NotFound error response
4. Client sends both hash and query string to Server
5. Server fulfills response and saves query string + hash for future lookup
6. Client receives data and completes request

### Build time generation

If you want to avoid hashing in the browser, you can use a build script to include the hash as part of the request, then pass a function to retrieve that hash when the operation is run. This works well with projects like [GraphQL Persisted Document Loader](https://github.com/leoasis/graphql-persisted-document-loader) which uses webpack to generate hashes at build time.

If you use the above loader, you can pass `{ generateHash: ({ documentId }) => documentId }` to the `createPersistedQueryLink` call.
30 changes: 30 additions & 0 deletions docs/source/caching/cache-interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,36 @@ cache.modify({
});
```

### Example: Invalidating fields within a cached object

Normally, changing or deleting a field's value also _invalidates_ the field, causing watched queries to be reread if they previously consumed the field.

Using `cache.modify`, it's also possible to invalidate the field without changing or deleting its value, by returning the `INVALIDATE` sentinel:

```js
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { INVALIDATE }) {
return INVALIDATE;
},
},
});
```

If you need to invalidate all fields within the given object, you can pass a modifier function as the value of the `fields` option:

```js
cache.modify({
id: cache.identify(myPost),
fields(fieldValue, details) {
return details.INVALIDATE;
},
});
```

When using this form of `cache.modify`, you can determine the individual field names using `details.fieldName`. This technique works for any modifier function, not just those that return `INVALIDATE`.

## Obtaining an object's custom ID

If a type in your cache uses a [custom identifier](./cache-configuration/#customizing-identifier-generation-by-type) (or even if it doesn't), you can use the `cache.identify` method to obtain the identifier for an object of that type. This method takes an object and computes its ID based on both its `__typename` and its identifier field(s). This means you don't have to keep track of which fields make up each type's identifier.
Expand Down
34 changes: 28 additions & 6 deletions docs/source/local-state/managing-state-with-field-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,14 @@ This `read` function returns the value of our reactive variable whenever `cartIt

Now, let's create a button component that enables the user to add a product to their cart:

```jsx{8}:title=AddToCartButton.js
```jsx{7}:title=AddToCartButton.js
import { cartItemsVar } from './cache';
// ... other imports

export function AddToCartButton({ productId }) {
const cartItems = cartItemsVar();
return (
<div class="add-to-cart-button">
<Button onClick={() => cartItemsVar([...cartItems, productId])}>
<Button onClick={() => cartItemsVar([...cartItemsVar(), productId])}>
Add to Cart
</Button>
</div>
Expand All @@ -167,8 +166,6 @@ On click, this button updates the value of `cartItemsVar` to append the button's

Here's a `Cart` component that uses the `GET_CART_ITEMS` query and therefore refreshes automatically whenever the value of `cartItemsVar` changes:

<ExpansionPanel title="Expand example">

```jsx:title=Cart.js
export const GET_CART_ITEMS = gql`
query GetCartItems {
Expand Down Expand Up @@ -199,7 +196,32 @@ export function Cart() {
}
```

</ExpansionPanel>
Alternatively, you can read directly from a reactive variable using the `useReactiveVar` hook introduced in Apollo Client 3.2.0:

```jsx:title=Cart.js
import { useReactiveVar } from '@apollo/client';

export function Cart() {
const cartItems = useReactiveVar(cartItemsVar);

return (
<div class="cart">
<Header>My Cart</Header>
{cartItems.length === 0 ? (
<p>No items in your cart</p>
) : (
<Fragment>
{cartItems.map(productId => (
<CartItem key={productId} />
))}
</Fragment>
)}
</div>
);
}
```

As in the earlier `useQuery` example, whenever the `cartItemsVar` variable is updated, any currently-mounted `Cart` components will rerender. Calling `cartItemsVar()` without `useReactiveVar` will not capture this dependency, so future variable updates will not rerender the component. Both of these approaches are useful in different situations.

### Storing local state in the cache

Expand Down
Loading

0 comments on commit a975320

Please sign in to comment.