diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d1263e2faf..fa6b5b705a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,9 @@ - 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) + ### Bug Fixes - `useMutation` adjustments to help avoid an infinite loop / too many renders issue, caused by unintentionally modifying the `useState` based mutation result directly.
diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index dceca187a21..9a510135ce7 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -2,9 +2,11 @@ import { DocumentNode } from 'graphql'; import { wrap } from 'optimism'; import { getFragmentQueryDocument } from '../../utilities/graphql/fragments'; +import { StoreObject } from '../../utilities/graphql/storeUtils'; import { DataProxy } from './types/DataProxy'; import { Cache } from './types/Cache'; import { queryFromPojo, fragmentFromPojo } from './utils'; +import { Modifier, Modifiers } from './types/common'; const justTypenameQuery: DocumentNode = { kind: "Document", @@ -61,23 +63,41 @@ export abstract class ApolloCache implements DataProxy { */ public abstract extract(optimistic?: boolean): TSerialized; - // optimistic API + // Optimistic API + public abstract removeOptimistic(id: string): void; - // transactional API + // Transactional API + public abstract performTransaction( transaction: Transaction, ): void; + public abstract recordOptimisticTransaction( transaction: Transaction, id: string, ): void; - // optional API + // Optional API + public transformDocument(document: DocumentNode): DocumentNode { return document; } - // experimental + + public identify(object: StoreObject): string | null { + return null; + } + + public modify( + dataId: string, + modifiers: Modifier | Modifiers, + optimistic = false, + ): boolean { + return false; + } + + // Experimental API + public transformForLink(document: DocumentNode): DocumentNode { return document; } diff --git a/src/cache/core/types/common.ts b/src/cache/core/types/common.ts new file mode 100644 index 00000000000..9ba9f4a1c06 --- /dev/null +++ b/src/cache/core/types/common.ts @@ -0,0 +1,38 @@ +import { SelectionSetNode } from 'graphql'; + +import { + isReference, + StoreValue, + StoreObject, + Reference +} from '../../../utilities/graphql/storeUtils'; +import { FragmentMap } from '../../../utilities/graphql/fragments'; + +// The Readonly type only really works for object types, since it marks +// all of the object's properties as readonly, but there are many cases when +// a generic type parameter like TExisting might be a string or some other +// primitive type, in which case we need to avoid wrapping it with Readonly. +// SafeReadonly collapses to just string, which makes string +// assignable to SafeReadonly, whereas string is not assignable to +// Readonly, somewhat surprisingly. +export type SafeReadonly = T extends object ? Readonly : T; + +export type Modifier = (value: T, details: { + DELETE: any; + fieldName: string; + storeFieldName: string; + isReference: typeof isReference; + toReference( + object: StoreObject, + selectionSet?: SelectionSetNode, + fragmentMap?: FragmentMap, + ): Reference; + readField( + fieldName: string, + objOrRef?: StoreObject | Reference, + ): SafeReadonly; +}) => T; + +export type Modifiers = { + [fieldName: string]: Modifier; +} diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index 3afefc3bf39..de184cec086 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -1,32 +1,23 @@ import { dep, OptimisticDependencyFunction, KeyTrie } from 'optimism'; import { equal } from '@wry/equality'; -import { isReference, StoreValue, Reference, makeReference } from '../../utilities/graphql/storeUtils'; +import { + isReference, + StoreValue, + StoreObject, + Reference, + makeReference +} from '../../utilities/graphql/storeUtils'; import { DeepMerger } from '../../utilities/common/mergeDeep'; import { maybeDeepFreeze } from '../../utilities/common/maybeDeepFreeze'; import { canUseWeakMap } from '../../utilities/common/canUse'; -import { NormalizedCache, NormalizedCacheObject, StoreObject, SafeReadonly } from './types'; +import { NormalizedCache, NormalizedCacheObject } from './types'; import { fieldNameFromStoreName } from './helpers'; import { Policies } from './policies'; +import { Modifier, Modifiers, SafeReadonly } from '../core/types/common'; const hasOwn = Object.prototype.hasOwnProperty; -export type Modifier = (value: T, details: { - DELETE: typeof DELETE; - fieldName: string; - storeFieldName: string; - isReference: typeof isReference; - toReference: Policies["toReference"]; - readField( - fieldName: string, - objOrRef?: StoreObject | Reference, - ): SafeReadonly; -}) => T; - -export type Modifiers = { - [fieldName: string]: Modifier; -} - const DELETE: any = Object.create(null); const delModifier: Modifier = () => DELETE; diff --git a/src/cache/inmemory/helpers.ts b/src/cache/inmemory/helpers.ts index 232e4c050a2..9f7ca769031 100644 --- a/src/cache/inmemory/helpers.ts +++ b/src/cache/inmemory/helpers.ts @@ -1,6 +1,13 @@ import { FieldNode } from 'graphql'; -import { NormalizedCache, StoreObject } from './types'; -import { Reference, isReference, StoreValue, isField } from '../../utilities/graphql/storeUtils'; + +import { NormalizedCache } from './types'; +import { + Reference, + isReference, + StoreValue, + StoreObject, + isField +} from '../../utilities/graphql/storeUtils'; import { DeepMerger, ReconcilerFunction } from '../../utilities/common/mergeDeep'; export function getTypenameFromStoreObject( diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index e3dbb16eb70..d2a2f66e8b7 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -6,15 +6,16 @@ import { dep, wrap } from 'optimism'; import { ApolloCache, Transaction } from '../core/cache'; import { Cache } from '../core/types/Cache'; +import { Modifier, Modifiers } from '../core/types/common'; import { addTypenameToDocument } from '../../utilities/graphql/transform'; +import { StoreObject } from '../../utilities/graphql/storeUtils'; import { ApolloReducerConfig, NormalizedCacheObject, - StoreObject, } from './types'; import { StoreReader } from './readFromStore'; import { StoreWriter } from './writeToStore'; -import { EntityStore, supportsResultCaching, Modifiers, Modifier } from './entityStore'; +import { EntityStore, supportsResultCaching } from './entityStore'; import { defaultDataIdFromObject, PossibleTypesMap, diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 514a97335f4..a46ac91308f 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -12,32 +12,26 @@ import { FragmentMap, getFragmentFromSelection, } from '../../utilities/graphql/fragments'; - import { isField, getTypenameFromResult, storeKeyNameFromField, StoreValue, + StoreObject, argumentsObjectFromField, Reference, makeReference, isReference, } from '../../utilities/graphql/storeUtils'; - import { canUseWeakMap } from '../../utilities/common/canUse'; - -import { - IdGetter, - StoreObject, - SafeReadonly, -} from "./types"; - +import { IdGetter } from "./types"; import { fieldNameFromStoreName, FieldValueToBeMerged, isFieldValueToBeMerged, } from './helpers'; import { FieldValueGetter } from './entityStore'; +import { SafeReadonly } from '../core/types/common'; const hasOwn = Object.prototype.hasOwnProperty; diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 1688f4343e3..99941d81b18 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -15,6 +15,7 @@ import { Reference, isReference, makeReference, + StoreObject, } from '../../utilities/graphql/storeUtils'; import { createFragmentMap, FragmentMap } from '../../utilities/graphql/fragments'; import { shouldInclude } from '../../utilities/graphql/directives'; @@ -31,7 +32,6 @@ import { Cache } from '../core/types/Cache'; import { DiffQueryAgainstStoreOptions, ReadQueryOptions, - StoreObject, NormalizedCache, } from './types'; import { supportsResultCaching } from './entityStore'; diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts index e66255c0c1a..06477429b8d 100644 --- a/src/cache/inmemory/types.ts +++ b/src/cache/inmemory/types.ts @@ -1,8 +1,9 @@ import { DocumentNode } from 'graphql'; import { Transaction } from '../core/cache'; -import { StoreValue } from '../../utilities/graphql/storeUtils'; -import { Modifiers, Modifier, FieldValueGetter } from './entityStore'; +import { Modifier, Modifiers } from '../core/types/common'; +import { StoreValue, StoreObject } from '../../utilities/graphql/storeUtils'; +import { FieldValueGetter } from './entityStore'; export { StoreValue } export interface IdGetterObj extends Object { @@ -59,20 +60,6 @@ export interface NormalizedCacheObject { [dataId: string]: StoreObject | undefined; } -export interface StoreObject { - __typename?: string; - [storeFieldName: string]: StoreValue; -} - -// The Readonly type only really works for object types, since it marks -// all of the object's properties as readonly, but there are many cases when -// a generic type parameter like TExisting might be a string or some other -// primitive type, in which case we need to avoid wrapping it with Readonly. -// SafeReadonly collapses to just string, which makes string -// assignable to SafeReadonly, whereas string is not assignable to -// Readonly, somewhat surprisingly. -export type SafeReadonly = T extends object ? Readonly : T; - export type OptimisticStoreItem = { id: string; data: NormalizedCacheObject; diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts index 6087b58068e..2587566f5b2 100644 --- a/src/cache/inmemory/writeToStore.ts +++ b/src/cache/inmemory/writeToStore.ts @@ -19,6 +19,7 @@ import { isField, resultKeyNameFromField, StoreValue, + StoreObject, } from '../../utilities/graphql/storeUtils'; import { shouldInclude } from '../../utilities/graphql/directives'; @@ -26,7 +27,7 @@ import { cloneDeep } from '../../utilities/common/cloneDeep'; import { Policies } from './policies'; import { defaultNormalizedCacheFactory } from './entityStore'; -import { NormalizedCache, StoreObject } from './types'; +import { NormalizedCache } from './types'; import { makeProcessedFieldsMerger, FieldValueToBeMerged } from './helpers'; export type WriteContext = { diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 50eb63fc881..4360af1e33c 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -5,7 +5,6 @@ import { ApolloLink } from '../link/core/ApolloLink'; import { execute } from '../link/core/execute'; import { FetchResult } from '../link/core/types'; import { Cache } from '../cache/core/types/Cache'; -import { DataProxy } from '../cache/core/types/DataProxy'; import { getDefaultValues, @@ -1452,7 +1451,9 @@ function markMutationResult( document: DocumentNode; variables: any; queryUpdatersById: Record; - update: ((proxy: DataProxy, mutationResult: Object) => void) | undefined; + update: + ((cache: ApolloCache, mutationResult: Object) => void) | + undefined; }, cache: ApolloCache, ) { diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index f789dd93db8..934ab26f62a 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -1,7 +1,7 @@ import { DocumentNode, ExecutionResult } from 'graphql'; +import { ApolloCache } from '../cache/core/cache'; import { FetchResult } from '../link/core/types'; -import { DataProxy } from '../cache/core/types/DataProxy'; import { MutationQueryReducersMap } from './types'; import { PureQueryOptions, OperationVariables } from './types'; @@ -218,9 +218,9 @@ export interface MutationBaseOptions< awaitRefetchQueries?: boolean; /** - * A function which provides a {@link DataProxy} and the result of the - * mutation to allow the user to update the store based on the results of the - * mutation. + * A function which provides an {@link ApolloCache} instance, and the result + * of the mutation, to allow the user to update the store based on the + * results of the mutation. * * This function will be called twice over the lifecycle of a mutation. Once * at the very beginning if an `optimisticResponse` was provided. The writes @@ -229,11 +229,6 @@ export interface MutationBaseOptions< * resolved. At that point `update` will be called with the *actual* mutation * result and those writes will not be rolled back. * - * The reason a {@link DataProxy} is provided instead of the user calling the - * methods directly on {@link ApolloClient} is that all of the writes are - * batched together at the end of the update, and it allows for writes - * generated by optimistic data to be rolled back. - * * Note that since this function is intended to be used to update the * store, it cannot be used with a `no-cache` fetch policy. If you're * interested in performing some action after a mutation has completed, @@ -284,6 +279,6 @@ export interface MutationOptions< // Add a level of indirection for `typedoc`. export type MutationUpdaterFn = ( - proxy: DataProxy, + cache: ApolloCache, mutationResult: FetchResult, ) => void; diff --git a/src/utilities/graphql/storeUtils.ts b/src/utilities/graphql/storeUtils.ts index 5bf701f1a2c..9cb365d3a29 100644 --- a/src/utilities/graphql/storeUtils.ts +++ b/src/utilities/graphql/storeUtils.ts @@ -44,6 +44,11 @@ export type StoreValue = | void | Object; +export interface StoreObject { + __typename?: string; + [storeFieldName: string]: StoreValue; +} + function isStringValue(value: ValueNode): value is StringValueNode { return value.kind === 'StringValue'; }