Skip to content

Commit

Permalink
Merge pull request #5643 from apollographql/allow-evicting-specific-f…
Browse files Browse the repository at this point in the history
…ields

Support eviction of specific entity fields.
  • Loading branch information
benjamn authored Dec 3, 2019
2 parents 32f1201 + 5f153e8 commit b709e86
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 65 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
"maxSize": "23.75 kB"
"maxSize": "23.9 kB"
}
],
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/__snapshots__/client.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`@connection should run a query with the @connection directive and write
Object {
"ROOT_QUERY": Object {
"__typename": "Query",
"abc": Array [
"books:abc": Array [
Object {
"__typename": "Book",
"name": "abcd",
Expand All @@ -18,7 +18,7 @@ exports[`@connection should run a query with the connection directive and filter
Object {
"ROOT_QUERY": Object {
"__typename": "Query",
"abc({\\"order\\":\\"popularity\\"})": Array [
"books:abc({\\"order\\":\\"popularity\\"})": Array [
Object {
"__typename": "Book",
"name": "abcd",
Expand Down
6 changes: 5 additions & 1 deletion src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
): void;
public abstract diff<T>(query: Cache.DiffOptions): Cache.DiffResult<T>;
public abstract watch(watch: Cache.WatchOptions): () => void;
public abstract evict(dataId: string): boolean;
public abstract reset(): Promise<void>;

// If called with only one argument, removes the entire entity
// identified by dataId. If called with a fieldName as well, removes all
// fields of the identified entity whose store names match fieldName.
public abstract evict(dataId: string, fieldName?: string): boolean;

// intializer / offline / ssr API
/**
* Replaces existing state in the cache (if any) with the values expressed by
Expand Down
299 changes: 299 additions & 0 deletions src/cache/inmemory/__tests__/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,305 @@ describe('EntityStore', () => {
]);
});

it("allows evicting specific fields", () => {
const query: DocumentNode = gql`
query {
authorOfBook(isbn: $isbn) {
name
hobby
}
publisherOfBook(isbn: $isbn) {
name
yearOfFounding
}
}
`;

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
authorOfBook: {
keyArgs: ["isbn"],
},
},
},
Author: {
keyFields: ["name"],
},
Publisher: {
keyFields: ["name"],
},
},
});

const TedChiangData = {
__typename: "Author",
name: "Ted Chiang",
hobby: "video games",
};

const KnopfData = {
__typename: "Publisher",
name: "Alfred A. Knopf",
yearOfFounding: 1915,
};

cache.writeQuery({
query,
data: {
authorOfBook: TedChiangData,
publisherOfBook: KnopfData,
},
variables: {
isbn: "1529014514",
},
});

const justTedRootQueryData = {
__typename: "Query",
'authorOfBook:{"isbn":"1529014514"}': {
__ref: 'Author:{"name":"Ted Chiang"}',
},
// This storeFieldName format differs slightly from that of
// authorOfBook because we did not define keyArgs for the
// publisherOfBook field, so the legacy storeKeyNameFromField
// function was used instead.
'publisherOfBook({"isbn":"1529014514"})': {
__ref: 'Publisher:{"name":"Alfred A. Knopf"}',
},
};

expect(cache.extract()).toEqual({
ROOT_QUERY: justTedRootQueryData,
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': KnopfData,
});

const JennyOdellData = {
__typename: "Author",
name: "Jenny Odell",
hobby: "birding",
};

const MelvilleData = {
__typename: "Publisher",
name: "Melville House",
yearOfFounding: 2001,
};

cache.writeQuery({
query,
data: {
authorOfBook: JennyOdellData,
publisherOfBook: MelvilleData,
},
variables: {
isbn: "1760641790",
},
});

const justJennyRootQueryData = {
__typename: "Query",
'authorOfBook:{"isbn":"1760641790"}': {
__ref: 'Author:{"name":"Jenny Odell"}',
},
'publisherOfBook({"isbn":"1760641790"})': {
__ref: 'Publisher:{"name":"Melville House"}',
},
};

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...justTedRootQueryData,
...justJennyRootQueryData,
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': KnopfData,
'Author:{"name":"Jenny Odell"}': JennyOdellData,
'Publisher:{"name":"Melville House"}': MelvilleData,
});

const fullTedResult = cache.readQuery<any>({
query,
variables: {
isbn: "1529014514",
},
});

expect(fullTedResult).toEqual({
authorOfBook: TedChiangData,
publisherOfBook: KnopfData,
});

const fullJennyResult = cache.readQuery<any>({
query,
variables: {
isbn: "1760641790",
},
});

expect(fullJennyResult).toEqual({
authorOfBook: JennyOdellData,
publisherOfBook: MelvilleData,
});

cache.evict(
cache.identify({
__typename: "Publisher",
name: "Alfred A. Knopf",
}),
"yearOfFounding",
);

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...justTedRootQueryData,
...justJennyRootQueryData,
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': {
__typename: "Publisher",
name: "Alfred A. Knopf",
// yearOfFounding has been removed
},
'Author:{"name":"Jenny Odell"}': JennyOdellData,
'Publisher:{"name":"Melville House"}': MelvilleData,
});

// Nothing to garbage collect yet.
expect(cache.gc()).toEqual([]);

cache.evict(cache.identify({
__typename: "Publisher",
name: "Melville House",
}));

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...justTedRootQueryData,
...justJennyRootQueryData,
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': {
__typename: "Publisher",
name: "Alfred A. Knopf",
},
'Author:{"name":"Jenny Odell"}': JennyOdellData,
// Melville House has been removed
});

cache.evict("ROOT_QUERY", "publisherOfBook");

function withoutPublisherOfBook(obj: object) {
const clean = { ...obj };
Object.keys(obj).forEach(key => {
if (key.startsWith("publisherOfBook")) {
delete clean[key];
}
});
return clean;
}

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...withoutPublisherOfBook(justTedRootQueryData),
...withoutPublisherOfBook(justJennyRootQueryData),
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': {
__typename: "Publisher",
name: "Alfred A. Knopf",
},
'Author:{"name":"Jenny Odell"}': JennyOdellData,
});

expect(cache.gc()).toEqual([
'Publisher:{"name":"Alfred A. Knopf"}',
]);

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...withoutPublisherOfBook(justTedRootQueryData),
...withoutPublisherOfBook(justJennyRootQueryData),
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Author:{"name":"Jenny Odell"}': JennyOdellData,
});

const partialTedResult = cache.diff<any>({
query,
returnPartialData: true,
optimistic: false, // required but not important
variables: {
isbn: "1529014514",
},
});
expect(partialTedResult.complete).toBe(false);
expect(partialTedResult.result).toEqual({
authorOfBook: TedChiangData,
});
// The result caching system preserves the referential identity of
// unchanged nested result objects.
expect(
partialTedResult.result.authorOfBook,
).toBe(fullTedResult.authorOfBook);

const partialJennyResult = cache.diff<any>({
query,
returnPartialData: true,
optimistic: true, // required but not important
variables: {
isbn: "1760641790",
},
});
expect(partialJennyResult.complete).toBe(false);
expect(partialJennyResult.result).toEqual({
authorOfBook: JennyOdellData,
});
// The result caching system preserves the referential identity of
// unchanged nested result objects.
expect(
partialJennyResult.result.authorOfBook,
).toBe(fullJennyResult.authorOfBook);

const tedWithoutHobby = {
__typename: "Author",
name: "Ted Chiang",
};

cache.evict(
cache.identify(tedWithoutHobby),
"hobby",
);

expect(cache.diff<any>({
query,
returnPartialData: true,
optimistic: false, // required but not important
variables: {
isbn: "1529014514",
},
})).toEqual({
complete: false,
result: {
authorOfBook: tedWithoutHobby,
},
});

cache.evict("ROOT_QUERY", "authorOfBook");
expect(cache.gc().sort()).toEqual([
'Author:{"name":"Jenny Odell"}',
'Author:{"name":"Ted Chiang"}',
]);
expect(cache.extract()).toEqual({
ROOT_QUERY: {
// Everything else has been removed.
__typename: "Query",
},
});
});

it("supports cache.identify(object)", () => {
const queryWithAliases: DocumentNode = gql`
query {
Expand Down
7 changes: 5 additions & 2 deletions src/cache/inmemory/__tests__/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ describe('reading from the store', () => {
it('properly handles the @connection directive', () => {
const store = defaultNormalizedCacheFactory({
ROOT_QUERY: {
abc: [
'books:abc': [
{
name: 'efgh',
},
Expand Down Expand Up @@ -778,6 +778,9 @@ describe('reading from the store', () => {
Query: {
fields: {
books: {
// Even though we're returning an arbitrary string here,
// the InMemoryCache will ensure the actual key begins
// with "books".
keyArgs: () => "abc",
},
},
Expand All @@ -788,7 +791,7 @@ describe('reading from the store', () => {

const store = defaultNormalizedCacheFactory({
ROOT_QUERY: {
abc: [
"books:abc": [
{
name: 'efgh',
},
Expand Down
Loading

0 comments on commit b709e86

Please sign in to comment.