diff --git a/CHANGELOG.md b/CHANGELOG.md index 6396331ce7d..1359a1669e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ - 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`.
[@hwillson](https://github.com/hwillson) in [#6837](https://github.com/apollographql/apollo-client/pull/6837) +- 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.
+ [@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`.
[@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) diff --git a/src/__tests__/__snapshots__/mutationResults.ts.snap b/src/__tests__/__snapshots__/mutationResults.ts.snap new file mode 100644 index 00000000000..ebcfff7bb25 --- /dev/null +++ b/src/__tests__/__snapshots__/mutationResults.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mutation results should write results to cache according to errorPolicy 1`] = `Object {}`; + +exports[`mutation results should write results to cache according to errorPolicy 2`] = ` +Object { + "Person:{\\"name\\":\\"Jenn Creighton\\"}": Object { + "__typename": "Person", + "name": "Jenn Creighton", + }, + "ROOT_MUTATION": Object { + "__typename": "Mutation", + "newPerson({\\"name\\":\\"Jenn Creighton\\"})": Object { + "__ref": "Person:{\\"name\\":\\"Jenn Creighton\\"}", + }, + }, +} +`; + +exports[`mutation results should write results to cache according to errorPolicy 3`] = ` +Object { + "Person:{\\"name\\":\\"Ellen Shapiro\\"}": Object { + "__typename": "Person", + "name": "Ellen Shapiro", + }, + "Person:{\\"name\\":\\"Jenn Creighton\\"}": Object { + "__typename": "Person", + "name": "Jenn Creighton", + }, + "ROOT_MUTATION": Object { + "__typename": "Mutation", + "newPerson({\\"name\\":\\"Ellen Shapiro\\"})": Object { + "__ref": "Person:{\\"name\\":\\"Ellen Shapiro\\"}", + }, + "newPerson({\\"name\\":\\"Jenn Creighton\\"})": Object { + "__ref": "Person:{\\"name\\":\\"Jenn Creighton\\"}", + }, + }, +} +`; diff --git a/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts index 9d87841f9fe..ed36c587cba 100644 --- a/src/__tests__/mutationResults.ts +++ b/src/__tests__/mutationResults.ts @@ -1,5 +1,6 @@ import { cloneDeep } from 'lodash'; import gql from 'graphql-tag'; +import { GraphQLError } from 'graphql'; import { ApolloClient } from '../core'; import { InMemoryCache } from '../cache'; @@ -306,6 +307,99 @@ describe('mutation results', () => { }); }); + itAsync("should write results to cache according to errorPolicy", async (resolve, reject) => { + const expectedFakeError = new GraphQLError("expected/fake error"); + + const client = new ApolloClient({ + cache: new InMemoryCache({ + typePolicies: { + Person: { + keyFields: ["name"], + }, + }, + }), + + link: new ApolloLink(operation => new Observable(observer => { + observer.next({ + errors: [ + expectedFakeError, + ], + data: { + newPerson: { + __typename: "Person", + name: operation.variables.newName, + }, + }, + }); + observer.complete(); + })).setOnError(reject), + }); + + const mutation = gql` + mutation AddNewPerson($newName: String!) { + newPerson(name: $newName) { + name + } + } + `; + + await client.mutate({ + mutation, + variables: { + newName: "Hugh Willson", + }, + }).then(() => { + reject("should have thrown for default errorPolicy"); + }, error => { + expect(error.message).toBe(expectedFakeError.message); + }); + + expect(client.cache.extract()).toMatchSnapshot(); + + const ignoreErrorsResult = await client.mutate({ + mutation, + errorPolicy: "ignore", + variables: { + newName: "Jenn Creighton", + }, + }); + + expect(ignoreErrorsResult).toEqual({ + data: { + newPerson: { + __typename: "Person", + name: "Jenn Creighton", + }, + }, + }); + + expect(client.cache.extract()).toMatchSnapshot(); + + const allErrorsResult = await client.mutate({ + mutation, + errorPolicy: "all", + variables: { + newName: "Ellen Shapiro", + }, + }); + + expect(allErrorsResult).toEqual({ + data: { + newPerson: { + __typename: "Person", + name: "Ellen Shapiro", + }, + }, + errors: [ + expectedFakeError, + ], + }); + + expect(client.cache.extract()).toMatchSnapshot(); + + resolve(); + }); + itAsync("should warn when the result fields don't match the query fields", (resolve, reject) => { let handle: any; let subscriptionHandle: Subscription; diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index dff299889df..e3c82e511c7 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -2,7 +2,7 @@ import { DocumentNode, GraphQLError } from 'graphql'; import { equal } from "@wry/equality"; import { Cache, ApolloCache } from '../cache'; -import { WatchQueryOptions } from './watchQueryOptions'; +import { WatchQueryOptions, ErrorPolicy } from './watchQueryOptions'; import { ObservableQuery } from './ObservableQuery'; import { QueryListener } from './types'; import { FetchResult } from '../link/core'; @@ -288,15 +288,7 @@ export class QueryInfo { this.diff = { result: result.data, complete: true }; } else if (allowCacheWrite) { - const ignoreErrors = - options.errorPolicy === 'ignore' || - options.errorPolicy === 'all'; - let writeWithErrors = !graphQLResultHasError(result); - if (!writeWithErrors && ignoreErrors && result.data) { - writeWithErrors = true; - } - - if (writeWithErrors) { + if (shouldWriteResult(result, options.errorPolicy)) { // Using a transaction here so we have a chance to read the result // back from the cache before the watch callback fires as a result // of writeQuery, so we can store the new diff quietly and ignore @@ -405,3 +397,17 @@ export class QueryInfo { return error; } } + +export function shouldWriteResult( + result: FetchResult, + errorPolicy: ErrorPolicy = "none", +) { + const ignoreErrors = + errorPolicy === "ignore" || + errorPolicy === "all"; + let writeWithErrors = !graphQLResultHasError(result); + if (!writeWithErrors && ignoreErrors && result.data) { + writeWithErrors = true; + } + return writeWithErrors; +} diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index af0fb4437fc..42fde4aefcb 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -39,7 +39,7 @@ import { } from './types'; import { LocalState } from './LocalState'; -import { QueryInfo, QueryStoreValue } from './QueryInfo'; +import { QueryInfo, QueryStoreValue, shouldWriteResult } from './QueryInfo'; const { hasOwnProperty } = Object.prototype; @@ -192,6 +192,7 @@ export class QueryManager { result: { data: optimistic }, document: mutation, variables: variables, + errorPolicy, queryUpdatersById: generateUpdateQueriesInfo(), update: updateWithProxyFn, }, cache); @@ -235,6 +236,7 @@ export class QueryManager { result, document: mutation, variables, + errorPolicy, queryUpdatersById: generateUpdateQueriesInfo(), update: updateWithProxyFn, }, self.cache); @@ -588,6 +590,7 @@ export class QueryManager { public startGraphQLSubscription({ query, fetchPolicy, + errorPolicy, variables, context = {}, }: SubscriptionOptions): Observable> { @@ -601,10 +604,10 @@ export class QueryManager { variables, false, ).map(result => { - if (!fetchPolicy || fetchPolicy !== 'no-cache') { + if (fetchPolicy !== 'no-cache') { // the subscription interface should handle not sending us results we no longer subscribe to. // XXX I don't think we ever send in an object with errors, but we might in the future... - if (!graphQLResultHasError(result)) { + if (shouldWriteResult(result, errorPolicy)) { this.cache.write({ query, result: result.data, @@ -1078,6 +1081,7 @@ function markMutationResult( result: FetchResult; document: DocumentNode; variables: any; + errorPolicy: ErrorPolicy; queryUpdatersById: Record; update: ((cache: ApolloCache, mutationResult: Object) => void) | @@ -1086,7 +1090,7 @@ function markMutationResult( cache: ApolloCache, ) { // Incorporate the result from this mutation into the store - if (!graphQLResultHasError(mutation.result)) { + if (shouldWriteResult(mutation.result, mutation.errorPolicy)) { const cacheWrites: Cache.WriteOptions[] = [{ result: mutation.result.data, dataId: 'ROOT_MUTATION', diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 4382a9888e3..e3b2ebe6602 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -169,6 +169,11 @@ export interface SubscriptionOptions