Skip to content

Commit

Permalink
Merge pull request #5970 from apollographql/persistent-options.toRefe…
Browse files Browse the repository at this point in the history
…rence

Support toReference(obj, true) to persist obj into cache.
  • Loading branch information
benjamn committed Feb 20, 2020
2 parents 0540f98 + 5600ca6 commit 4cca16e
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 77 deletions.
7 changes: 6 additions & 1 deletion docs/source/caching/cache-field-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,12 @@ interface FieldFunctionOptions {

// Utilities for handling { __ref: string } references.
isReference(obj: any): obj is Reference;
toReference(obj: StoreObject): Reference;
// Returns a Reference object if obj can be identified, which requires,
// at minimum, a __typename and any necessary key fields. If true is
// passed for the optional mergeIntoStore argument, the object's fields
// will also be persisted into the cache, which can be useful to ensure
// the Reference actually refers to data stored in the cache.
toReference(obj: StoreObject, mergeIntoStore?: boolean): Reference;

// Helper function for reading other fields within the current object.
// If a foreign object or reference is provided, the field will be read
Expand Down
11 changes: 3 additions & 8 deletions src/cache/core/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { SelectionSetNode } from 'graphql';

import {
isReference,
StoreValue,
StoreObject,
Reference
} from '../../../utilities/graphql/storeUtils';
import { FragmentMap } from '../../../utilities/graphql/fragments';

import { ToReferenceFunction } from '../../inmemory/entityStore';

// 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
Expand All @@ -22,11 +21,7 @@ export type Modifier<T> = (value: T, details: {
fieldName: string;
storeFieldName: string;
isReference: typeof isReference;
toReference(
object: StoreObject,
selectionSet?: SelectionSetNode,
fragmentMap?: FragmentMap,
): Reference;
toReference: ToReferenceFunction;
readField<V = StoreValue>(
fieldName: string,
objOrRef?: StoreObject | Reference,
Expand Down
278 changes: 278 additions & 0 deletions src/cache/inmemory/__tests__/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DocumentNode, FieldNode } from 'graphql';
import { Policies } from '../policies';
import { StoreObject } from '../types';
import { ApolloCache } from '../../core/cache';
import { Reference } from '../../../utilities/graphql/storeUtils';

describe('EntityStore', () => {
it('should support result caching if so configured', () => {
Expand Down Expand Up @@ -1357,4 +1358,281 @@ describe('EntityStore', () => {
},
});
});

it("supports toReference(obj, true) to persist obj", () => {
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
book(_, {
args,
toReference,
readField,
}) {
const ref = toReference({
__typename: "Book",
isbn: args.isbn,
}, true);

expect(readField("__typename", ref)).toEqual("Book");
const isbn = readField<string>("isbn", ref);
expect(isbn).toEqual(args.isbn);
expect(readField("title", ref)).toBe(titlesByISBN[isbn]);

return ref;
},

books: {
merge(existing: Reference[] = [], incoming: any[], {
isReference,
toReference,
readField,
}) {
incoming.forEach(book => {
expect(isReference(book)).toBe(false);
expect(book.__typename).toBeUndefined();
});

const refs = incoming.map(book => toReference({
__typename: "Book",
title: titlesByISBN[book.isbn],
...book,
}, true));

refs.forEach((ref, i) => {
expect(isReference(ref)).toBe(true);
expect(readField("__typename", ref)).toBe("Book");
const isbn = readField<string>("isbn", ref);
expect(typeof isbn).toBe("string");
expect(isbn).toBe(readField("isbn", incoming[i]));
});

return [...existing, ...refs];
},
},
},
},

Book: {
keyFields: ["isbn"],
},
},
});

const booksQuery = gql`
query {
books {
isbn
}
}
`;

const bookQuery = gql`
query {
book(isbn: $isbn) {
isbn
title
}
}
`;

const titlesByISBN = {
9781451673319: "Fahrenheit 451",
1603589082: "Eager",
1760641790: "How To Do Nothing",
};

cache.writeQuery({
query: booksQuery,
data: {
books: [{
// Note: intentionally omitting __typename:"Book" here.
isbn: "9781451673319",
}, {
isbn: "1603589082",
}],
},
});

const twoBookSnapshot = {
ROOT_QUERY: {
__typename: "Query",
books: [
{ __ref: 'Book:{"isbn":"9781451673319"}' },
{ __ref: 'Book:{"isbn":"1603589082"}' },
],
},
'Book:{"isbn":"9781451673319"}': {
__typename: "Book",
isbn: "9781451673319",
title: "Fahrenheit 451",
},
'Book:{"isbn":"1603589082"}': {
__typename: "Book",
isbn: "1603589082",
title: "Eager",
},
};

// Check that the __typenames were appropriately added.
expect(cache.extract()).toEqual(twoBookSnapshot);

cache.writeQuery({
query: booksQuery,
data: {
books: [{
isbn: "1760641790",
}],
},
});

const threeBookSnapshot = {
...twoBookSnapshot,
ROOT_QUERY: {
...twoBookSnapshot.ROOT_QUERY,
books: [
...twoBookSnapshot.ROOT_QUERY.books,
{ __ref: 'Book:{"isbn":"1760641790"}' },
],
},
'Book:{"isbn":"1760641790"}': {
__typename: "Book",
isbn: "1760641790",
title: "How To Do Nothing",
},
};

expect(cache.extract()).toEqual(threeBookSnapshot);

const howToDoNothingResult = cache.readQuery({
query: bookQuery,
variables: {
isbn: "1760641790",
},
});

expect(howToDoNothingResult).toEqual({
book: {
__typename: "Book",
isbn: "1760641790",
title: "How To Do Nothing",
},
});

// Check that reading the query didn't change anything.
expect(cache.extract()).toEqual(threeBookSnapshot);

const f451Result = cache.readQuery({
query: bookQuery,
variables: {
isbn: "9781451673319",
},
});

expect(f451Result).toEqual({
book: {
__typename: "Book",
isbn: "9781451673319",
title: "Fahrenheit 451",
},
});

const cuckoosCallingDiffResult = cache.diff({
query: bookQuery,
optimistic: true,
variables: {
isbn: "031648637X",
},
});

expect(cuckoosCallingDiffResult).toEqual({
complete: false,
result: {
book: {
__typename: "Book",
isbn: "031648637X",
},
},
});

expect(cache.extract()).toEqual({
...threeBookSnapshot,
// This book was added as a side effect of the read function.
'Book:{"isbn":"031648637X"}': {
__typename: "Book",
isbn: "031648637X",
},
});

const cuckoosCallingId = cache.identify({
__typename: "Book",
isbn: "031648637X",
});

expect(cuckoosCallingId).toBe('Book:{"isbn":"031648637X"}');

cache.writeQuery({
id: cuckoosCallingId,
query: gql`{ title }`,
data: {
title: "The Cuckoo's Calling",
},
});

expect(cache.extract()).toEqual({
...threeBookSnapshot,
// This book was added as a side effect of the read function.
'Book:{"isbn":"031648637X"}': {
__typename: "Book",
isbn: "031648637X",
title: "The Cuckoo's Calling",
},
});

cache.modify(cuckoosCallingId, {
title(title: string, {
isReference,
toReference,
readField,
}) {
const book = {
__typename: "Book",
isbn: readField("isbn"),
author: "J.K. Rowling",
};

// By not passing true as the second argument to toReference, we
// get back a Reference object, but the book.author field is not
// persisted into the store.
const refWithoutAuthor = toReference(book);
expect(isReference(refWithoutAuthor)).toBe(true);
expect(readField("author", refWithoutAuthor)).toBeUndefined();

// Update this very Book entity before we modify its title.
// Passing true for the second argument causes the extra
// book.author field to be persisted into the store.
const ref = toReference(book, true);
expect(isReference(ref)).toBe(true);
expect(readField("author", ref)).toBe("J.K. Rowling");

// In fact, readField doesn't need the ref if we're reading from
// the same entity that we're modifying.
expect(readField("author")).toBe("J.K. Rowling");

// Typography matters!
return title.split("'").join("’");
},
});

expect(cache.extract()).toEqual({
...threeBookSnapshot,
// This book was added as a side effect of the read function.
'Book:{"isbn":"031648637X"}': {
__typename: "Book",
isbn: "031648637X",
title: "The Cuckoo’s Calling",
author: "J.K. Rowling",
},
});
});
});
4 changes: 2 additions & 2 deletions src/cache/inmemory/__tests__/writeToStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ describe('writing to the store', () => {
const expStore = defaultNormalizedCacheFactory({
ROOT_QUERY: {
__typename: 'Query',
author: policies.toReference(data.author),
author: makeReference(policies.identify(data.author)),
},
[policies.identify(data.author)!]: {
firstName: data.author.firstName,
Expand Down Expand Up @@ -1288,7 +1288,7 @@ describe('writing to the store', () => {
const expStore = defaultNormalizedCacheFactory({
ROOT_QUERY: {
__typename: 'Query',
author: policies.toReference(data.author),
author: makeReference(policies.identify(data.author)),
},
[policies.identify(data.author)!]: {
__typename: data.author.__typename,
Expand Down
19 changes: 18 additions & 1 deletion src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export abstract class EntityStore implements NormalizedCache {
fieldName,
storeFieldName,
isReference,
toReference: this.policies.toReference,
toReference: this.toReference,
readField,
});
if (newValue === DELETE) newValue = void 0;
Expand Down Expand Up @@ -310,8 +310,25 @@ 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 = (
object: StoreObject,
mergeIntoStore?: boolean,
) => {
const id = this.policies.identify(object);
const ref = id && makeReference(id);
if (ref && mergeIntoStore) {
this.merge(id, object);
}
return ref;
}
}

export type ToReferenceFunction = EntityStore["toReference"];

export type FieldValueGetter = EntityStore["getFieldValue"];

// A single CacheGroup represents a set of one or more EntityStore objects,
Expand Down
Loading

0 comments on commit 4cca16e

Please sign in to comment.