From 5f7b723e5e2f32ca75c3dfdd3cb439b0b12b06fe Mon Sep 17 00:00:00 2001 From: James Baxley Date: Tue, 30 Jan 2018 16:31:44 -0500 Subject: [PATCH] support no-cache requests --- docs/source/basics/caching.md | 3 + packages/apollo-client/CHANGELOG.md | 1 + .../apollo-client/src/core/QueryManager.ts | 60 ++-- .../src/core/__tests__/fetchPolicies.ts | 324 ++++++++++++++++++ .../src/core/watchQueryOptions.ts | 9 +- 5 files changed, 374 insertions(+), 23 deletions(-) create mode 100644 packages/apollo-client/src/core/__tests__/fetchPolicies.ts diff --git a/docs/source/basics/caching.md b/docs/source/basics/caching.md index 5d5be05354b..6467e7c5ba5 100644 --- a/docs/source/basics/caching.md +++ b/docs/source/basics/caching.md @@ -226,6 +226,9 @@ client.writeQuery({ Here are some common situations where you would need to access the cache directly. If you're manipulating the cache in an interesting way and would like your example to be featured, please send in a pull request! +

Bypassing the cache

+Sometimes it makes sense to not use the cache for a specfic operation. This can be done using either the `network-only` or `no-cache` fetchPolicy. The key difference between these two policies is that `network-only` still saves the response to the cache for later use, bypassing the reading and forcing a network request. The `no-cache` policy does not read, nor does it write to the cache with the response. This may be useful for sensitive data like passwords that you don't want to keep in the cache. +

Server side rendering

First, you will need to initialize an `InMemoryCache` on the server and create an instance of `ApolloClient`. In the initial serialized HTML payload from the server, you should include a script tag that extracts the data from the cache. (The `.replace()` is necessary to prevent script injection attacks) diff --git a/packages/apollo-client/CHANGELOG.md b/packages/apollo-client/CHANGELOG.md index 79ec862ab3d..f805ae7abe9 100644 --- a/packages/apollo-client/CHANGELOG.md +++ b/packages/apollo-client/CHANGELOG.md @@ -2,6 +2,7 @@ # Change log ### vNEXT +- Add new fetchPolicy called 'no-cache' to bypass reading from or saving to the cache when making a query ### 2.2.1 - Allow optional parameter to include queries in standby mode when refetching observed queries [PR#2804](https://github.com/apollographql/apollo-client/pull/2804) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 51d72bdce75..11c875f8cd0 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -123,6 +123,7 @@ export class QueryManager { refetchQueries = [], update: updateWithProxyFn, errorPolicy = 'none', + fetchPolicy, context = {}, }: MutationOptions): Promise> { if (!mutation) { @@ -131,6 +132,12 @@ export class QueryManager { ); } + if (fetchPolicy && fetchPolicy !== 'no-cache') { + throw new Error( + "fetchPolicy for mutations currently only supports the 'no-cache' policy", + ); + } + const mutationId = this.generateQueryId(); const cache = this.dataStore.getCache(); (mutation = cache.transformDocument(mutation)), @@ -195,14 +202,16 @@ export class QueryManager { this.mutationStore.markMutationResult(mutationId); - this.dataStore.markMutationResult({ - mutationId, - result, - document: mutation, - variables: variables || {}, - updateQueries: generateUpdateQueriesInfo(), - update: updateWithProxyFn, - }); + if (fetchPolicy !== 'no-cache') { + this.dataStore.markMutationResult({ + mutationId, + result, + document: mutation, + variables: variables || {}, + updateQueries: generateUpdateQueriesInfo(), + update: updateWithProxyFn, + }); + } storeResult = result as FetchResult; }, error: (err: Error) => { @@ -283,12 +292,17 @@ export class QueryManager { const query = cache.transformDocument(options.query); let storeResult: any; - let needToFetch: boolean = fetchPolicy === 'network-only'; + let needToFetch: boolean = + fetchPolicy === 'network-only' || fetchPolicy === 'no-cache'; // If this is not a force fetch, we want to diff the query against the // store before we fetch it from the network interface. // TODO we hit the cache even if the policy is network-first. This could be unnecessary if the network is up. - if (fetchType !== FetchType.refetch && fetchPolicy !== 'network-only') { + if ( + fetchType !== FetchType.refetch && + fetchPolicy !== 'network-only' && + fetchPolicy !== 'no-cache' + ) { const { complete, result } = this.dataStore.getCache().diff({ query, variables, @@ -1025,7 +1039,7 @@ export class QueryManager { options: WatchQueryOptions; fetchMoreForQueryId?: string; }): Promise { - const { variables, context, errorPolicy = 'none' } = options; + const { variables, context, errorPolicy = 'none', fetchPolicy } = options; const operation = this.buildOperationForLink(document, variables, { ...context, // TODO: Should this be included for all entry points via @@ -1042,17 +1056,19 @@ export class QueryManager { // default the lastRequestId to 1 const { lastRequestId } = this.getQuery(queryId); if (requestId >= (lastRequestId || 1)) { - try { - this.dataStore.markQueryResult( - result, - document, - variables, - fetchMoreForQueryId, - errorPolicy === 'ignore', - ); - } catch (e) { - reject(e); - return; + if (fetchPolicy !== 'no-cache') { + try { + this.dataStore.markQueryResult( + result, + document, + variables, + fetchMoreForQueryId, + errorPolicy === 'ignore', + ); + } catch (e) { + reject(e); + return; + } } this.queryStore.markQueryResult( diff --git a/packages/apollo-client/src/core/__tests__/fetchPolicies.ts b/packages/apollo-client/src/core/__tests__/fetchPolicies.ts new file mode 100644 index 00000000000..adb9dc385a6 --- /dev/null +++ b/packages/apollo-client/src/core/__tests__/fetchPolicies.ts @@ -0,0 +1,324 @@ +import { cloneDeep, assign } from 'lodash'; +import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql'; +import gql from 'graphql-tag'; +import { print } from 'graphql/language/printer'; +import { ApolloLink, Observable } from 'apollo-link'; +import { + InMemoryCache, + IntrospectionFragmentMatcher, + FragmentMatcherInterface, +} from 'apollo-cache-inmemory'; + +import { QueryManager } from '../QueryManager'; +import { WatchQueryOptions } from '../watchQueryOptions'; + +import { ApolloError } from '../../errors/ApolloError'; + +import ApolloClient, { printAST } from '../..'; + +import subscribeAndCount from '../../util/subscribeAndCount'; +import { withWarning } from '../../util/wrap'; + +import { mockSingleLink } from '../../__mocks__/mockLinks'; + +const query = gql` + query { + author { + __typename + id + firstName + lastName + } + } +`; + +const result = { + author: { + __typename: 'Author', + id: 1, + firstName: 'John', + lastName: 'Smith', + }, +}; + +const mutation = gql` + mutation updateName($id: ID!, $firstName: String!) { + updateName(id: $id, firstName: $firstName) { + __typename + id + firstName + } + } +`; + +const variables = { + id: 1, + firstName: 'James', +}; + +const mutationResult = { + updateName: { + id: 1, + __typename: 'Author', + firstName: 'James', + }, +}; + +const merged = { author: { ...result.author, firstName: 'James' } }; + +const createLink = () => + mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query }, + result: { data: result }, + }, + ); + +const createFailureLink = () => + mockSingleLink( + { + request: { query }, + error: new Error('query failed'), + }, + { + request: { query }, + result: { data: result }, + }, + ); + +const createMutationLink = () => + // fetch the data + mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + // update the data + { + request: { query: mutation, variables }, + result: { data: mutationResult }, + }, + // get the new results + { + request: { query }, + result: { data: merged }, + }, + ); + +describe('network-only', () => { + it('requests from the network even if already in cache', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + return client.query({ query }).then(() => + client + .query({ fetchPolicy: 'network-only', query }) + .then(actualResult => { + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }), + ); + }); + it('saves data to the cache on success', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + return client.query({ query, fetchPolicy: 'network-only' }).then(() => + client.query({ query }).then(actualResult => { + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); + }), + ); + }); + it('does not save data to the cache on failure', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createFailureLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + let didFail = false; + return client + .query({ query, fetchPolicy: 'network-only' }) + .catch(e => { + expect(e.message).toMatch('query failed'); + didFail = true; + }) + .then(() => + client.query({ query }).then(actualResult => { + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); + }), + ); + }); + + it('updates the cache on a mutation', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createMutationLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + return client + .query({ query }) + .then(() => + // XXX currently only no-cache is supported as a fetchPolicy + // this mainly serves to ensure the cache is updated correctly + client.mutate({ mutation, variables }), + ) + .then(() => { + return client.query({ query }).then(actualResult => { + expect(actualResult.data).toEqual(merged); + }); + }); + }); +}); +describe('no-cache', () => { + it('requests from the network even if already in cache', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + return client.query({ query }).then(() => + client.query({ fetchPolicy: 'no-cache', query }).then(actualResult => { + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }), + ); + }); + it('saves data to the cache on success', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + return client.query({ query, fetchPolicy: 'no-cache' }).then(() => + client.query({ query }).then(actualResult => { + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }), + ); + }); + + it('does not save data to the cache on failure', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createFailureLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + let didFail = false; + return client + .query({ query, fetchPolicy: 'no-cache' }) + .catch(e => { + expect(e.message).toMatch('query failed'); + didFail = true; + }) + .then(() => + client.query({ query }).then(actualResult => { + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); + }), + ); + }); + it('does not update the cache on a mutation', () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map(result => { + called++; + return result; + }); + }); + + const client = new ApolloClient({ + link: inspector.concat(createMutationLink()), + cache: new InMemoryCache({ addTypename: false }), + }); + + return client + .query({ query }) + .then(() => + client.mutate({ mutation, variables, fetchPolicy: 'no-cache' }), + ) + .then(() => { + return client.query({ query }).then(actualResult => { + expect(actualResult.data).toEqual(result); + }); + }); + }); +}); diff --git a/packages/apollo-client/src/core/watchQueryOptions.ts b/packages/apollo-client/src/core/watchQueryOptions.ts index f9ac567d28d..c666b5da394 100644 --- a/packages/apollo-client/src/core/watchQueryOptions.ts +++ b/packages/apollo-client/src/core/watchQueryOptions.ts @@ -11,7 +11,8 @@ import { PureQueryOptions } from './types'; * - cache-first (default): return result from cache. Only fetch from network if cached result is not available. * - cache-and-network: return result from cache first (if it exists), then return network result once it's available. * - cache-only: return result from cache if available, fail otherwise. - * - network-only: return result from network, fail if network call doesn't succeed. + * - no-cache: return resutl from network, fail if network call doesn't succeed, don't save to cache + * - network-only: return result from network, fail if network call doesn't succeed, save to cache * - standby: only for queries that aren't actively watched, but should be available for refetch and updateQueries. */ @@ -20,6 +21,7 @@ export type FetchPolicy = | 'cache-and-network' | 'network-only' | 'cache-only' + | 'no-cache' | 'standby'; /** @@ -200,6 +202,11 @@ export interface MutationOptions * Context to be passed to link execution chain */ context?: any; + + /** + * Specifies the {@link FetchPolicy} to be used for this query + */ + fetchPolicy?: FetchPolicy; } // Add a level of indirection for `typedoc`.