Skip to content

Commit

Permalink
Support options.isReference(ref, true) to check Reference validity.
Browse files Browse the repository at this point in the history
The story we're telling in #6412 about using custom read functions to
filter out dangling references works best if there's an easy way to check
whether a Reference points to existing data in the cache. Although we
could have introduced a new options.isValidReference helper function, I
think it makes sense to let the existing options.isReference function
handle this use case as well.

I ended up refactoring how the toReference function gets created and
passed around as well, since I want toReference and isReference to remain
together as much as possible. I considered making isReference a property
of EntityStore (like toReference used to be), but that would not have
worked because the new isReference(ref, true) functionality needs access
to the topmost layer of the cache, which only InMemoryCache knows about.
Long story short, both isReference and toReference are now methods of
InMemoryCache, rather than EntityStore.
  • Loading branch information
benjamn committed Jun 8, 2020
1 parent 7f2b1b0 commit f6ec18f
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 66 deletions.
8 changes: 6 additions & 2 deletions src/cache/core/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Reference,
StoreObject,
StoreValue,
isReference,
} from '../../../core';

// The Readonly<T> type only really works for object types, since it marks
Expand Down Expand Up @@ -50,12 +49,17 @@ export type ToReferenceFunction = (
mergeIntoStore?: boolean,
) => Reference | undefined;

export type IsReferenceFunction = (
candidate: any,
mustBeValid?: boolean,
) => candidate is Reference;

export type Modifier<T> = (value: T, details: {
DELETE: any;
fieldName: string;
storeFieldName: string;
readField: ReadFieldFunction;
isReference: typeof isReference;
isReference: IsReferenceFunction;
toReference: ToReferenceFunction;
}) => T;

Expand Down
26 changes: 14 additions & 12 deletions src/cache/inmemory/__tests__/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,24 +923,26 @@ describe('reading from the store', () => {
Deity: {
keyFields: ["name"],
fields: {
children(offspring: Reference[], { readField }) {
return offspring ? offspring.filter(child => {
// TODO Improve this test? Maybe isReference(ref, true)?
return void 0 !== readField("__typename", child);
}) : [];
children(offspring: Reference[], { isReference }) {
return offspring ? offspring.filter(
// The true argument here makes isReference return true
// only if child is a Reference object that points to
// valid entity data in the EntityStore (that is, not a
// dangling reference).
child => isReference(child, true)
) : [];
},
},
},

Query: {
fields: {
ruler(ruler, { toReference, readField }) {
// TODO Improve this test? Maybe !isReference(ruler, true)?
if (!ruler || void 0 === readField("__typename", ruler)) {
// If there's no official ruler, promote Apollo!
return toReference({ __typename: "Deity", name: "Apollo" });
}
return ruler;
ruler(ruler, { isReference, toReference }) {
// If the throne is empty, promote Apollo!
return isReference(ruler, true) ? ruler : toReference({
__typename: "Deity",
name: "Apollo",
});
},
},
},
Expand Down
30 changes: 8 additions & 22 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
Modifiers,
ReadFieldFunction,
ReadFieldOptions,
ToReferenceFunction,
} from '../core/types/common';

const DELETE: any = Object.create(null);
Expand Down Expand Up @@ -126,6 +125,8 @@ export abstract class EntityStore implements NormalizedCache {
let needToMerge = false;
let allDeleted = true;

const { cache } = this.policies;

const readField: ReadFieldFunction = <V = StoreValue>(
fieldNameOrOptions: string | ReadFieldOptions,
from?: StoreObject | Reference,
Expand All @@ -134,10 +135,7 @@ export abstract class EntityStore implements NormalizedCache {
fieldName: fieldNameOrOptions,
from: from || makeReference(dataId),
} : fieldNameOrOptions,
{
toReference: this.toReference,
getFieldValue: this.getFieldValue,
},
this.getFieldValue,
);

Object.keys(storeObject).forEach(storeFieldName => {
Expand All @@ -153,8 +151,8 @@ export abstract class EntityStore implements NormalizedCache {
DELETE,
fieldName,
storeFieldName,
isReference,
toReference: this.toReference,
isReference: cache.isReference,
toReference: cache.toReference,
readField,
});
if (newValue === DELETE) newValue = void 0;
Expand Down Expand Up @@ -225,7 +223,9 @@ export abstract class EntityStore implements NormalizedCache {
// queries, even if no cache data was modified by the eviction,
// because queries may depend on computed fields with custom read
// functions, whose values are not stored in the EntityStore.
this.group.dirty(options.id, options.fieldName || "__exists");
if (options.fieldName || evicted) {
this.group.dirty(options.id, options.fieldName || "__exists");
}
}
return evicted;
}
Expand Down Expand Up @@ -346,20 +346,6 @@ export abstract class EntityStore implements NormalizedCache {
? this.get(objectOrReference.__ref, storeFieldName)
: objectOrReference && objectOrReference[storeFieldName]
) as SafeReadonly<T>;

// Bound function that converts an object with a __typename and primary
// key fields to a Reference object. Pass true for mergeIntoStore if you
// would also like this object to be persisted into the store.
public toReference: ToReferenceFunction = (object, mergeIntoStore) => {
const [id] = this.policies.identify(object);
if (id) {
const ref = makeReference(id);
if (mergeIntoStore) {
this.merge(id, object);
}
return ref;
}
}
}

export type FieldValueGetter = EntityStore["getFieldValue"];
Expand Down
28 changes: 27 additions & 1 deletion src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { dep, wrap } from 'optimism';
import { ApolloCache, Transaction } from '../core/cache';
import { Cache } from '../core/types/Cache';
import { addTypenameToDocument } from '../../utilities/graphql/transform';
import { StoreObject, Reference } from '../../utilities/graphql/storeUtils';
import { StoreObject, Reference, isReference, makeReference } from '../../utilities/graphql/storeUtils';
import {
ApolloReducerConfig,
NormalizedCacheObject,
Expand All @@ -22,6 +22,7 @@ import {
TypePolicies,
} from './policies';
import { hasOwn } from './helpers';
import { ToReferenceFunction, IsReferenceFunction } from '../core/types/common';

export interface InMemoryCacheConfig extends ApolloReducerConfig {
resultCaching?: boolean;
Expand Down Expand Up @@ -210,6 +211,31 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
return this.policies.identify(object)[0];
}

// Useful for determining if an object found in the EntityStore is a
// Reference or not. Pass true for mustBeValid to make isReference fail
// (return false) if the Reference does not refer to anything.
public isReference: IsReferenceFunction = (
candidate,
mustBeValid,
): candidate is Reference => {
return isReference(candidate) &&
Boolean(!mustBeValid || this.optimisticData.has(candidate.__ref));
};

// Bound function that converts an object with a __typename and primary
// key fields to a Reference object. Pass true for mergeIntoStore if you
// would also like this object to be persisted into the store.
public toReference: ToReferenceFunction = (object, mergeIntoStore) => {
const [id] = this.policies.identify(object);
if (id) {
const ref = makeReference(id);
if (mergeIntoStore) {
this.optimisticData.merge(id, object);
}
return ref;
}
};

public evict(options: Cache.EvictOptions): boolean {
if (!options.id) {
if (hasOwn.call(options, "id")) {
Expand Down
55 changes: 33 additions & 22 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { InMemoryCache } from './inMemoryCache';
import {
SafeReadonly,
FieldSpecifier,
IsReferenceFunction,
ToReferenceFunction,
ReadFieldFunction,
ReadFieldOptions,
Expand Down Expand Up @@ -132,7 +133,7 @@ export interface FieldFunctionOptions<
variables?: TVars;

// Utilities for dealing with { __ref } objects.
isReference: typeof isReference;
isReference: IsReferenceFunction;
toReference: ToReferenceFunction;

// A handy place to put field-specific data that you want to survive
Expand Down Expand Up @@ -495,7 +496,7 @@ export class Policies {

public readField<V = StoreValue>(
options: ReadFieldOptions,
context: ReadMergeContext,
getFieldValue: FieldValueGetter,
): SafeReadonly<V> | undefined {
const objectOrReference = options.from;
if (!objectOrReference) return;
Expand All @@ -504,14 +505,14 @@ export class Policies {
if (!nameOrField) return;

if (options.typename === void 0) {
const typename = context.getFieldValue<string>(
const typename = getFieldValue<string>(
objectOrReference, "__typename");
if (typename) options.typename = typename;
}

const storeFieldName = this.getStoreFieldName(options);
const fieldName = fieldNameFromStoreName(storeFieldName);
const existing = context.getFieldValue<V>(objectOrReference, storeFieldName);
const existing = getFieldValue<V>(objectOrReference, storeFieldName);
const policy = this.getFieldPolicy(options.typename, fieldName, false);
const read = policy && policy.read;

Expand All @@ -528,7 +529,8 @@ export class Policies {
objectOrReference,
options,
storage,
context,
getFieldValue,
options.variables,
)) as SafeReadonly<V>;
}

Expand All @@ -546,7 +548,8 @@ export class Policies {
public applyMerges<T extends StoreValue>(
existing: T | Reference,
incoming: T | FieldValueToBeMerged,
context: ReadMergeContext,
getFieldValue: FieldValueGetter,
variables: Record<string, any> | undefined,
storageKeys?: [string | StoreObject, string],
): T {
if (isFieldValueToBeMerged(incoming)) {
Expand Down Expand Up @@ -585,9 +588,10 @@ export class Policies {
{ typename: incoming.__typename,
fieldName,
field,
variables: context.variables },
variables },
storage,
context,
getFieldValue,
variables,
)) as T;
}

Expand All @@ -602,7 +606,8 @@ export class Policies {
// have custom read or merge functions.
void 0,
item,
context,
getFieldValue,
variables,
)) as T;
}

Expand All @@ -626,9 +631,10 @@ export class Policies {
Object.keys(i).forEach(storeFieldName => {
const incomingValue = i[storeFieldName];
const appliedValue = this.applyMerges(
context.getFieldValue(e, storeFieldName),
getFieldValue(e, storeFieldName),
incomingValue,
context,
getFieldValue,
variables,
// Avoid enabling options.storage when firstStorageKey is falsy,
// which implies no options.storage object has ever been created
// for a read/merge function for this field.
Expand All @@ -651,7 +657,6 @@ export class Policies {

export interface ReadMergeContext {
variables?: Record<string, any>;
toReference: ToReferenceFunction;
getFieldValue: FieldValueGetter;
}

Expand All @@ -660,9 +665,9 @@ function makeFieldFunctionOptions(
objectOrReference: StoreObject | Reference | undefined,
fieldSpec: FieldSpecifier,
storage: StorageType | null,
context: ReadMergeContext,
getFieldValue: FieldValueGetter,
variables: Record<string, any> | undefined,
): FieldFunctionOptions {
const { toReference, getFieldValue, variables } = context;
const storeFieldName = policies.getStoreFieldName(fieldSpec);
const fieldName = fieldNameFromStoreName(storeFieldName);
return {
Expand All @@ -671,8 +676,8 @@ function makeFieldFunctionOptions(
fieldName,
storeFieldName,
variables,
isReference,
toReference,
isReference: policies.cache.isReference,
toReference: policies.cache.toReference,
storage,
cache: policies.cache,

Expand All @@ -684,12 +689,17 @@ function makeFieldFunctionOptions(
typeof fieldNameOrOptions === "string" ? {
fieldName: fieldNameOrOptions,
from,
} : fieldNameOrOptions;
} : { ...fieldNameOrOptions };

if (void 0 === options.from) {
options.from = objectOrReference;
}

if (void 0 === options.variables) {
options.variables = variables;
}

return policies.readField<T>(options.from ? options : {
...options,
from: objectOrReference,
}, context);
return policies.readField<T>(options, getFieldValue);
},

mergeObjects(existing, incoming) {
Expand All @@ -710,7 +720,8 @@ function makeFieldFunctionOptions(
const applied = policies.applyMerges(
typesDiffer ? void 0 : existing,
incoming,
context,
getFieldValue,
variables,
);

if (
Expand Down
6 changes: 3 additions & 3 deletions src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export class StoreReader {
variables,
returnPartialData = true,
}: DiffQueryAgainstStoreOptions): Cache.DiffResult<T> {
const policies = this.config.cache.policies;
const { cache } = this.config;
const policies = cache.policies;

variables = {
...getDefaultValues(getQueryDefinition(query)),
Expand All @@ -142,7 +143,6 @@ export class StoreReader {
variables,
varString: JSON.stringify(variables),
fragmentMap: createFragmentMap(getFragmentDefinitions(query)),
toReference: store.toReference,
getFieldValue: store.getFieldValue,
path: [],
},
Expand Down Expand Up @@ -263,7 +263,7 @@ export class StoreReader {
field: selection,
variables: context.variables,
from: objectOrReference,
}, context);
}, store.getFieldValue);

const resultName = resultKeyNameFromField(selection);
context.path.push(resultName);
Expand Down
6 changes: 4 additions & 2 deletions src/cache/inmemory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
} from '../../utilities/graphql/storeUtils';
import { FieldValueGetter } from './entityStore';
import { KeyFieldsFunction } from './policies';
import { ToReferenceFunction, Modifier, Modifiers } from '../core/types/common';
import {
Modifier,
Modifiers,
} from '../core/types/common';
export { StoreObject, StoreValue, Reference }

export interface IdGetterObj extends Object {
Expand Down Expand Up @@ -55,7 +58,6 @@ export interface NormalizedCache {
release(rootId: string): number;

getFieldValue: FieldValueGetter;
toReference: ToReferenceFunction;
}

/**
Expand Down
Loading

0 comments on commit f6ec18f

Please sign in to comment.