Repository: apollographql/apollo-client

Problem statement:
Enable cache implementations to modify document before transmission to links
Per a Slack conversation with @jbaxleyiii.

As cache implementations become more sophisticated, it will be helpful to allow end users to signal cache-specific behaviors via directives in the GraphQL document. (A simple, popular example would be a `@ttl` directive to set a cache TTL on a specific field.) These directives are meant exclusively for consumption by the client, and need to be stripped before reaching the server, lest a spec-compliant server complain about an unknown directive.

The semantics of these directives is owned by the cache implementation, so it seems reasonable for the cache to be able to 'see' these directives in its various lifecycle methods (including `read` and `write`) and also be responsible for stripping the directives from the document before the document is sent to the link stack (and ultimately to a server).

Right now, the `prepareDocument` cache lifecycle method is too early to remove said directives, because the directives will then be absent from the document when it's passed to `read` and `write`. (However, `prepareDocument` serves a useful purpose for the `addTypenames` use case, where both cache and server should see the transformed document.) Ideally, there could be a `prepareDocumentForLink` lifecycle method that is called before a given document is sent onward to the network.


Hints:

Patch:
diff --git a/packages/apollo-cache/src/cache.ts b/packages/apollo-cache/src/cache.ts
--- a/packages/apollo-cache/src/cache.ts
+++ b/packages/apollo-cache/src/cache.ts
@@ -48,6 +48,10 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
   public transformDocument(document: DocumentNode): DocumentNode {
     return document;
   }
+  // experimental
+  public transformForLink(document: DocumentNode): DocumentNode {
+    return document;
+  }
 
   // DataProxy API
   /**
diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts
--- a/packages/apollo-client/src/core/QueryManager.ts
+++ b/packages/apollo-client/src/core/QueryManager.ts
@@ -132,8 +132,9 @@ export class QueryManager<TStore> {
     }
 
     const mutationId = this.generateQueryId();
+    const cache = this.dataStore.getCache();
     mutation = removeConnectionDirectiveFromDocument(
-      this.dataStore.getCache().transformDocument(mutation),
+      cache.transformDocument(mutation),
     );
 
     variables = assign(
@@ -187,7 +188,13 @@ export class QueryManager<TStore> {
     return new Promise((resolve, reject) => {
       let storeResult: FetchResult<T> | null;
       let error: ApolloError;
-      execute(this.link, request).subscribe({
+      let newRequest = {
+        ...request,
+        query: cache.transformForLink
+          ? cache.transformForLink(request.query)
+          : request.query,
+      };
+      execute(this.link, newRequest).subscribe({
         next: (result: ExecutionResult) => {
           if (result.errors && errorPolicy === 'none') {
             error = new ApolloError({
@@ -281,8 +288,9 @@ export class QueryManager<TStore> {
       metadata = null,
       fetchPolicy = 'cache-first', // cache-first is the default fetch policy.
     } = options;
+    const cache = this.dataStore.getCache();
 
-    const query = this.dataStore.getCache().transformDocument(options.query);
+    const query = cache.transformDocument(options.query);
 
     let storeResult: any;
     let needToFetch: boolean = fetchPolicy === 'network-only';
@@ -355,7 +363,9 @@ export class QueryManager<TStore> {
       const networkResult = this.fetchRequest({
         requestId,
         queryId,
-        document: query,
+        document: cache.transformForLink
+          ? cache.transformForLink(query)
+          : query,
         options,
         fetchMoreForQueryId,
       }).catch(error => {
@@ -840,7 +850,8 @@ export class QueryManager<TStore> {
     options: SubscriptionOptions,
   ): Observable<any> {
     const { query } = options;
-    let transformedDoc = this.dataStore.getCache().transformDocument(query);
+    const cache = this.dataStore.getCache();
+    let transformedDoc = cache.transformDocument(query);
 
     const variables = assign(
       {},
@@ -890,7 +901,13 @@ export class QueryManager<TStore> {
           },
         };
 
-        sub = execute(this.link, request).subscribe(handler);
+        let newRequest = {
+          ...request,
+          query: cache.transformForLink
+            ? cache.transformForLink(request.query)
+            : request.query,
+        };
+        sub = execute(this.link, newRequest).subscribe(handler);
       }
 
       return () => {


Test patch:
diff --git a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts b/packages/apollo-client/src/core/__tests__/ObservableQuery.ts
--- a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts
+++ b/packages/apollo-client/src/core/__tests__/ObservableQuery.ts
@@ -783,6 +783,69 @@ describe('ObservableQuery', () => {
         }
       });
     });
+    it('does invalidate the currentResult data if the variables change', done => {
+      // Standard data for all these tests
+      const query = gql`
+        query UsersQuery($page: Int) {
+          users {
+            id
+            name
+            posts(page: $page) {
+              title
+            }
+          }
+        }
+      `;
+      const variables = { page: 1 };
+      const differentVariables = { page: 2 };
+      const dataOne = {
+        users: [
+          {
+            id: 1,
+            name: 'James',
+            posts: [{ title: 'GraphQL Summit' }, { title: 'Awesome' }],
+          },
+        ],
+      };
+      const dataTwo = {
+        users: [
+          {
+            id: 1,
+            name: 'James',
+            posts: [{ title: 'Old post' }],
+          },
+        ],
+      };
+
+      const observable: ObservableQuery<any> = mockWatchQuery(
+        {
+          request: { query, variables },
+          result: { data: dataOne },
+        },
+        {
+          request: { query, variables: differentVariables },
+          result: { data: dataTwo },
+          delay: 25,
+        },
+      );
+
+      subscribeAndCount(done, observable, (handleCount, result) => {
+        if (handleCount === 1) {
+          expect(result.data).toEqual(dataOne);
+          expect(observable.currentResult().data).toEqual(dataOne);
+          observable.setVariables(differentVariables);
+          expect(observable.currentResult().data).toEqual({});
+          expect(observable.currentResult().loading).toBe(true);
+        }
+        // after loading is false and data has returned
+        if (handleCount === 3) {
+          expect(result.data).toEqual(dataTwo);
+          expect(observable.currentResult().data).toEqual(dataTwo);
+          expect(observable.currentResult().loading).toBe(false);
+          done();
+        }
+      });
+    });
 
     it('does not invalidate the currentResult errors if the variables change', done => {
       const queryManager = mockQueryManager(