diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index c5898b979fa..9b08248bae8 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2942,19 +2942,6 @@ describe('@connection', () => { checkLastResult(abResults, a456bOyez); checkLastResult(cResults, { c: "see" }); - cache.modify({ - c(value) { - expect(value).toBe("see"); - return "saw"; - }, - }); - await wait(); - - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "saw" }); - client.cache.evict("ROOT_QUERY", "c"); await wait(); @@ -2991,7 +2978,6 @@ describe('@connection', () => { expect(cResults).toEqual([ {}, { c: "see" }, - { c: "saw" }, {}, ]); diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts index e7ba416fe1b..0f7a94b243c 100644 --- a/src/__tests__/local-state/resolvers.ts +++ b/src/__tests__/local-state/resolvers.ts @@ -546,19 +546,22 @@ describe('Writing cache data from resolvers', () => { resolvers: { Mutation: { start() { + const obj = { + __typename: 'Object', + id: 'uniqueId', + field: 1, + }; + cache.writeQuery({ query, - data: { - obj: { field: 1, id: 'uniqueId', __typename: 'Object' }, - }, + data: { obj }, }); - cache.modify({ - field(value) { - expect(value).toBe(1); - return 2; - }, - }, 'Object:uniqueId'); + cache.writeFragment({ + id: cache.identify(obj)!, + fragment: gql`fragment Field on Object { field }`, + data: { field: 2 }, + }); return { start: true }; }, @@ -574,7 +577,7 @@ describe('Writing cache data from resolvers', () => { }); }); - it('should not overwrite __typename when writing to the cache with an id', () => { + itAsync('should not overwrite __typename when writing to the cache with an id', (resolve, reject) => { const query = gql` { obj @client { @@ -600,22 +603,35 @@ describe('Writing cache data from resolvers', () => { resolvers: { Mutation: { start() { + const obj = { + __typename: 'Object', + id: 'uniqueId', + field: { + __typename: 'Field', + field2: 1, + }, + }; + cache.writeQuery({ query, + data: { obj }, + }); + + cache.writeFragment({ + id: cache.identify(obj)!, + fragment: gql`fragment FieldField2 on Object { + field { + field2 + } + }`, data: { - obj: { - field: { field2: 1, __typename: 'Field' }, - id: 'uniqueId', - __typename: 'Object', + field: { + __typename: 'Field', + field2: 2, }, }, }); - cache.modify({ - field(value: { field2: number }) { - expect(value.field2).toBe(1); - return { ...value, field2: 2 }; - }, - }, 'Object:uniqueId'); + return { start: true }; }, }, @@ -628,8 +644,7 @@ describe('Writing cache data from resolvers', () => { .then(({ data }: any) => { expect(data.obj.__typename).toEqual('Object'); expect(data.obj.field.__typename).toEqual('Field'); - }) - .catch(e => console.log(e)); + }).then(resolve, reject); }); }); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index ff0415d9b73..759bf9f7ebd 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -5,7 +5,6 @@ import { getFragmentQueryDocument } from '../../utilities/graphql/fragments'; import { StoreObject } from '../../utilities/graphql/storeUtils'; import { DataProxy } from './types/DataProxy'; import { Cache } from './types/Cache'; -import { Modifier, Modifiers } from './types/common'; export type Transaction = (c: ApolloCache) => void; @@ -78,14 +77,6 @@ export abstract class ApolloCache implements DataProxy { public identify(object: StoreObject): string | undefined { return; } - - public modify( - modifiers: Modifier | Modifiers, - dataId?: string, - optimistic?: boolean, - ): boolean { - return false; - } public gc(): string[] { return []; diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts index 824539e5440..c2296308847 100644 --- a/src/cache/inmemory/__tests__/cache.ts +++ b/src/cache/inmemory/__tests__/cache.ts @@ -4,6 +4,7 @@ import { stripSymbols } from '../../../utilities/testing/stripSymbols'; import { cloneDeep } from '../../../utilities/common/cloneDeep'; import { makeReference, Reference } from '../../../core'; import { InMemoryCache, InMemoryCacheConfig } from '../inMemoryCache'; +import { fieldNameFromStoreName } from '../helpers'; disableFragmentWarnings(); @@ -1538,268 +1539,6 @@ describe("InMemoryCache#broadcastWatches", function () { }); describe("InMemoryCache#modify", () => { - it("should work with single modifier function", () => { - const cache = new InMemoryCache; - const query = gql` - query { - a - b - c - } - `; - - cache.writeQuery({ - query, - data: { - a: 0, - b: 0, - c: 0, - }, - }); - - const resultBeforeModify = cache.readQuery({ query }); - expect(resultBeforeModify).toEqual({ a: 0, b: 0, c: 0 }); - - cache.modify((value, { fieldName }) => { - switch (fieldName) { - case "a": return value + 1; - case "b": return value - 1; - default: return value; - } - }); - - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: 1, - b: -1, - c: 0, - }, - }); - - const resultAfterModify = cache.readQuery({ query }); - expect(resultAfterModify).toEqual({ a: 1, b: -1, c: 0 }); - }); - - it("should work with multiple modifier functions", () => { - const cache = new InMemoryCache; - const query = gql` - query { - a - b - c - } - `; - - cache.writeQuery({ - query, - data: { - a: 0, - b: 0, - c: 0, - }, - }); - - const resultBeforeModify = cache.readQuery({ query }); - expect(resultBeforeModify).toEqual({ a: 0, b: 0, c: 0 }); - - let checkedTypename = false; - cache.modify({ - a(value) { return value + 1 }, - b(value) { return value - 1 }, - __typename(t: string, { readField }) { - expect(t).toBe("Query"); - expect(readField("c")).toBe(0); - checkedTypename = true; - return t; - }, - }); - expect(checkedTypename).toBe(true); - - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: 1, - b: -1, - c: 0, - }, - }); - - const resultAfterModify = cache.readQuery({ query }); - expect(resultAfterModify).toEqual({ a: 1, b: -1, c: 0 }); - }); - - it("should allow deletion using details.DELETE", () => { - const cache = new InMemoryCache({ - typePolicies: { - Book: { - keyFields: ["isbn"], - }, - Author: { - keyFields: ["name"], - }, - }, - }); - - const query = gql` - query { - currentlyReading { - title - isbn - author { - name - yearOfBirth - } - } - } - `; - - const currentlyReading = { - __typename: "Book", - isbn: "147670032X", - title: "Why We're Polarized", - author: { - __typename: "Author", - name: "Ezra Klein", - yearOfBirth: 1983, - }, - }; - - cache.writeQuery({ - query, - data: { - currentlyReading, - } - }); - - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - currentlyReading: { - __ref: 'Book:{"isbn":"147670032X"}', - }, - }, - 'Book:{"isbn":"147670032X"}': { - __typename: "Book", - isbn: "147670032X", - author: { - __ref: 'Author:{"name":"Ezra Klein"}', - }, - title: "Why We're Polarized", - }, - 'Author:{"name":"Ezra Klein"}': { - __typename: "Author", - name: "Ezra Klein", - yearOfBirth: 1983, - }, - }); - - const authorId = cache.identify(currentlyReading.author)!; - expect(authorId).toBe('Author:{"name":"Ezra Klein"}'); - - cache.modify({ - yearOfBirth(yob) { - return yob + 1; - }, - }, authorId); - - const yobResult = cache.readFragment({ - id: authorId, - fragment: gql`fragment YOB on Author { yearOfBirth }`, - }); - - expect(yobResult).toEqual({ - __typename: "Author", - yearOfBirth: 1984, - }); - - const bookId = cache.identify(currentlyReading)!; - - // Modifying the Book in order to modify the Author is fancier than - // necessary, but we want fancy use cases to work, too. - cache.modify({ - author(author: Reference, { readField }) { - expect(readField("title")).toBe("Why We're Polarized"); - expect(readField("name", author)).toBe("Ezra Klein"); - cache.modify({ - yearOfBirth(yob, { DELETE }) { - expect(yob).toBe(1984); - return DELETE; - }, - }, cache.identify({ - __typename: readField("__typename", author), - name: readField("name", author), - })); - return author; - } - }, bookId); - - const snapshotWithoutYOB = cache.extract(); - expect(snapshotWithoutYOB[authorId]!.yearOfBirth).toBeUndefined(); - expect("yearOfBirth" in snapshotWithoutYOB[authorId]!).toBe(false); - expect(snapshotWithoutYOB).toEqual({ - ROOT_QUERY: { - __typename: "Query", - currentlyReading: { - __ref: 'Book:{"isbn":"147670032X"}', - }, - }, - 'Book:{"isbn":"147670032X"}': { - __typename: "Book", - isbn: "147670032X", - author: { - __ref: 'Author:{"name":"Ezra Klein"}', - }, - title: "Why We're Polarized", - }, - 'Author:{"name":"Ezra Klein"}': { - __typename: "Author", - name: "Ezra Klein", - // yearOfBirth is gone now - }, - }); - - // Delete the whole Book. - cache.modify((_, { DELETE }) => DELETE, bookId); - - const snapshotWithoutBook = cache.extract(); - expect(snapshotWithoutBook[bookId]).toBeUndefined(); - expect(bookId in snapshotWithoutBook).toBe(false); - expect(snapshotWithoutBook).toEqual({ - ROOT_QUERY: { - __typename: "Query", - currentlyReading: { - __ref: 'Book:{"isbn":"147670032X"}', - }, - }, - 'Author:{"name":"Ezra Klein"}': { - __typename: "Author", - name: "Ezra Klein", - }, - }); - - // Delete all fields of the Author, which also removes the object. - cache.modify({ - __typename(_, { DELETE }) { return DELETE }, - name(_, { DELETE }) { return DELETE }, - }, authorId); - - const snapshotWithoutAuthor = cache.extract(); - expect(snapshotWithoutAuthor[authorId]).toBeUndefined(); - expect(authorId in snapshotWithoutAuthor).toBe(false); - expect(snapshotWithoutAuthor).toEqual({ - ROOT_QUERY: { - __typename: "Query", - currentlyReading: { - __ref: 'Book:{"isbn":"147670032X"}', - }, - }, - }); - - cache.modify((_, { DELETE }) => DELETE); - expect(cache.extract()).toEqual({}); - }); - it("can remove specific items from paginated lists", () => { const cache = new InMemoryCache({ typePolicies: { @@ -1908,21 +1647,59 @@ describe("InMemoryCache#modify", () => { }, }); - cache.modify({ - comments(comments: Reference[], { readField }) { - debugger; - expect(Object.isFrozen(comments)).toBe(true); - expect(comments.length).toBe(3); - const filtered = comments.filter(comment => { - return readField("id", comment) !== "c1"; - }); - expect(filtered.length).toBe(2); - return filtered; - }, - }, cache.identify({ + const threadId = cache.identify({ __typename: "Thread", tid: 123, - })); + })!; + + const threadCommentsFragment = gql` + fragment Comments on Thread { + comments(offset: $offset, limit: $limit) { + id + text + } + } + `; + + const threadWithComments = cache.readFragment<{ + comments: any[], + }>({ + id: threadId, + fragment: threadCommentsFragment, + variables: { + offset: 0, + // There are only three comments at this point, but let's pretend + // we don't know that. + limit: 10, + }, + })!; + + // First evict the comments field, so the Thread.comments merge + // function does not try to combine the existing (unfiltered) comments + // list with the incoming (filtered) comments list when we call + // writeFragment below. + expect(cache.evict({ + id: threadId, + fieldName: "comments", + broadcast: false, + })).toBe(true); + + const commentsWithoutC1 = + threadWithComments.comments.filter(comment => { + return comment.id !== "c1"; + }); + + cache.writeFragment({ + id: threadId, + fragment: threadCommentsFragment, + data: { + comments: commentsWithoutC1, + }, + variables: { + offset: 0, + limit: commentsWithoutC1.length, + }, + }); expect(cache.gc()).toEqual(['Comment:{"id":"c1"}']); @@ -1954,47 +1731,6 @@ describe("InMemoryCache#modify", () => { }); }); - it("should not revisit deleted fields", () => { - const cache = new InMemoryCache; - const query = gql`query { a b c }`; - - cache.recordOptimisticTransaction(cache => { - cache.writeQuery({ - query, - data: { - a: 1, - b: 2, - c: 3, - }, - }) - }, "transaction"); - - cache.modify({ - b(value, { DELETE }) { - expect(value).toBe(2); - return DELETE; - }, - }, "ROOT_QUERY", true); - - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: 1, - c: 3, - }, - }); - - cache.modify((value, { fieldName }) => { - expect(fieldName).not.toBe("b"); - if (fieldName === "a") expect(value).toBe(1); - if (fieldName === "c") expect(value).toBe(3); - }, "ROOT_QUERY", true); - - cache.removeOptimistic("transaction"); - - expect(cache.extract(true)).toEqual({}); - }); - it("should broadcast watches for queries with changed fields", () => { const cache = new InMemoryCache; const queryA = gql`{ a { value } }`; @@ -2082,25 +1818,28 @@ describe("InMemoryCache#modify", () => { expect(aResults).toEqual([a123]); expect(bResults).toEqual([b321]); - const aId = cache.identify({ __typename: "A", id: 1 }); - const bId = cache.identify({ __typename: "B", id: 1 }); + const aId = cache.identify({ __typename: "A", id: 1 })!; + expect(aId).toBe("A:1"); - cache.modify({ - value(x: number) { - return x + 1; - }, - }, aId); + const bId = cache.identify({ __typename: "B", id: 1 })!; + expect(bId).toBe("B:1"); + + cache.writeFragment({ + id: aId, + fragment: gql`fragment AValue on A { value }`, + data: { value: a123.result.a.value + 1 }, + }); const a124 = makeResult("A", 124); expect(aResults).toEqual([a123, a124]); expect(bResults).toEqual([b321]); - cache.modify({ - value(x: number) { - return x + 1; - }, - }, bId); + cache.writeFragment({ + id: bId, + fragment: gql`fragment BValue on B { value }`, + data: { value: b321.result.b.value + 1 }, + }); const b322 = makeResult("B", 322); @@ -2184,40 +1923,22 @@ describe("InMemoryCache#modify", () => { expect(cache.extract()).toEqual(fullSnapshot); function check(isbnToDelete?: string) { - let bookCount = 0; - - cache.modify({ - book(book: Reference, { - fieldName, - storeFieldName, - isReference, - readField, - DELETE, - }) { - expect(fieldName).toBe("book"); - expect(isReference(book)).toBe(true); - expect(typeof readField("title", book)).toBe("string"); - expect(readField("__typename", book)).toBe("Book"); - - const parts = storeFieldName.split(":"); - expect(parts.shift()).toBe("book"); - const keyArgs = JSON.parse(parts.join(":")); - expect(typeof keyArgs.isbn).toBe("string"); - expect(Object.keys(keyArgs)).toEqual(["isbn"]); - - expect(readField("isbn", book)).toBe(keyArgs.isbn); - - if (isbnToDelete === keyArgs.isbn) { - return DELETE; - } - - ++bookCount; - - return book; - }, - }); + if (isbnToDelete) { + cache.evict({ + id: "ROOT_QUERY", + fieldName: "book", + args: { isbn: isbnToDelete }, + }); + } - return bookCount; + return Object.keys( + cache.extract().ROOT_QUERY! + ).reduce((count: number, storeFieldName: string) => { + if ("book" === fieldNameFromStoreName(storeFieldName)) { + ++count; + } + return count; + }, 0); } // No change from repeatedly calling check(). diff --git a/src/cache/inmemory/__tests__/entityStore.ts b/src/cache/inmemory/__tests__/entityStore.ts index 4e20c5bda6d..089a68b3e54 100644 --- a/src/cache/inmemory/__tests__/entityStore.ts +++ b/src/cache/inmemory/__tests__/entityStore.ts @@ -1917,51 +1917,5 @@ describe('EntityStore', () => { title: "The Cuckoo's Calling", }, }); - - cache.modify({ - 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 as Reference)).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 as Reference)).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("’"); - }, - }, cuckoosCallingId); - - 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", - }, - }); }); }); diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 22d797c924c..b1a35ba8d1c 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -6,7 +6,6 @@ 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 { @@ -151,25 +150,6 @@ export class InMemoryCache extends ApolloCache { } } - public modify( - modifiers: Modifier | Modifiers, - dataId = "ROOT_QUERY", - optimistic = false, - ): boolean { - if (typeof modifiers === "string") { - // In beta testing of Apollo Client 3, the dataId parameter used to - // come before the modifiers. The type system should complain about - // this, but it doesn't have to be fatal if we fix it here. - [modifiers, dataId] = [dataId as any, modifiers]; - } - const store = optimistic ? this.optimisticData : this.data; - if (store.modify(dataId, modifiers)) { - this.broadcastWatches(); - return true; - } - return false; - } - public diff(options: Cache.DiffOptions): Cache.DiffResult { return this.storeReader.diffQueryAgainstStore({ store: options.optimistic ? this.optimisticData : this.data, diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts index fccedfcda8c..f6230add07f 100644 --- a/src/cache/inmemory/types.ts +++ b/src/cache/inmemory/types.ts @@ -1,7 +1,6 @@ import { DocumentNode } from 'graphql'; import { Transaction } from '../core/cache'; -import { Modifier, Modifiers } from '../core/types/common'; import { StoreValue, StoreObject } from '../../utilities/graphql/storeUtils'; import { FieldValueGetter, ToReferenceFunction } from './entityStore'; import { KeyFieldsFunction } from './policies'; @@ -25,7 +24,6 @@ export interface NormalizedCache { has(dataId: string): boolean; get(dataId: string, fieldName: string): StoreValue; merge(dataId: string, incoming: StoreObject): void; - modify(dataId: string, modifiers: Modifier | Modifiers): boolean; clear(): void; // non-Map elements: