From 9281bb81875612cf23fb10e17ba2eef5b1753ef5 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 12 Jun 2020 15:25:46 -0400 Subject: [PATCH 1/7] Implement an offsetLimitPagination field policy function. I made this helper function an export of the the @apollo/client/utilities entry point so it will not be included in your application bundle unless you import and use it. We hope you will find it useful, but we also encourage using it just for guidance/inspiration when writing your own custom field policy helper functions. For example, you might want to change args.offset to args.start, or args.limit to args.max. --- src/__tests__/fetchMore.ts | 1 + src/utilities/index.ts | 4 ++++ src/utilities/policies/pagination.ts | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 src/utilities/policies/pagination.ts diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 67ed6796f96..814358ad1ae 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -5,6 +5,7 @@ import { mockSingleLink } from '../utilities/testing/mocking/mockLink'; import { InMemoryCache } from '../cache/inmemory/inMemoryCache'; import { ApolloClient, NetworkStatus, ObservableQuery } from '../'; import { itAsync } from '../utilities/testing/itAsync'; +import { offsetLimitPagination } from '../utilities'; describe('updateQuery on a simple query', () => { const query = gql` diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 320c73c5ee5..cd749e8ed4b 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -61,3 +61,7 @@ export { removeFragmentSpreadFromDocument, removeClientSetsFromDocument, } from './graphql/transform'; + +export { + offsetLimitPagination, +} from './policies/pagination'; diff --git a/src/utilities/policies/pagination.ts b/src/utilities/policies/pagination.ts new file mode 100644 index 00000000000..30f54413dba --- /dev/null +++ b/src/utilities/policies/pagination.ts @@ -0,0 +1,24 @@ +import { FieldPolicy, Reference } from '../../cache'; + +type KeyArgs = FieldPolicy["keyArgs"]; + +// A basic field policy that uses options.args.{offset,limit} to splice +// the incoming data into the existing array. If your arguments are called +// something different (like args.{start,count}), feel free to copy/paste +// this implementation and make the appropriate changes. +export function offsetLimitPagination( + keyArgs: KeyArgs = false, +): FieldPolicy { + return { + keyArgs, + merge(existing, incoming, { args }) { + const merged = existing ? existing.slice(0) : []; + const start = args ? args.offset : merged.length; + const end = start + incoming.length; + for (let i = start; i < end; ++i) { + merged[i] = incoming[i - start]; + } + return merged; + }, + }; +} From 616b5182050661f9eba4301330ca2ad1a6d8ce8f Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 17 Jun 2020 18:29:24 -0400 Subject: [PATCH 2/7] Implement a naive concatPagination field policy function. This pagination helper policy is flawed because it does not consider any field arguments, but it seems useful for AC3 migration purposes, because it does exactly what a typical fetchMore updateQuery function would do, just a little more reliably. --- src/cache/inmemory/policies.ts | 2 +- src/utilities/index.ts | 1 + src/utilities/policies/pagination.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index f0c98ee069b..23e5508e8ea 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -186,7 +186,7 @@ export type FieldMergeFunction = ( // reasons discussed in FieldReadFunction above. incoming: SafeReadonly, options: FieldFunctionOptions, -) => TExisting; +) => SafeReadonly; export const defaultDataIdFromObject = ( { __typename, id, _id }: Readonly, diff --git a/src/utilities/index.ts b/src/utilities/index.ts index cd749e8ed4b..6c905bbb215 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -63,5 +63,6 @@ export { } from './graphql/transform'; export { + concatPagination, offsetLimitPagination, } from './policies/pagination'; diff --git a/src/utilities/policies/pagination.ts b/src/utilities/policies/pagination.ts index 30f54413dba..e30d617c0c4 100644 --- a/src/utilities/policies/pagination.ts +++ b/src/utilities/policies/pagination.ts @@ -2,6 +2,22 @@ import { FieldPolicy, Reference } from '../../cache'; type KeyArgs = FieldPolicy["keyArgs"]; +// A very basic pagination field policy that always concatenates new +// results onto the existing array, without examining options.args. +export function concatPagination( + keyArgs: KeyArgs = false, +): FieldPolicy { + return { + keyArgs, + merge(existing, incoming) { + return existing ? [ + ...existing, + ...incoming, + ] : incoming; + }, + }; +} + // A basic field policy that uses options.args.{offset,limit} to splice // the incoming data into the existing array. If your arguments are called // something different (like args.{start,count}), feel free to copy/paste From d261c4fef8b1062a3b74cd5c4d7ccc1a7ce9b146 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 10 Jun 2020 18:21:29 -0400 Subject: [PATCH 3/7] Make fetchMore pass new variables to merge functions. This commit also deprecates the updateQuery function, while preserving its existing behavior for backwards compatibility. If you're using a field policy, you shouldn't need an updateQuery function. Fixes #5951. --- src/core/ObservableQuery.ts | 46 +++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index c78639c201a..7f6d6e6a7ca 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -54,6 +54,8 @@ export const hasError = ( (policy === 'none' && isNonEmptyArray(storeValue.graphQLErrors)) ); +let warnedAboutUpdateQuery = false; + export class ObservableQuery< TData = any, TVariables = OperationVariables @@ -309,15 +311,45 @@ export class ObservableQuery< combinedOptions, NetworkStatus.fetchMore, ).then(fetchMoreResult => { - this.updateQuery((previousResult: any) => { - const data = fetchMoreResult.data as TData; - const { updateQuery } = fetchMoreOptions; - return updateQuery ? updateQuery(previousResult, { + const data = fetchMoreResult.data as TData; + const { updateQuery } = fetchMoreOptions; + + if (updateQuery) { + if (process.env.NODE_ENV !== "production" && + !warnedAboutUpdateQuery) { + invariant.warn( +`The updateQuery callback for fetchMore is deprecated, and will be removed +in the next major version of Apollo Client. + +Please convert updateQuery functions to field policies with appropriate +read and merge functions, or use/adapt a helper function (such as +concatPagination, offsetLimitPagination, or relayStylePagination) from +@apollo/client/utilities. + +The field policy system handles pagination more effectively than a +hand-written updateQuery function, and you only need to define the policy +once, rather than every time you call fetchMore.`); + warnedAboutUpdateQuery = true; + } + this.updateQuery(previous => updateQuery(previous, { fetchMoreResult: data, variables: combinedOptions.variables as TVariables, - }) : data; - }); + })); + } else { + // If we're using a field policy instead of updateQuery, the only + // thing we need to do is write the new data to the cache using + // combinedOptions.variables (instead of this.variables, which is + // what this.updateQuery uses, because it works by abusing the + // original field value, keyed by the original variables). + this.queryManager.cache.writeQuery({ + query: combinedOptions.query, + variables: combinedOptions.variables, + data, + }); + } + return fetchMoreResult as ApolloQueryResult; + }).finally(() => { this.queryManager.stopQuery(qid); this.reobserve(); @@ -423,7 +455,7 @@ export class ObservableQuery< return Promise.resolve(); } - let { fetchPolicy } = this.options; + let { fetchPolicy = 'cache-first' } = this.options; if (fetchPolicy !== 'cache-first' && fetchPolicy !== 'no-cache' && fetchPolicy !== 'network-only') { From 3329497d9be696db3dae715caa96e0970b80799a Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 18 Jun 2020 15:24:00 -0400 Subject: [PATCH 4/7] Test that fetchMore passes new variables to merge functions. --- src/__tests__/fetchMore.ts | 192 ++++++++++++++++++++++++++++++++++++- 1 file changed, 190 insertions(+), 2 deletions(-) diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 814358ad1ae..595c68385dc 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -2,6 +2,7 @@ import { assign, cloneDeep } from 'lodash'; import gql from 'graphql-tag'; import { mockSingleLink } from '../utilities/testing/mocking/mockLink'; +import subscribeAndCount from '../utilities/testing/subscribeAndCount'; import { InMemoryCache } from '../cache/inmemory/inMemoryCache'; import { ApolloClient, NetworkStatus, ObservableQuery } from '../'; import { itAsync } from '../utilities/testing/itAsync'; @@ -211,7 +212,19 @@ describe('fetchMore on an observable query', () => { const client = new ApolloClient({ link, - cache: new InMemoryCache(), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + entry: { + merge(_, incoming) { + return incoming; + }, + }, + }, + }, + }, + }), }); return client.watchQuery({ @@ -298,6 +311,169 @@ describe('fetchMore on an observable query', () => { }).then(resolve, reject); }); + itAsync('fetchMore passes new args to field merge function', (resolve, reject) => { + const mergeArgsHistory: (Record | null)[] = []; + const groceriesFieldPolicy = offsetLimitPagination(); + const { merge } = groceriesFieldPolicy; + groceriesFieldPolicy.merge = function (existing, incoming, options) { + mergeArgsHistory.push(options.args); + return merge!.call(this, existing, incoming, options); + }; + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + groceries: groceriesFieldPolicy, + }, + }, + }, + }); + + const query = gql` + query GroceryList($offset: Int!, $limit: Int!) { + groceries(offset: $offset, limit: $limit) { + id + item + found + } + } + `; + + const initialVars = { + offset: 0, + limit: 2, + }; + + const initialGroceries = [ + { + __typename: "GroceryItem", + id: 1, + item: "organic whole milk", + found: false, + }, + { + __typename: "GroceryItem", + id: 2, + item: "beer that we both like", + found: false, + }, + ]; + + const additionalVars = { + offset: 2, + limit: 3, + }; + + const additionalGroceries = [ + { + __typename: "GroceryItem", + id: 3, + item: "gluten-free pasta", + found: false, + }, + { + __typename: "GroceryItem", + id: 4, + item: "goat cheese", + found: false, + }, + { + __typename: "GroceryItem", + id: 5, + item: "paper towels", + found: false, + }, + ]; + + const finalGroceries = [ + ...initialGroceries, + ...additionalGroceries, + ]; + + const client = new ApolloClient({ + cache, + link: mockSingleLink({ + request: { + query, + variables: initialVars, + }, + result: { + data: { + groceries: initialGroceries, + }, + }, + }, { + request: { + query, + variables: additionalVars, + }, + result: { + data: { + groceries: additionalGroceries, + }, + }, + }).setOnError(reject), + }); + + const observable = client.watchQuery({ + query, + variables: initialVars, + }); + + subscribeAndCount(reject, observable, (count, result) => { + if (count === 1) { + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + groceries: initialGroceries, + }, + }); + + expect(mergeArgsHistory).toEqual([ + { offset: 0, limit: 2 }, + ]); + + observable.fetchMore({ + variables: { + offset: 2, + limit: 3, + }, + }).then(result => { + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + groceries: additionalGroceries, + }, + }); + + expect(observable.options.fetchPolicy).toBeUndefined(); + }); + + } else if (count === 2) { + // This result comes entirely from the cache, without updating the + // original variables for the ObservableQuery, because the + // offsetLimitPagination field policy has keyArgs:false. + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + groceries: finalGroceries, + }, + }); + + expect(mergeArgsHistory).toEqual([ + { offset: 0, limit: 2 }, + { offset: 2, limit: 3 }, + ]); + + resolve(); + } + }); + }); + itAsync('fetching more with a different query', (resolve, reject) => { const observable = setup(reject, { request: { @@ -482,7 +658,19 @@ describe('fetchMore on an observable query with connection', () => { const client = new ApolloClient({ link, - cache: new InMemoryCache(), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + entry: { + merge(_, incoming) { + return incoming; + }, + }, + }, + }, + }, + }), }); return client.watchQuery({ From f3676b66595064c6c07c8df5f27ad6b63b1e5461 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 22 Jun 2020 11:17:29 -0400 Subject: [PATCH 5/7] Run fetchMore tests using both updateQuery and field policies. Once we remove the deprecated updateQuery function, we can remove those tests, but it seems important to test both styles for now. --- src/__tests__/fetchMore.ts | 674 ++++++++++++++------ src/react/hooks/__tests__/useQuery.test.tsx | 293 ++++++--- 2 files changed, 682 insertions(+), 285 deletions(-) diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 595c68385dc..17aeecd18df 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -3,10 +3,10 @@ import gql from 'graphql-tag'; import { mockSingleLink } from '../utilities/testing/mocking/mockLink'; import subscribeAndCount from '../utilities/testing/subscribeAndCount'; -import { InMemoryCache } from '../cache/inmemory/inMemoryCache'; +import { InMemoryCache, InMemoryCacheConfig } from '../cache/inmemory/inMemoryCache'; import { ApolloClient, NetworkStatus, ObservableQuery } from '../'; import { itAsync } from '../utilities/testing/itAsync'; -import { offsetLimitPagination } from '../utilities'; +import { offsetLimitPagination, concatPagination } from '../utilities'; describe('updateQuery on a simple query', () => { const query = gql` @@ -233,82 +233,181 @@ describe('fetchMore on an observable query', () => { }); } - itAsync('triggers new result withAsync new variables', (resolve, reject) => { - const observable = setup(reject, { - request: { - query, - variables: variablesMore, - }, - result: resultMore, + function setupWithCacheConfig( + reject: (reason: any) => any, + cacheConfig: InMemoryCacheConfig, + ...mockedResponses: any[] + ) { + const client = new ApolloClient({ + link: mockSingleLink({ + request: { + query, + variables, + }, + result, + }, ...mockedResponses).setOnError(reject), + cache: new InMemoryCache(cacheConfig), }); - let latestResult: any; - observable.subscribe({ - next(result: any) { - latestResult = result; - }, + return client.watchQuery({ + query, + variables, }); + } - return observable.fetchMore({ - // Rely on the fact that the original variables had limit: 10 - variables: { start: 10 }, - updateQuery: (prev, options) => { - expect(options.variables).toEqual(variablesMore); + describe('triggers new result with async new variables', () => { + itAsync('updateQuery', (resolve, reject) => { + const observable = setup(reject, { + request: { + query, + variables: variablesMore, + }, + result: resultMore, + }); - const state = cloneDeep(prev) as any; - state.entry.comments = [ - ...state.entry.comments, - ...(options.fetchMoreResult as any).entry.comments, - ]; - return state; - }, - }).then(data => { - // This is the server result - expect(data.data.entry.comments).toHaveLength(10); - expect(data.loading).toBe(false); - const comments = latestResult.data.entry.comments; - expect(comments).toHaveLength(20); - for (let i = 1; i <= 20; i++) { - expect(comments[i - 1].text).toEqual(`comment ${i}`); - } - }).then(resolve, reject); - }); + let latestResult: any; + observable.subscribe({ + next(result: any) { + latestResult = result; + }, + }); + + return observable.fetchMore({ + // Rely on the fact that the original variables had limit: 10 + variables: { start: 10 }, + updateQuery: (prev, options) => { + expect(options.variables).toEqual(variablesMore); + + const state = cloneDeep(prev) as any; + state.entry.comments = [ + ...state.entry.comments, + ...(options.fetchMoreResult as any).entry.comments, + ]; + return state; + }, + }).then(data => { + // This is the server result + expect(data.data.entry.comments).toHaveLength(10); + expect(data.loading).toBe(false); + const comments = latestResult.data.entry.comments; + expect(comments).toHaveLength(20); + for (let i = 1; i <= 20; i++) { + expect(comments[i - 1].text).toEqual(`comment ${i}`); + } + }).then(resolve, reject); + }); - itAsync('basic fetchMore results merging', (resolve, reject) => { - const observable = setup(reject, { - request: { - query, - variables: variablesMore, - }, - result: resultMore, + itAsync('field policy', (resolve, reject) => { + const observable = setupWithCacheConfig(reject, { + typePolicies: { + Entry: { + fields: { + comments: concatPagination(), + }, + }, + }, + }, { + request: { query, variables: variablesMore }, + result: resultMore, + }); + + let latestResult: any; + observable.subscribe({ + next(result: any) { + latestResult = result; + }, + }); + + return observable.fetchMore({ + // Rely on the fact that the original variables had limit: 10 + variables: { start: 10 }, + }).then(data => { + // This is the server result + expect(data.data.entry.comments).toHaveLength(10); + expect(data.loading).toBe(false); + const comments = latestResult.data.entry.comments; + expect(comments).toHaveLength(20); + for (let i = 1; i <= 20; i++) { + expect(comments[i - 1].text).toEqual(`comment ${i}`); + } + }).then(resolve, reject); }); + }); - let latestResult: any; - observable.subscribe({ - next(result: any) { - latestResult = result; - }, + describe('basic fetchMore results merging', () => { + itAsync('updateQuery', (resolve, reject) => { + const observable = setup(reject, { + request: { + query, + variables: variablesMore, + }, + result: resultMore, + }); + + let latestResult: any; + observable.subscribe({ + next(result: any) { + latestResult = result; + }, + }); + + return observable.fetchMore({ + variables: { start: 10 }, // rely on the fact that the original variables had limit: 10 + updateQuery: (prev, options) => { + const state = cloneDeep(prev) as any; + state.entry.comments = [ + ...state.entry.comments, + ...(options.fetchMoreResult as any).entry.comments, + ]; + return state; + }, + }).then(data => { + expect(data.data.entry.comments).toHaveLength(10); // this is the server result + expect(data.loading).toBe(false); + const comments = latestResult.data.entry.comments; + expect(comments).toHaveLength(20); + for (let i = 1; i <= 20; i++) { + expect(comments[i - 1].text).toEqual(`comment ${i}`); + } + }).then(resolve, reject); }); - return observable.fetchMore({ - variables: { start: 10 }, // rely on the fact that the original variables had limit: 10 - updateQuery: (prev, options) => { - const state = cloneDeep(prev) as any; - state.entry.comments = [ - ...state.entry.comments, - ...(options.fetchMoreResult as any).entry.comments, - ]; - return state; - }, - }).then(data => { - expect(data.data.entry.comments).toHaveLength(10); // this is the server result - expect(data.loading).toBe(false); - const comments = latestResult.data.entry.comments; - expect(comments).toHaveLength(20); - for (let i = 1; i <= 20; i++) { - expect(comments[i - 1].text).toEqual(`comment ${i}`); - } - }).then(resolve, reject); + itAsync('field policy', (resolve, reject) => { + const observable = setupWithCacheConfig(reject, { + typePolicies: { + Entry: { + fields: { + comments: concatPagination(), + }, + }, + }, + }, { + request: { + query, + variables: variablesMore, + }, + result: resultMore, + }); + + let latestResult: any; + observable.subscribe({ + next(result: any) { + latestResult = result; + }, + }); + + return observable.fetchMore({ + variables: { start: 10 }, // rely on the fact that the original variables had limit: 10 + }).then(data => { + expect(data.data.entry.comments).toHaveLength(10); // this is the server result + expect(data.loading).toBe(false); + const comments = latestResult.data.entry.comments; + expect(comments).toHaveLength(20); + for (let i = 1; i <= 20; i++) { + expect(comments[i - 1].text).toEqual(`comment ${i}`); + } + }).then(resolve, reject); + }); }); itAsync('fetchMore passes new args to field merge function', (resolve, reject) => { @@ -513,57 +612,128 @@ describe('fetchMore on an observable query', () => { }).then(resolve, reject); }); - itAsync('will not get an error from `fetchMore` if thrown', (resolve, reject) => { - const fetchMoreError = new Error('Uh, oh!'); - const link = mockSingleLink({ - request: { query, variables }, - result, - delay: 5, - }, { - request: { query, variables: variablesMore }, - error: fetchMoreError, - delay: 5, - }).setOnError(reject); + describe('will not get an error from `fetchMore` if thrown', () => { + itAsync('updateQuery', (resolve, reject) => { + const fetchMoreError = new Error('Uh, oh!'); + const link = mockSingleLink({ + request: { query, variables }, + result, + delay: 5, + }, { + request: { query, variables: variablesMore }, + error: fetchMoreError, + delay: 5, + }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - const observable = client.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: true, + const observable = client.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: true, + }); + + let count = 0; + observable.subscribe({ + next: ({ data, networkStatus }) => { + switch (++count) { + case 1: + expect(networkStatus).toBe(NetworkStatus.ready); + expect((data as any).entry.comments.length).toBe(10); + observable.fetchMore({ + variables: { start: 10 }, + updateQuery: prev => { + reject(new Error("should not have called updateQuery")); + return prev; + }, + }).catch(e => { + expect(e.networkError).toBe(fetchMoreError); + resolve(); + }); + break; + } + }, + error: () => { + reject(new Error('`error` called when it wasn’t supposed to be.')); + }, + complete: () => { + reject( + new Error('`complete` called when it wasn’t supposed to be.'), + ); + }, + }); }); - let count = 0; - observable.subscribe({ - next: ({ data, networkStatus }) => { - switch (++count) { - case 1: - expect(networkStatus).toBe(NetworkStatus.ready); - expect((data as any).entry.comments.length).toBe(10); - observable.fetchMore({ - variables: { start: 10 }, - updateQuery: prev => { - reject(new Error("should not have called updateQuery")); - return prev; + itAsync('field policy', (resolve, reject) => { + const fetchMoreError = new Error('Uh, oh!'); + const link = mockSingleLink({ + request: { query, variables }, + result, + delay: 5, + }, { + request: { query, variables: variablesMore }, + error: fetchMoreError, + delay: 5, + }).setOnError(reject); + + let calledFetchMore = false; + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Entry: { + fields: { + comments: { + keyArgs: false, + merge(_, incoming) { + if (calledFetchMore) { + reject(new Error("should not have called merge")); + } + return incoming; + }, + }, + }, }, - }).catch(e => { - expect(e.networkError).toBe(fetchMoreError); - resolve(); - }); - break; - } - }, - error: () => { - reject(new Error('`error` called when it wasn’t supposed to be.')); - }, - complete: () => { - reject( - new Error('`complete` called when it wasn’t supposed to be.'), - ); - }, + }, + }), + }); + + const observable = client.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: true, + }); + + let count = 0; + observable.subscribe({ + next: ({ data, networkStatus }) => { + switch (++count) { + case 1: + expect(networkStatus).toBe(NetworkStatus.ready); + expect((data as any).entry.comments.length).toBe(10); + calledFetchMore = true; + observable.fetchMore({ + variables: { start: 10 }, + }).catch(e => { + expect(e.networkError).toBe(fetchMoreError); + resolve(); + }); + break; + } + }, + error: () => { + reject(new Error('`error` called when it wasn’t supposed to be.')); + }, + complete: () => { + reject( + new Error('`complete` called when it wasn’t supposed to be.'), + ); + }, + }); }); }); @@ -679,95 +849,213 @@ describe('fetchMore on an observable query with connection', () => { }); } - itAsync('fetchMore with connection results merging', (resolve, reject) => { - const observable = setup(reject, { - request: { - query: transformedQuery, - variables: variablesMore, - }, - result: resultMore, - }) + function setupWithCacheConfig( + reject: (reason: any) => any, + cacheConfig: InMemoryCacheConfig, + ...mockedResponses: any[] + ) { + const client = new ApolloClient({ + link: mockSingleLink({ + request: { + query: transformedQuery, + variables, + }, + result, + }, ...mockedResponses).setOnError(reject), + cache: new InMemoryCache(cacheConfig), + }); - let latestResult: any; - observable.subscribe({ - next(result: any) { - latestResult = result; - }, + return client.watchQuery({ + query, + variables, }); + } - return observable.fetchMore({ - variables: { start: 10 }, // rely on the fact that the original variables had limit: 10 - updateQuery: (prev, options) => { - const state = cloneDeep(prev) as any; - state.entry.comments = [ - ...state.entry.comments, - ...(options.fetchMoreResult as any).entry.comments, - ]; - return state; - }, - }).then(data => { - expect(data.data.entry.comments).toHaveLength(10); // this is the server result - expect(data.loading).toBe(false); - const comments = latestResult.data.entry.comments; - expect(comments).toHaveLength(20); - for (let i = 1; i <= 20; i++) { - expect(comments[i - 1].text).toBe(`comment ${i}`); - } - }).then(resolve, reject); - }); + describe('fetchMore with connection results merging', () => { + itAsync('updateQuery', (resolve, reject) => { + const observable = setup(reject, { + request: { + query: transformedQuery, + variables: variablesMore, + }, + result: resultMore, + }) - itAsync('will set the network status to `fetchMore`', (resolve, reject) => { - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result, - delay: 5, - }, { - request: { query: transformedQuery, variables: variablesMore }, - result: resultMore, - delay: 5, - }).setOnError(reject); + let latestResult: any; + observable.subscribe({ + next(result: any) { + latestResult = result; + }, + }); + + return observable.fetchMore({ + variables: { start: 10 }, // rely on the fact that the original variables had limit: 10 + updateQuery: (prev, options) => { + const state = cloneDeep(prev) as any; + state.entry.comments = [ + ...state.entry.comments, + ...(options.fetchMoreResult as any).entry.comments, + ]; + return state; + }, + }).then(data => { + expect(data.data.entry.comments).toHaveLength(10); // this is the server result + expect(data.loading).toBe(false); + const comments = latestResult.data.entry.comments; + expect(comments).toHaveLength(20); + for (let i = 1; i <= 20; i++) { + expect(comments[i - 1].text).toBe(`comment ${i}`); + } + }).then(resolve, reject); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), + itAsync('field policy', (resolve, reject) => { + const observable = setupWithCacheConfig(reject, { + typePolicies: { + Entry: { + fields: { + comments: concatPagination(), + }, + }, + }, + }, { + request: { + query: transformedQuery, + variables: variablesMore, + }, + result: resultMore, + }) + + let latestResult: any; + observable.subscribe({ + next(result: any) { + latestResult = result; + }, + }); + + return observable.fetchMore({ + variables: { start: 10 }, // rely on the fact that the original variables had limit: 10 + }).then(data => { + expect(data.data.entry.comments).toHaveLength(10); // this is the server result + expect(data.loading).toBe(false); + const comments = latestResult.data.entry.comments; + expect(comments).toHaveLength(20); + for (let i = 1; i <= 20; i++) { + expect(comments[i - 1].text).toBe(`comment ${i}`); + } + }).then(resolve, reject); }); + }); - const observable = client.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: true, + describe('will set the network status to `fetchMore`', () => { + itAsync('updateQuery', (resolve, reject) => { + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result, + delay: 5, + }, { + request: { query: transformedQuery, variables: variablesMore }, + result: resultMore, + delay: 5, + }).setOnError(reject); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + const observable = client.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: true, + }); + + let count = 0; + observable.subscribe({ + next: ({ data, networkStatus }) => { + switch (count++) { + case 0: + expect(networkStatus).toBe(NetworkStatus.ready); + expect((data as any).entry.comments.length).toBe(10); + observable.fetchMore({ + variables: { start: 10 }, + updateQuery: (prev: any, options: any) => { + const state = cloneDeep(prev) as any; + state.entry.comments = [ + ...state.entry.comments, + ...(options.fetchMoreResult as any).entry.comments, + ]; + return state; + }, + }); + break; + case 1: + expect(networkStatus).toBe(NetworkStatus.ready); + expect((data as any).entry.comments.length).toBe(20); + resolve(); + break; + default: + reject(new Error('`next` called too many times')); + } + }, + error: (error: any) => reject(error), + complete: () => reject(new Error('Should not have completed')), + }); }); - let count = 0; - observable.subscribe({ - next: ({ data, networkStatus }) => { - switch (count++) { - case 0: - expect(networkStatus).toBe(NetworkStatus.ready); - expect((data as any).entry.comments.length).toBe(10); - observable.fetchMore({ - variables: { start: 10 }, - updateQuery: (prev: any, options: any) => { - const state = cloneDeep(prev) as any; - state.entry.comments = [ - ...state.entry.comments, - ...(options.fetchMoreResult as any).entry.comments, - ]; - return state; + itAsync('field policy', (resolve, reject) => { + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result, + delay: 5, + }, { + request: { query: transformedQuery, variables: variablesMore }, + result: resultMore, + delay: 5, + }).setOnError(reject); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Entry: { + fields: { + comments: concatPagination(), }, - }); - break; - case 1: - expect(networkStatus).toBe(NetworkStatus.ready); - expect((data as any).entry.comments.length).toBe(20); - resolve(); - break; - default: - reject(new Error('`next` called too many times')); - } - }, - error: (error: any) => reject(error), - complete: () => reject(new Error('Should not have completed')), + }, + }, + }), + }); + + const observable = client.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: true, + }); + + let count = 0; + observable.subscribe({ + next: ({ data, networkStatus }) => { + switch (count++) { + case 0: + expect(networkStatus).toBe(NetworkStatus.ready); + expect((data as any).entry.comments.length).toBe(10); + observable.fetchMore({ + variables: { start: 10 }, + }); + break; + case 1: + expect(networkStatus).toBe(NetworkStatus.ready); + expect((data as any).entry.comments.length).toBe(20); + resolve(); + break; + default: + reject(new Error('`next` called too many times')); + } + }, + error: (error: any) => reject(error), + complete: () => reject(new Error('Should not have completed')), + }); }); }); }); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 58c78775405..cb1cfa081f4 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -16,6 +16,7 @@ import { useMutation } from '../useMutation'; import { QueryFunctionOptions } from '../..'; import { NetworkStatus } from '../../../core/networkStatus'; import { Reference } from '../../../utilities/graphql/storeUtils'; +import { concatPagination } from '../../../utilities'; describe('useQuery Hook', () => { const CAR_QUERY: DocumentNode = gql` @@ -1117,57 +1118,55 @@ describe('useQuery Hook', () => { }); describe('Pagination', () => { - itAsync( - 'should render `fetchMore.updateQuery` updated results with proper ' + - 'loading status, when `notifyOnNetworkStatusChange` is true', - (resolve, reject) => { - const carQuery: DocumentNode = gql` - query cars($limit: Int) { - cars(limit: $limit) { - id - make - model - vin - __typename - } + describe('should render fetchMore-updated results with proper loading status, when `notifyOnNetworkStatusChange` is true', () => { + const carQuery: DocumentNode = gql` + query cars($limit: Int) { + cars(limit: $limit) { + id + make + model + vin + __typename } - `; - - const carResults = { - cars: [ - { - id: 1, - make: 'Audi', - model: 'RS8', - vin: 'DOLLADOLLABILL', - __typename: 'Car' - } - ] - }; - - const moreCarResults = { - cars: [ - { - id: 2, - make: 'Audi', - model: 'eTron', - vin: 'TREESRGOOD', - __typename: 'Car' - } - ] - }; + } + `; - const mocks = [ + const carResults = { + cars: [ { - request: { query: carQuery, variables: { limit: 1 } }, - result: { data: carResults } - }, + id: 1, + make: 'Audi', + model: 'RS8', + vin: 'DOLLADOLLABILL', + __typename: 'Car' + } + ] + }; + + const moreCarResults = { + cars: [ { - request: { query: carQuery, variables: { limit: 1 } }, - result: { data: moreCarResults } + id: 2, + make: 'Audi', + model: 'eTron', + vin: 'TREESRGOOD', + __typename: 'Car' } - ]; + ] + }; + + const mocks = [ + { + request: { query: carQuery, variables: { limit: 1 } }, + result: { data: carResults } + }, + { + request: { query: carQuery, variables: { limit: 1 } }, + result: { data: moreCarResults } + } + ]; + itAsync('updateQuery', (resolve, reject) => { let renderCount = 0; function App() { const { loading, data, fetchMore } = useQuery(carQuery, { @@ -1218,60 +1217,115 @@ describe('useQuery Hook', () => { return wait(() => { expect(renderCount).toBe(3); }).then(resolve, reject); - } - ); + }); - itAsync( - 'should render `fetchMore.updateQuery` updated results with no ' + - 'loading status, when `notifyOnNetworkStatusChange` is false', - (resolve, reject) => { - const carQuery: DocumentNode = gql` - query cars($limit: Int) { - cars(limit: $limit) { - id - make - model - vin - __typename - } + itAsync('field policy', (resolve, reject) => { + let renderCount = 0; + function App() { + const { loading, data, fetchMore } = useQuery(carQuery, { + variables: { limit: 1 }, + notifyOnNetworkStatusChange: true + }); + + switch (++renderCount) { + case 1: + expect(loading).toBeTruthy(); + break; + case 2: + expect(loading).toBeFalsy(); + expect(data).toEqual(carResults); + fetchMore({ + variables: { + limit: 1 + }, + }); + break; + case 3: + expect(loading).toBeFalsy(); + expect(data).toEqual({ + cars: [ + carResults.cars[0], + moreCarResults.cars[0], + ], + }); + break; + default: } - `; - const carResults = { - cars: [ - { - id: 1, - make: 'Audi', - model: 'RS8', - vin: 'DOLLADOLLABILL', - __typename: 'Car' - } - ] - }; + return null; + } - const moreCarResults = { - cars: [ - { - id: 2, - make: 'Audi', - model: 'eTron', - vin: 'TREESRGOOD', - __typename: 'Car' - } - ] - }; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + cars: concatPagination(), + }, + }, + }, + }); - const mocks = [ + render( + + + + ); + + return wait(() => { + expect(renderCount).toBe(3); + }).then(resolve, reject); + }); + }); + + describe('should render fetchMore-updated results with no loading status, when `notifyOnNetworkStatusChange` is false', () => { + const carQuery: DocumentNode = gql` + query cars($limit: Int) { + cars(limit: $limit) { + id + make + model + vin + __typename + } + } + `; + + const carResults = { + cars: [ { - request: { query: carQuery, variables: { limit: 1 } }, - result: { data: carResults } - }, + id: 1, + make: 'Audi', + model: 'RS8', + vin: 'DOLLADOLLABILL', + __typename: 'Car' + } + ] + }; + + const moreCarResults = { + cars: [ { - request: { query: carQuery, variables: { limit: 1 } }, - result: { data: moreCarResults } + id: 2, + make: 'Audi', + model: 'eTron', + vin: 'TREESRGOOD', + __typename: 'Car' } - ]; + ] + }; + + const mocks = [ + { + request: { query: carQuery, variables: { limit: 1 } }, + result: { data: carResults } + }, + { + request: { query: carQuery, variables: { limit: 1 } }, + result: { data: moreCarResults } + } + ]; + itAsync('updateQuery', (resolve, reject) => { let renderCount = 0; function App() { const { loading, data, fetchMore } = useQuery(carQuery, { @@ -1317,8 +1371,63 @@ describe('useQuery Hook', () => { return wait(() => { expect(renderCount).toBe(3); }).then(resolve, reject); - } - ); + }); + + itAsync('field policy', (resolve, reject) => { + let renderCount = 0; + function App() { + const { loading, data, fetchMore } = useQuery(carQuery, { + variables: { limit: 1 }, + notifyOnNetworkStatusChange: false + }); + + switch (renderCount) { + case 0: + expect(loading).toBeTruthy(); + break; + case 1: + expect(loading).toBeFalsy(); + expect(data).toEqual(carResults); + fetchMore({ + variables: { + limit: 1 + }, + }); + break; + case 2: + expect(loading).toBeFalsy(); + expect(data).toEqual({ + cars: [carResults.cars[0], moreCarResults.cars[0]] + }); + break; + default: + } + + renderCount += 1; + return null; + } + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + cars: concatPagination(), + }, + }, + }, + }); + + render( + + + + ); + + return wait(() => { + expect(renderCount).toBe(3); + }).then(resolve, reject); + }); + }); }); describe('Refetching', () => { From a58efd3fb996dcff6010bd3a4389d47864942c61 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 22 Jun 2020 11:40:55 -0400 Subject: [PATCH 6/7] First pass at reorganizing the AC3 section of CHANGELOG.md. --- CHANGELOG.md | 158 +++++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a25b92945..e8976e0cff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,72 @@ # Apollo Client 3.0.0 (TBD - not yet released) -⚠️ **Note:** As of 3.0.0, Apollo Client uses a new package name: [`@apollo/client`](https://www.npmjs.com/package/@apollo/client) - ## Improvements -- **[BREAKING]** `InMemoryCache` will no longer merge the fields of written objects unless the objects are known to have the same identity, and the values of fields with the same name will not be recursively merged unless a custom `merge` function is defined by a field policy for that field, within a type policy associated with the `__typename` of the parent object.
- [@benjamn](https://github.com/benjamn) in [#5603](https://github.com/apollographql/apollo-client/pull/5603) +> ⚠️ **Note:** As of 3.0.0, Apollo Client uses a new package name: [`@apollo/client`](https://www.npmjs.com/package/@apollo/client) -- **[BREAKING]** Eliminate "generated" cache IDs to avoid normalizing objects with no meaningful ID, significantly reducing cache memory usage. This is a backwards-incompatible change if your code depends on the precise internal representation of normalized data in the cache.
- [@benjamn](https://github.com/benjamn) in [#5146](https://github.com/apollographql/apollo-client/pull/5146) +### `ApolloClient` -- **[BREAKING]** Removed `graphql-anywhere` since it's no longer used by Apollo Client.
- [@hwillson](https://github.com/hwillson) in [#5159](https://github.com/apollographql/apollo-client/pull/5159) - -- **[BREAKING]** Removed `apollo-boost` since Apollo Client 3.0 provides a boost like getting started experience out of the box.
- [@hwillson](https://github.com/hwillson) in [#5217](https://github.com/apollographql/apollo-client/pull/5217) +- **[BREAKING]** `ApolloClient` is now only available as a named export. The default `ApolloClient` export has been removed.
+ [@hwillson](https://github.com/hwillson) in [#5425](https://github.com/apollographql/apollo-client/pull/5425) - **[BREAKING]** The `queryManager` property of `ApolloClient` instances is now marked as `private`, paving the way for a more aggressive redesign of its API. +- **[BREAKING]** Apollo Client will no longer deliver "stale" results to `ObservableQuery` consumers, but will instead log more helpful errors about which cache fields were missing.
+ [@benjamn](https://github.com/benjamn) in [#6058](https://github.com/apollographql/apollo-client/pull/6058) + +- **[BREAKING]** `ApolloError`'s thrown by Apollo Client no longer prefix error messages with `GraphQL error:` or `Network error:`. To differentiate between GraphQL/network errors, refer to `ApolloError`'s public `graphQLErrors` and `networkError` properties.
+ [@lorensr](https://github.com/lorensr) in [#3892](https://github.com/apollographql/apollo-client/pull/3892) + +- **[BREAKING]** Support for the `@live` directive has been removed, but might be restored in the future if a more thorough implementation is proposed.
+ [@benjamn](https://github.com/benjamn) in [#6221](https://github.com/apollographql/apollo-client/pull/6221) + +- **[BREAKING]** Apollo Client 2.x allowed `@client` fields to be passed into the `link` chain if `resolvers` were not set in the constructor. This allowed `@client` fields to be passed into Links like `apollo-link-state`. Apollo Client 3 enforces that `@client` fields are local only, meaning they are no longer passed into the `link` chain, under any circumstances.
+ [@hwillson](https://github.com/hwillson) in [#5982](https://github.com/apollographql/apollo-client/pull/5982) + +- **[BREAKING?]** Refactor `QueryManager` to make better use of observables and enforce `fetchPolicy` more reliably.
+ [@benjamn](https://github.com/benjamn) in [#6221](https://github.com/apollographql/apollo-client/pull/6221) + +- Updated to work with `graphql@15`.
+ [@durchanek](https://github.com/durchanek) in [#6194](https://github.com/apollographql/apollo-client/pull/6194) and [#6279](https://github.com/apollographql/apollo-client/pull/6279)
+ [@hagmic](https://github.com/hagmic) in [#6328](https://github.com/apollographql/apollo-client/pull/6328) + +- Apollo Link core and HTTP related functionality has been merged into `@apollo/client`. Functionality that was previously available through the `apollo-link`, `apollo-link-http-common` and `apollo-link-http` packages is now directly available from `@apollo/client` (e.g. `import { HttpLink } from '@apollo/client'`). The `ApolloClient` constructor has also been updated to accept new `uri`, `headers` and `credentials` options. If `uri` is specified, Apollo Client will take care of creating the necessary `HttpLink` behind the scenes.
+ [@hwillson](https://github.com/hwillson) in [#5412](https://github.com/apollographql/apollo-client/pull/5412) + +- The `gql` template tag should now be imported from the `@apollo/client` package, rather than the `graphql-tag` package. Although the `graphql-tag` package still works for now, future versions of `@apollo/client` may change the implementation details of `gql` without a major version bump.
+ [@hwillson](https://github.com/hwillson) in [#5451](https://github.com/apollographql/apollo-client/pull/5451) + +- `@apollo/client/core` can be used to import the Apollo Client core, which includes everything the main `@apollo/client` package does, except for all React related functionality.
+ [@kamilkisiela](https://github.com/kamilkisiela) in [#5541](https://github.com/apollographql/apollo-client/pull/5541) + +- Several deprecated methods have been fully removed: + - `ApolloClient#initQueryManager` + - `QueryManager#startQuery` + - `ObservableQuery#currentResult` + +- Apollo Client now supports setting a new `ApolloLink` (or link chain) after `new ApolloClient()` has been called, using the `ApolloClient#setLink` method.
+ [@hwillson](https://github.com/hwillson) in [#6193](https://github.com/apollographql/apollo-client/pull/6193) + +### `InMemoryCache` + +> ⚠️ **Note:** `InMemoryCache` has been significantly redesigned and rewritten in Apollo Client 3.0. Please consult the [migration guide](https://www.apollographql.com/docs/react/v3.0-beta/migrating/apollo-client-3-migration/#cache-improvements) and read the new [documentation](https://www.apollographql.com/docs/react/v3.0-beta/caching/cache-configuration/) to understand everything that has been improved. + +- The `InMemoryCache` constructor should now be imported directly from `@apollo/client`, rather than from a separate package. The `apollo-cache-inmemory` package is no longer supported. + + > The `@apollo/client/cache` entry point can be used to import `InMemoryCache` without importing other parts of the Apollo Client codebase.
+ [@hwillson](https://github.com/hwillson) in [#5577](https://github.com/apollographql/apollo-client/pull/5577) + - **[BREAKING]** `FragmentMatcher`, `HeuristicFragmentMatcher`, and `IntrospectionFragmentMatcher` have all been removed. We now recommend using `InMemoryCache`’s `possibleTypes` option instead. For more information see the [Defining `possibleTypes` manually](https://www.apollographql.com/docs/react/v3.0-beta/data/fragments/#defining-possibletypes-manually) section of the docs.
[@benjamn](https://github.com/benjamn) in [#5073](https://github.com/apollographql/apollo-client/pull/5073) - **[BREAKING]** As promised in the [Apollo Client 2.6 blog post](https://blog.apollographql.com/whats-new-in-apollo-client-2-6-b3acf28ecad1), all cache results are now frozen/immutable.
[@benjamn](https://github.com/benjamn) in [#5153](https://github.com/apollographql/apollo-client/pull/5153) -- **[BREAKING]** `ApolloClient` is now only available as a named export. The default `ApolloClient` export has been removed.
- [@hwillson](https://github.com/hwillson) in [#5425](https://github.com/apollographql/apollo-client/pull/5425) - -- **[BREAKING]** The `QueryOptions`, `MutationOptions`, and `SubscriptionOptions` React Apollo interfaces have been renamed to `QueryDataOptions`, `MutationDataOptions`, and `SubscriptionDataOptions` (to avoid conflicting with similarly named and exported Apollo Client interfaces). - -- **[BREAKING]** We are no longer exporting certain (intended to be) internal utilities. If you are depending on some of the lesser known exports from `apollo-cache`, `apollo-cache-inmemory`, or `apollo-utilities`, they may no longer be available from `@apollo/client`.
- [@hwillson](https://github.com/hwillson) in [#5437](https://github.com/apollographql/apollo-client/pull/5437) and [#5514](https://github.com/apollographql/apollo-client/pull/5514) - -- **[BREAKING?]** Remove `fixPolyfills.ts`, except when bundling for React Native. If you have trouble with `Map` or `Set` operations due to frozen key objects in React Native, either update React Native to version 0.59.0 (or 0.61.x, if possible) or investigate why `fixPolyfills.native.js` is not included in your bundle.
- [@benjamn](https://github.com/benjamn) in [#5962](https://github.com/apollographql/apollo-client/pull/5962) +- **[BREAKING]** Eliminate "generated" cache IDs to avoid normalizing objects with no meaningful ID, significantly reducing cache memory usage. This might be a backwards-incompatible change if your code depends on the precise internal representation of normalized data in the cache.
+ [@benjamn](https://github.com/benjamn) in [#5146](https://github.com/apollographql/apollo-client/pull/5146) -- **[BREAKING]** Apollo Client 2.x allowed `@client` fields to be passed into the `link` chain if `resolvers` were not set in the constructor. This allowed `@client` fields to be passed into Links like `apollo-link-state`. Apollo Client 3 enforces that `@client` fields are local only, meaning they are no longer passed into the `link` chain, under any circumstances.
- [@hwillson](https://github.com/hwillson) in [#5982](https://github.com/apollographql/apollo-client/pull/5982) +- **[BREAKING]** `InMemoryCache` will no longer merge the fields of written objects unless the objects are known to have the same identity, and the values of fields with the same name will not be recursively merged unless a custom `merge` function is defined by a field policy for that field, within a type policy associated with the `__typename` of the parent object.
+ [@benjamn](https://github.com/benjamn) in [#5603](https://github.com/apollographql/apollo-client/pull/5603) - **[BREAKING]** `InMemoryCache` now _throws_ when data with missing or undefined query fields is written into the cache, rather than just warning in development.
[@benjamn](https://github.com/benjamn) in [#6055](https://github.com/apollographql/apollo-client/pull/6055) @@ -44,18 +74,6 @@ - **[BREAKING]** `client|cache.writeData` have been fully removed. `writeData` usage is one of the easiest ways to turn faulty assumptions about how the cache represents data internally, into cache inconsistency and corruption. `client|cache.writeQuery`, `client|cache.writeFragment`, and/or `cache.modify` can be used to update the cache.
[@benjamn](https://github.com/benjamn) in [#5923](https://github.com/apollographql/apollo-client/pull/5923) -- **[BREAKING]** Apollo Client will no longer deliver "stale" results to `ObservableQuery` consumers, but will instead log more helpful errors about which cache fields were missing.
- [@benjamn](https://github.com/benjamn) in [#6058](https://github.com/apollographql/apollo-client/pull/6058) - -- **[BREAKING]** `ApolloError`'s thrown by Apollo Client no longer prefix error messages with `GraphQL error:` or `Network error:`. To differentiate between GraphQL/network errors, refer to `ApolloError`'s public `graphQLErrors` and `networkError` properties.
- [@lorensr](https://github.com/lorensr) in [#3892](https://github.com/apollographql/apollo-client/pull/3892) - -- **[BREAKING]** Support for the `@live` directive has been removed, but might be restored in the future if a more thorough implementation is proposed.
- [@benjamn](https://github.com/benjamn) in [#6221](https://github.com/apollographql/apollo-client/pull/6221) - -- **[BREAKING?]** Refactor `QueryManager` to make better use of observables and enforce `fetchPolicy` more reliably.
- [@benjamn](https://github.com/benjamn) in [#6221](https://github.com/apollographql/apollo-client/pull/6221) - - `InMemoryCache` now supports tracing garbage collection and eviction. Note that the signature of the `evict` method has been simplified in a potentially backwards-incompatible way.
[@benjamn](https://github.com/benjamn) in [#5310](https://github.com/apollographql/apollo-client/pull/5310) @@ -71,24 +89,6 @@ - `InMemoryCache` now `console.warn`s in development whenever non-normalized data is dangerously overwritten, with helpful links to documentation about normalization and custom `merge` functions.
[@benjamn](https://github.com/benjamn) in [#6372](https://github.com/apollographql/apollo-client/pull/6372) -- The contents of the `@apollo/react-hooks` package have been merged into `@apollo/client`, enabling the following all-in-one `import`: - ```ts - import { ApolloClient, ApolloProvider, useQuery } from '@apollo/client'; - ``` - [@hwillson](https://github.com/hwillson) in [#5357](https://github.com/apollographql/apollo-client/pull/5357) - -- Apollo Link core and HTTP related functionality has been merged into `@apollo/client`. Functionality that was previously available through the `apollo-link`, `apollo-link-http-common` and `apollo-link-http` packages is now directly available from `@apollo/client` (e.g. `import { HttpLink } from '@apollo/client'`). The `ApolloClient` constructor has also been updated to accept new `uri`, `headers` and `credentials` options. If `uri` is specified, Apollo Client will take care of creating the necessary `HttpLink` behind the scenes.
- [@hwillson](https://github.com/hwillson) in [#5412](https://github.com/apollographql/apollo-client/pull/5412) - -- The `gql` template tag should now be imported from the `@apollo/client` package, rather than the `graphql-tag` package. Although the `graphql-tag` package still works for now, future versions of `@apollo/client` may change the implementation details of `gql` without a major version bump.
- [@hwillson](https://github.com/hwillson) in [#5451](https://github.com/apollographql/apollo-client/pull/5451) - -- `@apollo/client/core` can be used to import the Apollo Client core, which includes everything the main `@apollo/client` package does, except for all React related functionality.
- [@kamilkisiela](https://github.com/kamilkisiela) in [#5541](https://github.com/apollographql/apollo-client/pull/5541) - -- `@apollo/client/cache` can be used to import the Apollo Client cache without importing other parts of the Apollo Client codebase.
- [@hwillson](https://github.com/hwillson) in [#5577](https://github.com/apollographql/apollo-client/pull/5577) - - The result caching system (introduced in [#3394](https://github.com/apollographql/apollo-client/pull/3394)) now tracks dependencies at the field level, rather than at the level of whole entity objects, allowing the cache to return identical (`===`) results much more often than before.
[@benjamn](https://github.com/benjamn) in [#5617](https://github.com/apollographql/apollo-client/pull/5617) @@ -124,11 +124,6 @@ - The `cache.readQuery` and `cache.writeQuery` methods now accept an `options.id` string, which eliminates most use cases for `cache.readFragment` and `cache.writeFragment`, and skips the implicit conversion of fragment documents to query documents performed by `cache.{read,write}Fragment`.
[@benjamn](https://github.com/benjamn) in [#5930](https://github.com/apollographql/apollo-client/pull/5930) -- Several deprecated methods have been fully removed: - - `ApolloClient#initQueryManager` - - `QueryManager#startQuery` - - `ObservableQuery#currentResult` - - Support `cache.identify(entity)` for easily computing entity ID strings.
[@benjamn](https://github.com/benjamn) in [#5642](https://github.com/apollographql/apollo-client/pull/5642) @@ -147,8 +142,38 @@ - Custom field `read` functions can read from neighboring fields using the `readField(fieldName)` helper, and may also read fields from other entities by calling `readField(fieldName, objectOrReference)`.
[@benjamn](https://github.com/benjamn) in [#5651](https://github.com/apollographql/apollo-client/pull/5651) -- Utilities that were previously externally available through the `apollo-utilities` package are now only available by importing from `@apollo/client/utilities`.
- [@hwillson](https://github.com/hwillson) in [#5683](https://github.com/apollographql/apollo-client/pull/5683) +- Expose cache `modify` and `identify` to the mutate `update` function.
+ [@hwillson](https://github.com/hwillson) in [#5956](https://github.com/apollographql/apollo-client/pull/5956) + +- Add a default `gc` implementation to `ApolloCache`.
+ [@justinwaite](https://github.com/justinwaite) in [#5974](https://github.com/apollographql/apollo-client/pull/5974) + +### React + +- **[BREAKING]** The `QueryOptions`, `MutationOptions`, and `SubscriptionOptions` React Apollo interfaces have been renamed to `QueryDataOptions`, `MutationDataOptions`, and `SubscriptionDataOptions` (to avoid conflicting with similarly named and exported Apollo Client interfaces). + +- **[BREAKING?]** Remove `fixPolyfills.ts`, except when bundling for React Native. If you have trouble with `Map` or `Set` operations due to frozen key objects in React Native, either update React Native to version 0.59.0 (or 0.61.x, if possible) or investigate why `fixPolyfills.native.js` is not included in your bundle.
+ [@benjamn](https://github.com/benjamn) in [#5962](https://github.com/apollographql/apollo-client/pull/5962) + +- The contents of the `@apollo/react-hooks` package have been merged into `@apollo/client`, enabling the following all-in-one `import`: + ```ts + import { ApolloClient, ApolloProvider, useQuery } from '@apollo/client'; + ``` + [@hwillson](https://github.com/hwillson) in [#5357](https://github.com/apollographql/apollo-client/pull/5357) + +### General + +- **[BREAKING]** Removed `graphql-anywhere` since it's no longer used by Apollo Client.
+ [@hwillson](https://github.com/hwillson) in [#5159](https://github.com/apollographql/apollo-client/pull/5159) + +- **[BREAKING]** Removed `apollo-boost` since Apollo Client 3.0 provides a boost like getting started experience out of the box.
+ [@hwillson](https://github.com/hwillson) in [#5217](https://github.com/apollographql/apollo-client/pull/5217) + +- **[BREAKING]** We are no longer exporting certain (intended to be) internal utilities. If you are depending on some of the lesser known exports from `apollo-cache`, `apollo-cache-inmemory`, or `apollo-utilities`, they may no longer be available from `@apollo/client`.
+ [@hwillson](https://github.com/hwillson) in [#5437](https://github.com/apollographql/apollo-client/pull/5437) and [#5514](https://github.com/apollographql/apollo-client/pull/5514) + + > Utilities that were previously externally available through the `apollo-utilities` package are now only available by importing from `@apollo/client/utilities`.
+ [@hwillson](https://github.com/hwillson) in [#5683](https://github.com/apollographql/apollo-client/pull/5683) - Make sure all `graphql-tag` public exports are re-exported.
[@hwillson](https://github.com/hwillson) in [#5861](https://github.com/apollographql/apollo-client/pull/5861) @@ -159,19 +184,6 @@ - Make sure `ApolloContext` plays nicely with IE11 when storing the shared context.
[@ms](https://github.com/ms) in [#5840](https://github.com/apollographql/apollo-client/pull/5840) -- Expose cache `modify` and `identify` to the mutate `update` function.
- [@hwillson](https://github.com/hwillson) in [#5956](https://github.com/apollographql/apollo-client/pull/5956) - -- Add a default `gc` implementation to `ApolloCache`.
- [@justinwaite](https://github.com/justinwaite) in [#5974](https://github.com/apollographql/apollo-client/pull/5974) - -- Updated to work with `graphql@15`.
- [@durchanek](https://github.com/durchanek) in [#6194](https://github.com/apollographql/apollo-client/pull/6194) and [#6279](https://github.com/apollographql/apollo-client/pull/6279)
- [@hagmic](https://github.com/hagmic) in [#6328](https://github.com/apollographql/apollo-client/pull/6328) - -- Apollo Client now supports setting a new `ApolloLink` (or link chain) after `new ApolloClient()` has been called, using the `ApolloClient#setLink` method.
- [@hwillson](https://github.com/hwillson) in [#6193](https://github.com/apollographql/apollo-client/pull/6193) - ### Bug Fixes - `useMutation` adjustments to help avoid an infinite loop / too many renders issue, caused by unintentionally modifying the `useState` based mutation result directly.
From b1810372511dafcd301c153dab995e1bd407e5e8 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 22 Jun 2020 11:45:22 -0400 Subject: [PATCH 7/7] Mention PR #6464 in CHANGELOG.md. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8976e0cff1..07c621b909a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ - **[BREAKING?]** Refactor `QueryManager` to make better use of observables and enforce `fetchPolicy` more reliably.
[@benjamn](https://github.com/benjamn) in [#6221](https://github.com/apollographql/apollo-client/pull/6221) +- The `updateQuery` function previously required by `fetchMore` has been deprecated with a warning, and will be removed in the next major version of Apollo Client. Please consider using a `merge` function to handle incoming data instead of relying on `updateQuery`.
+ [@benjamn](https://github.com/benjamn) in [#6464](https://github.com/apollographql/apollo-client/pull/6464) + - Updated to work with `graphql@15`.
[@durchanek](https://github.com/durchanek) in [#6194](https://github.com/apollographql/apollo-client/pull/6194) and [#6279](https://github.com/apollographql/apollo-client/pull/6279)
[@hagmic](https://github.com/hagmic) in [#6328](https://github.com/apollographql/apollo-client/pull/6328)