Skip to content

Commit

Permalink
add support for eviction of field names by arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
danReynolds committed Apr 16, 2020
1 parent c69f135 commit 28b139e
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"watch": "tsc-watch --onSuccess \"npm run postbuild\"",
"clean": "rimraf -r dist coverage lib",
"test": "jest --config ./config/jest.config.js",
"test:debug": "BABEL_ENV=server node --inspect-brk node_modules/.bin/jest --config ./config/jest.config.js --runInBand",
"test:ci": "npm run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit",
"test:watch": "jest --config ./config/jest.config.js --watch",
"bundle": "rollup -c ./config/rollup.config.js",
Expand Down
124 changes: 124 additions & 0 deletions src/cache/inmemory/__tests__/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,130 @@ describe('EntityStore', () => {
});
});

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

const cache = new InMemoryCache();

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

const IsaacAsimovData = {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
};

const JamesCoreyData = {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
};

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

cache.writeQuery({
query,
data: {
authorOfBook: IsaacAsimovData,
},
variables: {
isbn: "2",
},
});

cache.writeQuery({
query,
data: {
authorOfBook: JamesCoreyData,
},
variables: {},
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"1\"})": {
__typename: "Author",
name: "Ted Chiang",
hobby: "video games",
},
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
"authorOfBook({})": {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
}
},
});

cache.evict('ROOT_QUERY', 'authorOfBook', { isbn: "1" });

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
"authorOfBook({})": {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
}
},
});

cache.evict('ROOT_QUERY', 'authorOfBook', { isbn: '3' });

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
"authorOfBook({})": {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
}
},
});

cache.evict('ROOT_QUERY', 'authorOfBook', {});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
},
});
});

it("supports cache.identify(object)", () => {
const queryWithAliases: DocumentNode = gql`
query {
Expand Down
20 changes: 12 additions & 8 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export abstract class EntityStore implements NormalizedCache {
if (fieldValue === void 0) return;
const modify: Modifier<StoreValue> = typeof modifiers === "function"
? modifiers
: modifiers[fieldName];
: modifiers[storeFieldName] || modifiers[fieldName];
if (modify) {
let newValue = modify === delModifier ? DELETE :
modify(maybeDeepFreeze(fieldValue), {
Expand Down Expand Up @@ -176,20 +176,24 @@ export abstract class EntityStore implements NormalizedCache {
// If called with only one argument, removes the entire entity
// identified by dataId. If called with a fieldName as well, removes all
// fields of that entity whose names match fieldName according to the
// fieldNameFromStoreName helper function.
public delete(dataId: string, fieldName?: string) {
return this.modify(dataId, fieldName ? {
[fieldName]: delModifier,
// fieldNameFromStoreName helper function. If called with a fieldName
// and variables, removes all fields of that entity whose names match fieldName
// and whose arguments when cached exactly match the variables passed.
public delete(dataId: string, fieldName?: string, variables?: Record<string, any>) {
const storeFieldName = fieldName && variables ?
this.policies.getStoreFieldName(dataId, fieldName, variables) : fieldName;
return this.modify(dataId, storeFieldName ? {
[storeFieldName]: delModifier,
} : delModifier);
}

public evict(dataId: string, fieldName?: string): boolean {
public evict(dataId: string, fieldName?: string, variables?: Record<string, any>): boolean {
let evicted = false;
if (hasOwn.call(this.data, dataId)) {
evicted = this.delete(dataId, fieldName);
evicted = this.delete(dataId, fieldName, variables);
}
if (this instanceof Layer) {
evicted = this.parent.evict(dataId, fieldName) || evicted;
evicted = this.parent.evict(dataId, fieldName, variables) || evicted;
}
// Always invalidate the field to trigger rereading of watched
// queries, even if no cache data was modified by the eviction,
Expand Down
4 changes: 2 additions & 2 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
return this.policies.identify(object)[0];
}

public evict(dataId: string, fieldName?: string): boolean {
const evicted = this.optimisticData.evict(dataId, fieldName);
public evict(dataId: string, fieldName?: string, variables?: Record<string, any>): boolean {
const evicted = this.optimisticData.evict(dataId, fieldName, variables);
this.broadcastWatches();
return evicted;
}
Expand Down
4 changes: 3 additions & 1 deletion src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getFragmentFromSelection,
} from '../../utilities/graphql/fragments';
import {
fieldNodeFromName,
isField,
getTypenameFromResult,
storeKeyNameFromField,
Expand Down Expand Up @@ -430,9 +431,10 @@ export class Policies {

public getStoreFieldName(
typename: string | undefined,
field: FieldNode,
nameOrField: string | FieldNode,
variables: Record<string, any>,
): string {
const field = typeof nameOrField === 'string' ? fieldNodeFromName(nameOrField, variables) : nameOrField;
const fieldName = field.name.value;
const policy = this.getFieldPolicy(typename, fieldName, false);
let storeFieldName: string | undefined;
Expand Down
26 changes: 26 additions & 0 deletions src/utilities/graphql/storeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,29 @@ export function isInlineFragment(

export type VariableValue = (node: VariableNode) => any;

export function fieldNodeFromName(fieldName: string, variables: Record<string, any>): FieldNode {
return {
kind: 'Field',
name: {
kind: "Name",
value: fieldName,
},
arguments: Object.keys(variables).reduce((args, key) => ([
...args,
{
kind: 'Argument',
name: {
kind: 'Name',
value: key,
},
value: {
kind: 'Variable',
name: {
kind: 'Name',
value: key,
}
}
}
]), [])
};
}

0 comments on commit 28b139e

Please sign in to comment.