Repository: apollographql/apollo-client

Problem statement:
Adjusting the store in order to squash pagination results via Relay's @connection directive.
I am using the Relay convention for pagination as described [here](http://dev.apollodata.com/react/pagination.html#relay-cursors). Coming from Relay Modern, there is a directive called `@connection`. It contains a key which Relay makes use of in order to retrieve all elements of one connection from the store. Apollo Client on the other side does ignore the `@connection` and stores objects by using keys containing variables like `after` or `count`, used to fetch the right amount of data by using a cursor based pagination approach.

The real problem of that comes into play when I am executing a mutation for adding a new edge into the connection. Here the result has to be stored under the correct key in the store via `proxy.readQuery(...)`, which is impossible if you have multiple keys for lists of the same connection.

I think the problem is better understandable with a short example. This is how a proxy store should look like and how it is right now:

**Intended outcome:**

```
Project:AAAAAAAAA
  | issues -> ref to $Project:AAAAAAAAA.issues.edges

$Project:AAAAAAAAA.issues.edges
  | edges
    | 0 -> ref to $Project:AAAAAAAAA.issues.edges.0
    | ...

$Project:AAAAAAAAA.issues.edges.0
$Project:AAAAAAAAA.issues.edges.1
$Project:AAAAAAAAA.issues.edges.2
$Project:AAAAAAAAA.issues.edges.pageInfo
```

**Actual outcome:**
```
Project:AAAAAAAAA
  | issues({"first":2,"after":null}) -> ref to $Project:AAAAAAAAA.issues({"first":2,"after":null})
  | issues({"first":1,"after":XYXYXYXYXY}) -> ref to $Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY})


$Project:AAAAAAAAA.issues({"first":2,"after":null})
  | edges
    | 0 -> ref to $Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.0
    | 1 -> ref to $Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.1
$Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY})
  | edges
    | 0 -> ref to $Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY}).edges.0

$Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.0
$Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.1
  | cursor="XYXYXYXYXY"
$Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.pageInfo

$Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY}).edges.0
```

How can I tell the Apollo Client store to use a unified key for storing edges of the same connection, ignoring fields like `after` or `cursor` used for accessing the field? Is there a way to make use of the `@connection` information provided in the schema?
Adjusting the store in order to squash pagination results via Relay's @connection directive.
I am using the Relay convention for pagination as described [here](http://dev.apollodata.com/react/pagination.html#relay-cursors). Coming from Relay Modern, there is a directive called `@connection`. It contains a key which Relay makes use of in order to retrieve all elements of one connection from the store. Apollo Client on the other side does ignore the `@connection` and stores objects by using keys containing variables like `after` or `count`, used to fetch the right amount of data by using a cursor based pagination approach.

The real problem of that comes into play when I am executing a mutation for adding a new edge into the connection. Here the result has to be stored under the correct key in the store via `proxy.readQuery(...)`, which is impossible if you have multiple keys for lists of the same connection.

I think the problem is better understandable with a short example. This is how a proxy store should look like and how it is right now:

**Intended outcome:**

```
Project:AAAAAAAAA
  | issues -> ref to $Project:AAAAAAAAA.issues.edges

$Project:AAAAAAAAA.issues.edges
  | edges
    | 0 -> ref to $Project:AAAAAAAAA.issues.edges.0
    | ...

$Project:AAAAAAAAA.issues.edges.0
$Project:AAAAAAAAA.issues.edges.1
$Project:AAAAAAAAA.issues.edges.2
$Project:AAAAAAAAA.issues.edges.pageInfo
```

**Actual outcome:**
```
Project:AAAAAAAAA
  | issues({"first":2,"after":null}) -> ref to $Project:AAAAAAAAA.issues({"first":2,"after":null})
  | issues({"first":1,"after":XYXYXYXYXY}) -> ref to $Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY})


$Project:AAAAAAAAA.issues({"first":2,"after":null})
  | edges
    | 0 -> ref to $Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.0
    | 1 -> ref to $Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.1
$Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY})
  | edges
    | 0 -> ref to $Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY}).edges.0

$Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.0
$Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.1
  | cursor="XYXYXYXYXY"
$Project:AAAAAAAAA.issues({"first":2,"after":null}).edges.pageInfo

$Project:AAAAAAAAA.issues({"first":1,"after":XYXYXYXYXY}).edges.0
```

How can I tell the Apollo Client store to use a unified key for storing edges of the same connection, ignoring fields like `after` or `cursor` used for accessing the field? Is there a way to make use of the `@connection` information provided in the schema?


Hints:
You won't need to worry about that provided you are using fetchMore -> updateQuery to update your cursor or in your case first/after.  The query will get stored in redux with the new key name based on the variables, but the data will flow down to the original query.  That said, the readQuery/writeQuery is the spot that needs some work.  This is what I did (in an infinite scroll solution...):

I prefetch immediately after login as to instantiate cache for multiple queries:

`await this.props.client.query({
query: ALL_ITEMS_QUERY,
  variables: rootQueryVariables('allItems'),
});`

Then the Key to it working....

The below is within my mutate.update function.  unshiftItemToQuery is simply a helper but the point to note is that "rootQueryVariables" will always return the same vars as the initial "allItems" query.  

` 
// All Items
unshiftItemToQuery(
 proxy,
  {
    mutationResult,
    query: ALL_ITEMS_QUERY,
    variables: rootQueryVariables('allItems'),
    ownProps,
    which: 'allItems',
    self: 'assignItem',
   })
`

Since the paginated data flows down to the initial query, reading the query with the initial vars will allow you to find the result set and mutate that.

@jonmanzo That variables part is exactly what gives me headache. I guess you're having some sort of store for variables accessible through the `rootQueryVariables` function. I imagine it being a bit problematic if the variables are given through user input. Surely there are ways to implement `rootQueryVariables` to hack around it, but this solution still feels like a hack, as the original Apollo store implementation does not provide the solution for that case.

It is also worth mentioning that the work-around causes the problem of having links to paginated objects **twice** in the data store. Though in most cases it shouldn't be a problem, it still feels very wrong to have this redundancy in the store.

In my opinion there is need to access the fields in store by some unique id, which can be defined on the query level. Relay has a pretty nice way of doing it by `@connection` annotation:

```
query PeopleQuery($count: Int!, $cursor: String) {
   persons(first: $count, after: $cursor) @connection(key: "KEY_TO_RETRIEVE_PERSONS") {
      ...
   }
}
```

and then accessing that connection from store via `ConnectionHandler`.

**PS**: for now I am using another work around of `updateQueries` in the `mutate` function. Which I am still not comfortable with, as it is relying on the exact naming of the initial query. Additionally I have to drill down the whole query tree in order to push into the affected array instead of directly accessing it from the store. (in contrary to e.g. `proxy.readFragment(...)` if that one would accept my proposal of `@connection` key directive)
Agreed 100%, it is a hack in my opinion and I've had to make functional exclusions to work under this model to boot.  Digging through the src, I didn't find any other options that would allow a modification of the key utilized to store the query...

Honestly, I'd even be ok with the ability to define a query cache key name.  We're pushing an ES query through the args, thus the potential exists for an insane amount of possible argument structures.  Allowing the ability to choose what arguments are utilized in the key name would clean this up instantly in our use case.
Oh, and we cannot solely use update queries as we're moving objs from one cache to another based in user action and ran into issues where the destination query was no longer watched once their component unmounted.  Found this a better solution that putting all the queries into a top level wrapper just to keep them available to updateQueries
We did want to have this in the original implementation for pagination, but didn't do it at first because the simpler version has worked fine for a while. I agree that being able to pick a custom store field name is a great way to do this.

We used to call this concept `quietArguments` and were thinking of an `@apolloFetchMore` directive: https://github.com/apollographql/apollo-client/pull/350

Perhaps we should bring this back and now may be the time to do it!
@zunder As @stubailo said, we think it would be a good idea to allow for an `@connection` directive. The key would determine where in the store the connection gets written to, so it would be easy to read it out with `readQuery`. The only thing that we'd have to decide is how you would tell Apollo Client when you're fetching more for a certain connection, and whether that "more" goes at the beginning, the end or somewhere in the middle.
I don't think we need to change anything about the fetchMore mechanism to add this directive - we could still use the manual reducer approach. Only difference would be having a more reasonable cache key!
Yeah, you're right, since `fetchMore` is imperative we don't need any configuration.
You won't need to worry about that provided you are using fetchMore -> updateQuery to update your cursor or in your case first/after.  The query will get stored in redux with the new key name based on the variables, but the data will flow down to the original query.  That said, the readQuery/writeQuery is the spot that needs some work.  This is what I did (in an infinite scroll solution...):

I prefetch immediately after login as to instantiate cache for multiple queries:

`await this.props.client.query({
query: ALL_ITEMS_QUERY,
  variables: rootQueryVariables('allItems'),
});`

Then the Key to it working....

The below is within my mutate.update function.  unshiftItemToQuery is simply a helper but the point to note is that "rootQueryVariables" will always return the same vars as the initial "allItems" query.  

` 
// All Items
unshiftItemToQuery(
 proxy,
  {
    mutationResult,
    query: ALL_ITEMS_QUERY,
    variables: rootQueryVariables('allItems'),
    ownProps,
    which: 'allItems',
    self: 'assignItem',
   })
`

Since the paginated data flows down to the initial query, reading the query with the initial vars will allow you to find the result set and mutate that.

@jonmanzo That variables part is exactly what gives me headache. I guess you're having some sort of store for variables accessible through the `rootQueryVariables` function. I imagine it being a bit problematic if the variables are given through user input. Surely there are ways to implement `rootQueryVariables` to hack around it, but this solution still feels like a hack, as the original Apollo store implementation does not provide the solution for that case.

It is also worth mentioning that the work-around causes the problem of having links to paginated objects **twice** in the data store. Though in most cases it shouldn't be a problem, it still feels very wrong to have this redundancy in the store.

In my opinion there is need to access the fields in store by some unique id, which can be defined on the query level. Relay has a pretty nice way of doing it by `@connection` annotation:

```
query PeopleQuery($count: Int!, $cursor: String) {
   persons(first: $count, after: $cursor) @connection(key: "KEY_TO_RETRIEVE_PERSONS") {
      ...
   }
}
```

and then accessing that connection from store via `ConnectionHandler`.

**PS**: for now I am using another work around of `updateQueries` in the `mutate` function. Which I am still not comfortable with, as it is relying on the exact naming of the initial query. Additionally I have to drill down the whole query tree in order to push into the affected array instead of directly accessing it from the store. (in contrary to e.g. `proxy.readFragment(...)` if that one would accept my proposal of `@connection` key directive)
Agreed 100%, it is a hack in my opinion and I've had to make functional exclusions to work under this model to boot.  Digging through the src, I didn't find any other options that would allow a modification of the key utilized to store the query...

Honestly, I'd even be ok with the ability to define a query cache key name.  We're pushing an ES query through the args, thus the potential exists for an insane amount of possible argument structures.  Allowing the ability to choose what arguments are utilized in the key name would clean this up instantly in our use case.
Oh, and we cannot solely use update queries as we're moving objs from one cache to another based in user action and ran into issues where the destination query was no longer watched once their component unmounted.  Found this a better solution that putting all the queries into a top level wrapper just to keep them available to updateQueries
We did want to have this in the original implementation for pagination, but didn't do it at first because the simpler version has worked fine for a while. I agree that being able to pick a custom store field name is a great way to do this.

We used to call this concept `quietArguments` and were thinking of an `@apolloFetchMore` directive: https://github.com/apollographql/apollo-client/pull/350

Perhaps we should bring this back and now may be the time to do it!
@zunder As @stubailo said, we think it would be a good idea to allow for an `@connection` directive. The key would determine where in the store the connection gets written to, so it would be easy to read it out with `readQuery`. The only thing that we'd have to decide is how you would tell Apollo Client when you're fetching more for a certain connection, and whether that "more" goes at the beginning, the end or somewhere in the middle.
I don't think we need to change anything about the fetchMore mechanism to add this directive - we could still use the manual reducer approach. Only difference would be having a more reasonable cache key!
Yeah, you're right, since `fetchMore` is imperative we don't need any configuration.

Patch:
diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts
--- a/src/ApolloClient.ts
+++ b/src/ApolloClient.ts
@@ -65,7 +65,7 @@ import {
 } from './core/watchQueryOptions';
 
 import {
-  storeKeyNameFromFieldNameAndArgs,
+  getStoreKeyName,
 } from './data/storeUtils';
 
 import {
@@ -222,7 +222,7 @@ export default class ApolloClient implements DataProxy {
     this.disableNetworkFetches = ssrMode || ssrForceFetchDelay > 0;
     this.dataId = dataIdFromObject = dataIdFromObject || defaultDataIdFromObject;
     this.dataIdFromObject = this.dataId;
-    this.fieldWithArgs = storeKeyNameFromFieldNameAndArgs;
+    this.fieldWithArgs = getStoreKeyName;
     this.queryDeduplication = queryDeduplication;
     this.ssrMode = ssrMode;
 
diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts
--- a/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -1096,24 +1096,30 @@ export class QueryManager {
           }
 
           return result;
-        }).then(() => {
-
+        }).then((result) => {
           let resultFromStore: any;
-          try {
-            // ensure result is combined with data already in store
-            // this will throw an error if there are missing fields in
-            // the results if returnPartialData is false.
-            resultFromStore = readQueryFromStore({
-              store: this.getApolloState().data,
-              variables,
-              query: document,
-              config: this.reducerConfig,
-              fragmentMatcherFunction: this.fragmentMatcher.match,
-            });
-            // ensure multiple errors don't get thrown
-            /* tslint:disable */
-          } catch (e) {}
-          /* tslint:enable */
+
+          if (fetchMoreForQueryId) {
+            // XXX We don't write fetchMore results to the store because this would overwrite
+            // the original result in case an @connection directive is used.
+            resultFromStore = result.data;
+          } else {
+            try {
+              // ensure result is combined with data already in store
+              // this will throw an error if there are missing fields in
+              // the results if returnPartialData is false.
+              resultFromStore = readQueryFromStore({
+                store: this.getApolloState().data,
+                variables,
+                query: document,
+                config: this.reducerConfig,
+                fragmentMatcherFunction: this.fragmentMatcher.match,
+              });
+              // ensure multiple errors don't get thrown
+              /* tslint:disable */
+            } catch (e) {}
+            /* tslint:enable */
+          }
 
           const { reducerError } = this.getApolloState();
           if (reducerError && reducerError.queryId === queryId) {
diff --git a/src/data/readFromStore.ts b/src/data/readFromStore.ts
--- a/src/data/readFromStore.ts
+++ b/src/data/readFromStore.ts
@@ -16,7 +16,7 @@ import {
 } from './storeUtils';
 
 import {
-  storeKeyNameFromFieldNameAndArgs,
+  getStoreKeyName,
 } from './storeUtils';
 
 import {
@@ -132,13 +132,13 @@ const readStoreResolver: Resolver = (
   idValue: IdValueWithPreviousResult,
   args: any,
   context: ReadStoreContext,
-  { resultKey }: ExecInfo,
+  { resultKey, directives }: ExecInfo,
 ) => {
   assertIdValue(idValue);
 
   const objId = idValue.id;
   const obj = context.store[objId];
-  const storeKeyName = storeKeyNameFromFieldNameAndArgs(fieldName, args);
+  const storeKeyName = getStoreKeyName(fieldName, args, directives);
   let fieldValue = (obj || {})[storeKeyName];
 
   if (typeof fieldValue === 'undefined') {
diff --git a/src/data/store.ts b/src/data/store.ts
--- a/src/data/store.ts
+++ b/src/data/store.ts
@@ -69,7 +69,8 @@ export function data(
     // Ignore results from old requests
     // XXX this means that if you have a refetch interval which is shorter than your roundtrip time,
     // your query will be in the loading state forever!
-    if (action.requestId < queries[action.queryId].lastRequestId) {
+    // do not write to the store if this is for fetchMore
+    if (action.requestId < queries[action.queryId].lastRequestId || action.fetchMoreForQueryId) {
       return previousState;
     }
 
diff --git a/src/data/storeUtils.ts b/src/data/storeUtils.ts
--- a/src/data/storeUtils.ts
+++ b/src/data/storeUtils.ts
@@ -74,19 +74,40 @@ export function valueToObjectRepresentation(argObj: any, name: NameNode, value:
 }
 
 export function storeKeyNameFromField(field: FieldNode, variables?: Object): string {
-  if (field.arguments && field.arguments.length) {
-    const argObj: Object = {};
+  let directivesObj: any = null;
+  if (field.directives) {
+    directivesObj = {};
+    field.directives.forEach((directive) => {
+      directivesObj[directive.name.value] = {};
+
+      if (directive.arguments) {
+        directive.arguments.forEach((({name, value}) => valueToObjectRepresentation(
+          directivesObj[directive.name.value], name, value, variables)));
+      }
+    });
+  }
 
+  let argObj: any = null;
+  if (field.arguments && field.arguments.length) {
+    argObj = {};
     field.arguments.forEach(({name, value}) => valueToObjectRepresentation(
       argObj, name, value, variables));
-
-    return storeKeyNameFromFieldNameAndArgs(field.name.value, argObj);
   }
 
-  return field.name.value;
+  return getStoreKeyName(field.name.value, argObj, directivesObj);
 }
 
-export function storeKeyNameFromFieldNameAndArgs(fieldName: string, args?: Object): string {
+export type Directives = {
+    [directiveName: string]: {
+        [argName: string]: any;
+    };
+};
+
+export function getStoreKeyName(fieldName: string, args?: Object, directives?: Directives): string {
+  if (directives && directives['connection'] && directives['connection']['key']) {
+    return directives['connection']['key'];
+  }
+
   if (args) {
     const stringifiedArgs: string = JSON.stringify(args);
 
diff --git a/src/queries/queryTransform.ts b/src/queries/queryTransform.ts
--- a/src/queries/queryTransform.ts
+++ b/src/queries/queryTransform.ts
@@ -51,6 +51,38 @@ function addTypenameToSelectionSet(
   }
 }
 
+function removeConnectionDirectiveFromSelectionSet(selectionSet: SelectionSetNode) {
+  if (selectionSet.selections) {
+    selectionSet.selections.forEach((selection) => {
+      if (selection.kind === 'Field' && selection as FieldNode && selection.directives) {
+        selection.directives = selection.directives.filter((directive) => {
+          const willRemove = directive.name.value === 'connection';
+          if (willRemove) {
+            if (!directive.arguments || !directive.arguments.some((arg) => arg.name.value === 'key')) {
+              console.warn('Removing an @connection directive even though it does not have a key. ' +
+                'You may want to use the key parameter to specify a store key.');
+            }
+          }
+
+          return !willRemove;
+        });
+      }
+    });
+
+    selectionSet.selections.forEach((selection) => {
+      if (selection.kind === 'Field') {
+        if (selection.selectionSet) {
+          removeConnectionDirectiveFromSelectionSet(selection.selectionSet);
+        }
+      } else if (selection.kind === 'InlineFragment') {
+        if (selection.selectionSet) {
+          removeConnectionDirectiveFromSelectionSet(selection.selectionSet);
+        }
+      }
+    });
+  }
+}
+
 export function addTypenameToDocument(doc: DocumentNode) {
   checkDocument(doc);
   const docClone = cloneDeep(doc);
@@ -62,3 +94,14 @@ export function addTypenameToDocument(doc: DocumentNode) {
 
   return docClone;
 }
+
+export function removeConnectionDirectiveFromDocument(doc: DocumentNode) {
+  checkDocument(doc);
+  const docClone = cloneDeep(doc);
+
+  docClone.definitions.forEach((definition: DefinitionNode) => {
+    removeConnectionDirectiveFromSelectionSet((definition as OperationDefinitionNode).selectionSet);
+  });
+
+  return docClone;
+}
diff --git a/src/transport/networkInterface.ts b/src/transport/networkInterface.ts
--- a/src/transport/networkInterface.ts
+++ b/src/transport/networkInterface.ts
@@ -17,6 +17,10 @@ import {
   BatchAfterwareInterface,
 } from './afterware';
 
+import {
+  removeConnectionDirectiveFromDocument,
+} from '../queries/queryTransform';
+
 /**
  * This is an interface that describes an GraphQL document to be sent
  * to the server.
@@ -192,6 +196,12 @@ export class HTTPFetchNetworkInterface extends BaseNetworkInterface {
     return this.applyMiddlewares({
       request,
       options,
+    }).then((rao) => {
+      if (rao.request.query) {
+        rao.request.query = removeConnectionDirectiveFromDocument(rao.request.query);
+      }
+
+      return rao;
     }).then( (rao) => this.fetchFromRemoteEndpoint.call(this, rao))
       .then(response => this.applyAfterwares({
         response: response as Response,


Test patch:
diff --git a/test/client.ts b/test/client.ts
--- a/test/client.ts
+++ b/test/client.ts
@@ -2554,8 +2554,137 @@ describe('client', () => {
 
     return withWarning(() => client.query({ query }), /Missing field description/);
   });
+
+  it('runs a query with the connection directive and writes it to the store key defined in the directive', () => {
+    const query = gql`
+      {
+        books(skip: 0, limit: 2) @connection(key: "abc") {
+          name
+        }
+      }`;
+
+    const transformedQuery = gql`
+      {
+        books(skip: 0, limit: 2) @connection(key: "abc") {
+          name
+          __typename
+        }
+      }`;
+
+    const result = {
+      'books': [
+        {
+          'name': 'abcd',
+          '__typename': 'Book',
+        },
+      ],
+    };
+
+    const networkInterface = mockNetworkInterface({
+      request: { query: transformedQuery },
+      result: { data: result },
+    });
+
+    const client = new ApolloClient({
+      networkInterface,
+    });
+
+    return client.query({ query }).then((actualResult) => {
+      assert.deepEqual(actualResult.data, result);
+    });
+  });
+
+  it('should not remove the connection directive at the store level', () => {
+    const query = gql`
+      {
+        books(skip: 0, limit: 2) @connection {
+          name
+        }
+      }`;
+
+    const transformedQuery = gql`
+      {
+        books(skip: 0, limit: 2) @connection {
+          name
+          __typename
+        }
+      }`;
+
+    const result = {
+      'books': [
+        {
+          'name': 'abcd',
+          '__typename': 'Book',
+        },
+      ],
+    };
+
+    const networkInterface = mockNetworkInterface({
+      request: { query: transformedQuery },
+      result: { data: result },
+    });
+
+    const client = new ApolloClient({
+      networkInterface,
+    });
+
+    return client.query({ query }).then((actualResult) => {
+      assert.deepEqual(actualResult.data, result);
+    });
+  });
 });
 
+it('should run a query with the connection directive and write the result to the store key defined in the directive', () => {
+    const query = gql`
+      {
+        books(skip: 0, limit: 2) @connection(key: "abc") {
+          name
+        }
+      }`;
+
+    const transformedQuery = gql`
+      {
+        books(skip: 0, limit: 2) @connection(key: "abc") {
+          name
+          __typename
+        }
+      }`;
+
+    const result = {
+      'books': [
+        {
+          'name': 'abcd',
+          '__typename': 'Book',
+        },
+      ],
+    };
+
+    const networkInterface = mockNetworkInterface({
+      request: { query: transformedQuery },
+      result: { data: result },
+    });
+
+    const client = new ApolloClient({
+      networkInterface,
+    });
+
+    return client.query({ query }).then((actualResult) => {
+      assert.deepEqual(actualResult.data, result);
+      assert.deepEqual(client.store.getState().apollo.data, {
+        'ROOT_QUERY.abc.0': { name: 'abcd', __typename: 'Book' },
+        'ROOT_QUERY': {
+          abc: [
+            {
+              'generated': true,
+              'id': 'ROOT_QUERY.abc.0',
+              'type': 'id',
+            },
+          ],
+        },
+      });
+    });
+  });
+
 function clientRoundtrip(
   query: DocumentNode,
   data: ExecutionResult,
diff --git a/test/networkInterface.ts b/test/networkInterface.ts
--- a/test/networkInterface.ts
+++ b/test/networkInterface.ts
@@ -56,6 +56,26 @@ describe('network interface', () => {
     }
   `;
 
+  const simpleQueryWithConnection = gql`
+    query people {
+      allPeople(first: 1) @connection(key: "people") {
+        people {
+          name
+        }
+      }
+    }
+  `;
+
+  const simpleQueryWithConnectionButNoKey = gql`
+    query people {
+      allPeople(first: 1) @connection {
+        people {
+          name
+        }
+      }
+    }
+  `;
+
   const simpleResult = {
     data: {
       allPeople: {
@@ -483,6 +503,44 @@ describe('network interface', () => {
       });
     });
   });
+
+  describe('transforming queries', () => {
+    it('should remove the @connection directive', () => {
+      const swapi = createNetworkInterface({ uri: swapiUrl });
+
+      const simpleRequestWithConnection = {
+        query: simpleQueryWithConnection,
+        variables: {},
+        debugName: 'People query',
+      };
+
+      return assert.eventually.deepEqual(
+        swapi.query(simpleRequestWithConnection),
+        simpleResult,
+      );
+    });
+
+    it('should remove the @connection directive even with no key but warn the user', () => {
+      const swapi = createNetworkInterface({ uri: swapiUrl });
+
+      const simpleRequestWithConnectionButNoKey = {
+        query: simpleQueryWithConnectionButNoKey,
+        variables: {},
+        debugName: 'People query',
+      };
+
+      const expected = 'Removing an @connection directive even though it does not have a ' +
+        'key. You may want to use the key parameter to specify a store key.';
+
+      return assert.eventually.deepEqual(
+        withWarning(
+          () => swapi.query(simpleRequestWithConnectionButNoKey),
+          new RegExp(expected),
+        ),
+        simpleResult,
+      );
+    });
+  });
 });
 
 // simulate middleware by altering variables and options
diff --git a/test/readFromStore.ts b/test/readFromStore.ts
--- a/test/readFromStore.ts
+++ b/test/readFromStore.ts
@@ -812,4 +812,43 @@ describe('reading from the store', () => {
       nullField: null,
     });
   });
+
+  it('properly handles the connection directive', () => {
+    const store: NormalizedCache = {
+      'ROOT_QUERY': {
+        'abc': [
+          {
+            'generated': true,
+            'id': 'ROOT_QUERY.abc.0',
+            'type': 'id',
+          },
+        ],
+      },
+      'ROOT_QUERY.abc.0': {
+        'name': 'efgh',
+      },
+    };
+
+    const queryResult = readQueryFromStore({
+      store,
+      query: gql`
+        {
+          books(skip: 0, limit: 2) @connection(key: "abc") {
+            name
+          }
+        }
+      `,
+    });
+
+    assert.deepEqual<{}>(
+      queryResult,
+      {
+        'books': [
+          {
+            'name': 'efgh',
+          },
+        ],
+      },
+    );
+  });
 });
diff --git a/test/util/wrap.ts b/test/util/wrap.ts
--- a/test/util/wrap.ts
+++ b/test/util/wrap.ts
@@ -17,9 +17,10 @@ export function withWarning(func: Function, regex: RegExp) {
 
   console.warn = (m: string) => message = m;
 
-  return Promise.resolve(func()).then(() => {
+  return Promise.resolve(func()).then((val) => {
     assert.match(message, regex);
     console.warn = oldWarn;
+    return val;
   });
 }
 
diff --git a/test/writeToStore.ts b/test/writeToStore.ts
--- a/test/writeToStore.ts
+++ b/test/writeToStore.ts
@@ -1311,4 +1311,62 @@ describe('writing to the store', () => {
       });
     }, /stringField(.|\n)*abcd/g);
   });
+
+  it('properly handles the connection directive', () => {
+    const store: NormalizedCache = {};
+
+    writeQueryToStore({
+      query: gql`
+        {
+          books(skip: 0, limit: 2) @connection(key: "abc") {
+            name
+          }
+        }
+      `,
+      result: {
+        books: [
+          {
+            name: 'abcd',
+          },
+        ],
+      },
+      store,
+    });
+
+    writeQueryToStore({
+      query: gql`
+        {
+          books(skip: 2, limit: 4) @connection(key: "abc") {
+            name
+          }
+        }
+      `,
+      result: {
+        books: [
+          {
+            name: 'efgh',
+          },
+        ],
+      },
+      store,
+    });
+
+    assert.deepEqual<NormalizedCache>(
+      store,
+      {
+        'ROOT_QUERY': {
+          'abc': [
+            {
+              'generated': true,
+              'id': 'ROOT_QUERY.abc.0',
+              'type': 'id',
+            },
+          ],
+        },
+        'ROOT_QUERY.abc.0': {
+          'name': 'efgh',
+        },
+      },
+    );
+  });
 });