Skip to content

Commit

Permalink
Mutate update function DataProxy/ApolloCache changes
Browse files Browse the repository at this point in the history
This commit gives the mutate `update` function direct
access to the cache, instead of using a scaled down
`DataProxy`. While the `DataProxy` approach helped in the
past as a way to batch cache updates, it is no longer
necessary. We are only ever passing a cache instance into
`update`, which means limiting the type to be a `DataProxy`
gives us no additional advantages. The only other `DataProxy`
implementor in the AC codebase is `ApolloClient`, which
is never passed to the `update` function.

This commit also exposes optional `identify` and `modify`
API elements in `ApolloCache`. Since `update` now has
access to the full `ApolloCache` instance passed in,
`identify` and `modify` can be used in the update function.
Note that the `ApolloCache` implementations of `identify`
and `modify` do nothing of value; they should be overridden
by `ApolloCache` subclasses that are interested in using them,
like `InMemoryCache` does. By including them in `ApolloCache`
as basically no-op methods, we're avoiding forcing
alternative cache implementations that subclass `ApolloCache`
to have to support them.
  • Loading branch information
hwillson committed Feb 18, 2020
1 parent da8c3c3 commit be73571
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 65 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@
- Make sure `ApolloContext` plays nicely with IE11 when storing the shared context. <br/>
[@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. <br/>
[@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. <br/>
Expand Down
28 changes: 24 additions & 4 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -61,23 +63,41 @@ export abstract class ApolloCache<TSerialized> 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<TSerialized>,
): void;

public abstract recordOptimisticTransaction(
transaction: Transaction<TSerialized>,
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<any> | Modifiers,
optimistic = false,
): boolean {
return false;
}

// Experimental API

public transformForLink(document: DocumentNode): DocumentNode {
return document;
}
Expand Down
38 changes: 38 additions & 0 deletions src/cache/core/types/common.ts
Original file line number Diff line number Diff line change
@@ -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<T> 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<string> collapses to just string, which makes string
// assignable to SafeReadonly<any>, whereas string is not assignable to
// Readonly<any>, somewhat surprisingly.
export type SafeReadonly<T> = T extends object ? Readonly<T> : T;

export type Modifier<T> = (value: T, details: {
DELETE: any;
fieldName: string;
storeFieldName: string;
isReference: typeof isReference;
toReference(
object: StoreObject,
selectionSet?: SelectionSetNode,
fragmentMap?: FragmentMap,
): Reference;
readField<V = StoreValue>(
fieldName: string,
objOrRef?: StoreObject | Reference,
): SafeReadonly<V>;
}) => T;

export type Modifiers = {
[fieldName: string]: Modifier<any>;
}
27 changes: 9 additions & 18 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
@@ -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<T> = (value: T, details: {
DELETE: typeof DELETE;
fieldName: string;
storeFieldName: string;
isReference: typeof isReference;
toReference: Policies["toReference"];
readField<V = StoreValue>(
fieldName: string,
objOrRef?: StoreObject | Reference,
): SafeReadonly<V>;
}) => T;

export type Modifiers = {
[fieldName: string]: Modifier<any>;
}

const DELETE: any = Object.create(null);
const delModifier: Modifier<any> = () => DELETE;

Expand Down
11 changes: 9 additions & 2 deletions src/cache/inmemory/helpers.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
5 changes: 3 additions & 2 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 3 additions & 9 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,7 +32,6 @@ import { Cache } from '../core/types/Cache';
import {
DiffQueryAgainstStoreOptions,
ReadQueryOptions,
StoreObject,
NormalizedCache,
} from './types';
import { supportsResultCaching } from './entityStore';
Expand Down
19 changes: 3 additions & 16 deletions src/cache/inmemory/types.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -59,20 +60,6 @@ export interface NormalizedCacheObject {
[dataId: string]: StoreObject | undefined;
}

export interface StoreObject {
__typename?: string;
[storeFieldName: string]: StoreValue;
}

// The Readonly<T> 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<string> collapses to just string, which makes string
// assignable to SafeReadonly<any>, whereas string is not assignable to
// Readonly<any>, somewhat surprisingly.
export type SafeReadonly<T> = T extends object ? Readonly<T> : T;

export type OptimisticStoreItem = {
id: string;
data: NormalizedCacheObject;
Expand Down
3 changes: 2 additions & 1 deletion src/cache/inmemory/writeToStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import {
isField,
resultKeyNameFromField,
StoreValue,
StoreObject,
} from '../../utilities/graphql/storeUtils';

import { shouldInclude } from '../../utilities/graphql/directives';
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 = {
Expand Down
5 changes: 3 additions & 2 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1452,7 +1451,9 @@ function markMutationResult<TStore>(
document: DocumentNode;
variables: any;
queryUpdatersById: Record<string, QueryWithUpdater>;
update: ((proxy: DataProxy, mutationResult: Object) => void) | undefined;
update:
((cache: ApolloCache<TStore>, mutationResult: Object) => void) |
undefined;
},
cache: ApolloCache<TStore>,
) {
Expand Down
15 changes: 5 additions & 10 deletions src/core/watchQueryOptions.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -284,6 +279,6 @@ export interface MutationOptions<

// Add a level of indirection for `typedoc`.
export type MutationUpdaterFn<T = { [key: string]: any }> = (
proxy: DataProxy,
cache: ApolloCache<T>,
mutationResult: FetchResult<T>,
) => void;
5 changes: 5 additions & 0 deletions src/utilities/graphql/storeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down

0 comments on commit be73571

Please sign in to comment.