Repository: FormidableLabs/urql
Pull request #1186 (2020-12-02 12:30:51)

(core) - Fix formatDocument edge case and add caching
## Summary

Fix an edge case in `formatDocument` for aliased fields and add in-memory caching to it via the document key.

## Set of changes

- Add `keyDocument` for `formatDocument` to cache its result
- Add `alias` check to `formatDocument`'s visit function
- Code golf `collectTypesFromResponse` a little

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: f493d174a7af7c73a603004e3070c2f3a62aacaf

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name       | Type  |
| ---------- | ----- |
| @urql/core | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/FormidableLabs/urql/new/fix/format-document-edgecases?filename=.changeset/lazy-bats-appear.md&value=---%0A%22%40urql%2Fcore%22%3A%20patch%0A---%0A%0A(core)%20-%20Fix%20formatDocument%20edge%20case%20and%20add%20caching%0A)



Patch:
diff --git a/.changeset/nice-rivers-run.md b/.changeset/nice-rivers-run.md
new file mode 100644
index 00000000..c0044b66
--- /dev/null
+++ b/.changeset/nice-rivers-run.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Fix edge case in `formatDocument`, which fails to add a `__typename` field if it has been aliased to a different name.
diff --git a/.changeset/serious-gorillas-develop.md b/.changeset/serious-gorillas-develop.md
new file mode 100644
index 00000000..4facbfb0
--- /dev/null
+++ b/.changeset/serious-gorillas-develop.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Cache results of `formatDocument` by the input document's key.
diff --git a/.changeset/smart-lizards-crash.md b/.changeset/smart-lizards-crash.md
new file mode 100644
index 00000000..cdff4b5e
--- /dev/null
+++ b/.changeset/smart-lizards-crash.md
@@ -0,0 +1,9 @@
+---
+'@urql/exchange-graphcache': minor
+---
+
+Increase the consistency of when and how the `__typename` field is added to results. Instead of
+adding it by default and automatically first, the `__typename` field will now be added along with
+the usual selection set. The `write` operation now automatically issues a warning if `__typename`
+isn't present where it's expected more often, which helps in debugging. Also the `__typename` field
+may now not proactively be added to root results, e.g. `"Query"`.
diff --git a/docs/graphcache/errors.md b/docs/graphcache/errors.md
index 86dc97c5..6d156d0a 100644
--- a/docs/graphcache/errors.md
+++ b/docs/graphcache/errors.md
@@ -194,6 +194,17 @@ As data is written to the cache, this warning is issued when `undefined` is enco
 GraphQL results should never contain an `undefined` value, so this warning will let you
 know which part of your result did contain `undefined`.
 
+## (14) Couldn't find \_\_typename when writing.
+
+> Couldn't find **typename when writing.
+> If you're writing to the cache manually have to pass a `**typename` property on each entity in your data.
+
+You probably have called `cache.writeFragment` or `cache.updateQuery` with data that is missing a
+`__typename` field for an entity where your document contains a selection set. The cache won't be
+able to generate a key for entities that are missing the `__typename` field.
+
+Please make sure that you include enough properties on your data so that `write` can generate a key.
+
 ## (15) Invalid key
 
 > Invalid key: The GraphQL query at the field at `???` has a selection set,
diff --git a/exchanges/graphcache/default-storage/package.json b/exchanges/graphcache/default-storage/package.json
index 1d3a886e..645fc708 100644
--- a/exchanges/graphcache/default-storage/package.json
+++ b/exchanges/graphcache/default-storage/package.json
@@ -15,7 +15,7 @@
     "./package.json": "./package.json"
   },
   "dependencies": {
-    "@urql/core": ">=1.14.1",
+    "@urql/core": ">=1.15.2",
     "wonka": "^4.0.14"
   }
 }
diff --git a/exchanges/graphcache/src/helpers/help.ts b/exchanges/graphcache/src/helpers/help.ts
index 86d54fc4..7454272b 100644
--- a/exchanges/graphcache/src/helpers/help.ts
+++ b/exchanges/graphcache/src/helpers/help.ts
@@ -19,6 +19,7 @@ export type ErrorCode =
   | 11
   | 12
   | 13
+  | 14
   | 15
   | 16
   | 17
diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts
index ecd1a3f3..d40ed7cc 100644
--- a/exchanges/graphcache/src/operations/query.ts
+++ b/exchanges/graphcache/src/operations/query.ts
@@ -271,9 +271,6 @@ const readSelection = (
     return;
   }
 
-  // The following closely mirrors readSelection, but differs only slightly for the
-  // sake of resolving from an existing resolver result
-  data.__typename = typename;
   const iterate = makeSelectionIterator(typename, entityKey, select, ctx);
 
   let node: FieldNode | void;
@@ -298,7 +295,10 @@ const readSelection = (
     // means that the value is missing from the cache
     let dataFieldValue: void | DataField;
 
-    if (resultValue !== undefined && node.selectionSet === undefined) {
+    if (fieldName === '__typename') {
+      data[fieldAlias] = typename;
+      continue;
+    } else if (resultValue !== undefined && node.selectionSet === undefined) {
       // The field is a scalar and can be retrieved directly from the result
       dataFieldValue = resultValue;
     } else if (
diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts
index fe43224e..787e5c86 100644
--- a/exchanges/graphcache/src/operations/shared.ts
+++ b/exchanges/graphcache/src/operations/shared.ts
@@ -149,7 +149,7 @@ export const makeSelectionIterator = (
             ))();
           }
         }
-      } else if (getName(node) !== '__typename') {
+      } else {
         return node;
       }
     }
diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts
index a4872a15..3a3a42d4 100644
--- a/exchanges/graphcache/src/operations/write.ts
+++ b/exchanges/graphcache/src/operations/write.ts
@@ -194,6 +194,11 @@ const writeSelection = (
   const isRoot = !isQuery && !!ctx.store.rootNames[entityKey!];
   const typename = isRoot || isQuery ? entityKey : data.__typename;
   if (!typename) {
+    warn(
+      "Couldn't find __typename when writing.\n" +
+        "If you're writing to the cache manually have to pass a `__typename` property on each entity in your data.",
+      14
+    );
     return;
   } else if (!isRoot && !isQuery && entityKey) {
     InMemoryData.writeRecord(entityKey, '__typename', typename);
@@ -236,12 +241,14 @@ const writeSelection = (
         );
 
         continue; // Skip this field
-      } else if (ctx.store.schema && typename) {
+      } else if (ctx.store.schema && typename && fieldName !== '__typename') {
         isFieldAvailableOnType(ctx.store.schema, typename, fieldName);
       }
     }
 
-    if (ctx.optimistic && isRoot) {
+    if (fieldName === '__typename') {
+      continue;
+    } else if (ctx.optimistic && isRoot) {
       const resolver = ctx.store.optimisticMutations[fieldName];
 
       if (!resolver) continue;
diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts
index d9ecd405..65d7bccf 100644
--- a/exchanges/graphcache/src/store/store.ts
+++ b/exchanges/graphcache/src/store/store.ts
@@ -5,7 +5,7 @@ import {
   GraphQLSchema,
 } from 'graphql';
 
-import { TypedDocumentNode, createRequest } from '@urql/core';
+import { TypedDocumentNode, formatDocument, createRequest } from '@urql/core';
 
 import {
   Cache,
@@ -177,6 +177,7 @@ export class Store implements Cache {
     updater: (data: T | null) => T | null
   ): void {
     const request = createRequest<T, V>(input.query, input.variables as any);
+    request.query = formatDocument(request.query);
     const output = updater(this.readQuery(request));
     if (output !== null) {
       startWrite(this, request, output as any);
@@ -184,8 +185,9 @@ export class Store implements Cache {
   }
 
   readQuery<T = Data, V = Variables>(input: QueryInput<T, V>): T | null {
-    return read(this, createRequest(input.query, input.variables!))
-      .data as T | null;
+    const request = createRequest(input.query, input.variables!);
+    request.query = formatDocument(request.query);
+    return read(this, request).data as T | null;
   }
 
   readFragment<T = Data, V = Variables>(
@@ -193,7 +195,12 @@ export class Store implements Cache {
     entity: string | Data | T,
     variables?: V
   ): T | null {
-    return readFragment(this, fragment, entity, variables as any) as T | null;
+    return readFragment(
+      this,
+      formatDocument(fragment),
+      entity,
+      variables as any
+    ) as T | null;
   }
 
   writeFragment<T = Data, V = Variables>(
@@ -201,6 +208,6 @@ export class Store implements Cache {
     data: T,
     variables?: V
   ): void {
-    writeFragment(this, fragment, data, variables as any);
+    writeFragment(this, formatDocument(fragment), data, variables as any);
   }
 }
diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts
index ab78297c..002974bc 100644
--- a/packages/core/src/utils/request.ts
+++ b/packages/core/src/utils/request.ts
@@ -4,38 +4,45 @@ import { hash, phash } from './hash';
 import { stringifyVariables } from './stringifyVariables';
 import { GraphQLRequest } from '../types';
 
-interface Documents {
-  [key: number]: DocumentNode;
+export interface KeyedDocumentNode extends DocumentNode {
+  __key: number;
 }
 
 const hashQuery = (q: string): number =>
   hash(q.replace(/([\s,]|#[^\n\r]+)+/g, ' ').trim());
 
-const docs: Documents = Object.create(null);
+const docs = new Map<number, KeyedDocumentNode>();
 
-export const createRequest = <Data = any, Variables = object>(
-  q: string | DocumentNode | TypedDocumentNode<Data, Variables>,
-  vars?: Variables
-): GraphQLRequest<Data, Variables> => {
+export const keyDocument = (
+  q: string | DocumentNode | TypedDocumentNode
+): KeyedDocumentNode => {
   let key: number;
   let query: DocumentNode;
   if (typeof q === 'string') {
     key = hashQuery(q);
-    query =
-      docs[key] !== undefined ? docs[key] : parse(q, { noLocation: true });
-  } else if ((q as any).__key !== undefined) {
+    query = docs.get(key) || parse(q, { noLocation: true });
+  } else if ((q as any).__key != null) {
     key = (q as any).__key;
     query = q;
   } else {
     key = hashQuery(print(q));
-    query = docs[key] !== undefined ? docs[key] : q;
+    query = docs.get(key) || q;
   }
 
-  docs[key] = query;
-  (query as any).__key = key;
+  (query as KeyedDocumentNode).__key = key;
+  docs.set(key, query as KeyedDocumentNode);
+  return query as KeyedDocumentNode;
+};
 
+export const createRequest = <Data = any, Variables = object>(
+  q: string | DocumentNode | TypedDocumentNode<Data, Variables>,
+  vars?: Variables
+): GraphQLRequest<Data, Variables> => {
+  const query = keyDocument(q);
   return {
-    key: vars ? phash(key, stringifyVariables(vars)) >>> 0 : key,
+    key: vars
+      ? phash(query.__key, stringifyVariables(vars)) >>> 0
+      : query.__key,
     query,
     variables: vars || ({} as Variables),
   };
diff --git a/packages/core/src/utils/typenames.ts b/packages/core/src/utils/typenames.ts
index 5369e3a7..60c56f16 100755
--- a/packages/core/src/utils/typenames.ts
+++ b/packages/core/src/utils/typenames.ts
@@ -7,6 +7,8 @@ import {
   visit,
 } from 'graphql';
 
+import { KeyedDocumentNode, keyDocument } from './request';
+
 interface EntityLike {
   [key: string]: EntityLike | EntityLike[] | any;
   __typename: string | null | void;
@@ -38,7 +40,10 @@ const formatNode = (node: FieldNode | InlineFragmentNode) => {
   if (
     node.selectionSet &&
     !node.selectionSet.selections.some(
-      node => node.kind === Kind.FIELD && node.name.value === '__typename'
+      node =>
+        node.kind === Kind.FIELD &&
+        node.name.value === '__typename' &&
+        !node.alias
     )
   ) {
     return {
@@ -60,13 +65,21 @@ const formatNode = (node: FieldNode | InlineFragmentNode) => {
   }
 };
 
+const formattedDocs = new Map<number, KeyedDocumentNode>();
+
 export const formatDocument = <T extends DocumentNode>(node: T): T => {
-  const result = visit(node, {
-    Field: formatNode,
-    InlineFragment: formatNode,
-  });
+  const query = keyDocument(node);
+
+  let result = formattedDocs.get(query.__key);
+  if (!result) {
+    result = visit(query, {
+      Field: formatNode,
+      InlineFragment: formatNode,
+    }) as KeyedDocumentNode;
+    // Ensure that the hash of the resulting document won't suddenly change
+    result.__key = query.__key;
+    formattedDocs.set(query.__key, result);
+  }
 
-  // Ensure that the hash of the resulting document won't suddenly change
-  result.__key = (node as any).__key;
-  return result;
+  return (result as unknown) as T;
 };


Test patch:
diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts
index 072f8a59..605dcfc7 100644
--- a/exchanges/graphcache/src/cacheExchange.test.ts
+++ b/exchanges/graphcache/src/cacheExchange.test.ts
@@ -1011,6 +1011,7 @@ describe('optimistic updates', () => {
     expect(reexec).toHaveBeenCalledTimes(1);
 
     jest.runAllTimers();
+
     expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2);
     expect(response).toHaveBeenCalledTimes(2);
     expect(result).toHaveBeenCalledTimes(4);
@@ -1456,7 +1457,6 @@ describe('schema awareness', () => {
     expect(reexec).toHaveBeenCalledTimes(1);
     expect(result.mock.calls[1][0].stale).toBe(true);
     expect(result.mock.calls[1][0].data).toEqual({
-      __typename: 'Query',
       todos: [
         {
           __typename: 'Todo',
diff --git a/exchanges/graphcache/src/extras/relayPagination.test.ts b/exchanges/graphcache/src/extras/relayPagination.test.ts
index fe75ad0a..7b74d79a 100644
--- a/exchanges/graphcache/src/extras/relayPagination.test.ts
+++ b/exchanges/graphcache/src/extras/relayPagination.test.ts
@@ -20,6 +20,7 @@ function itemEdge(numItem: number) {
 it('works with forward pagination', () => {
   const Pagination = gql`
     query($cursor: String) {
+      __typename
       items(first: 1, after: $cursor) {
         __typename
         edges {
@@ -97,6 +98,7 @@ it('works with forward pagination', () => {
 it('works with backwards pagination', () => {
   const Pagination = gql`
     query($cursor: String) {
+      __typename
       items(last: 1, before: $cursor) {
         __typename
         edges {
@@ -174,6 +176,7 @@ it('works with backwards pagination', () => {
 it('handles duplicate edges', () => {
   const Pagination = gql`
     query($cursor: String) {
+      __typename
       items(first: 2, after: $cursor) {
         __typename
         edges {
@@ -259,6 +262,7 @@ it('handles duplicate edges', () => {
 it('works with simultaneous forward and backward pagination (outwards merging)', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
@@ -389,6 +393,7 @@ it('works with simultaneous forward and backward pagination (outwards merging)',
 it('works with simultaneous forward and backward pagination (inwards merging)', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
@@ -652,7 +657,9 @@ it('returns an empty array of edges when the cache has zero edges stored', () =>
 it('returns other fields on the same level as the edges', () => {
   const Pagination = gql`
     query {
+      __typename
       items(first: 1) {
+        __typename
         totalCount
       }
     }
@@ -691,6 +698,7 @@ it('returns other fields on the same level as the edges', () => {
 it('returns a subset of the cached items if the query requests less items than the cached ones', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
@@ -754,6 +762,7 @@ it('returns a subset of the cached items if the query requests less items than t
 it("returns the cached items even if they don't fullfil the query", () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
@@ -821,6 +830,7 @@ it("returns the cached items even if they don't fullfil the query", () => {
 it('returns the cached items even when they come from a different query', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
@@ -884,6 +894,7 @@ it('returns the cached items even when they come from a different query', () =>
 it('caches and retrieves correctly queries with inwards pagination', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
@@ -951,6 +962,7 @@ it('caches and retrieves correctly queries with inwards pagination', () => {
 it('does not include a previous result when adding parameters', () => {
   const Pagination = gql`
     query($first: Int, $filter: String) {
+      __typename
       items(first: $first, filter: $filter) {
         __typename
         edges {
@@ -1033,6 +1045,7 @@ it('does not include a previous result when adding parameters', () => {
 it('Works with edges absent from query', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         nodes {
@@ -1088,6 +1101,7 @@ it('Works with edges absent from query', () => {
 it('Works with nodes absent from query', () => {
   const Pagination = gql`
     query($first: Int, $last: Int, $before: String, $after: String) {
+      __typename
       items(first: $first, last: $last, before: $before, after: $after) {
         __typename
         edges {
diff --git a/exchanges/graphcache/src/extras/simplePagination.test.ts b/exchanges/graphcache/src/extras/simplePagination.test.ts
index fd2e60b8..51ffdf70 100644
--- a/exchanges/graphcache/src/extras/simplePagination.test.ts
+++ b/exchanges/graphcache/src/extras/simplePagination.test.ts
@@ -6,6 +6,7 @@ import { simplePagination } from './simplePagination';
 it('works with forward pagination', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number) {
+      __typename
       persons(skip: $skip, limit: $limit) {
         __typename
         id
@@ -76,6 +77,7 @@ it('works with forward pagination', () => {
 it('works with backwards pagination', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number) {
+      __typename
       persons(skip: $skip, limit: $limit) {
         __typename
         id
@@ -146,6 +148,7 @@ it('works with backwards pagination', () => {
 it('handles duplicates', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number) {
+      __typename
       persons(skip: $skip, limit: $limit) {
         __typename
         id
@@ -204,6 +207,7 @@ it('handles duplicates', () => {
 it('should not return previous result when adding a parameter', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number, $filter: String) {
+      __typename
       persons(skip: $skip, limit: $limit, filter: $filter) {
         __typename
         id
@@ -255,6 +259,7 @@ it('should not return previous result when adding a parameter', () => {
 it('should preserve the correct order in forward pagination', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number) {
+      __typename
       persons(skip: $skip, limit: $limit) {
         __typename
         id
@@ -313,6 +318,7 @@ it('should preserve the correct order in forward pagination', () => {
 it('should preserve the correct order in backward pagination', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number) {
+      __typename
       persons(skip: $skip, limit: $limit) {
         __typename
         id
@@ -362,6 +368,7 @@ it('should preserve the correct order in backward pagination', () => {
     query: Pagination,
     variables: { skip: 3, limit: 3 },
   });
+
   expect(result.data).toEqual({
     __typename: 'Query',
     persons: [...pageTwo.persons, ...pageOne.persons],
@@ -371,6 +378,7 @@ it('should preserve the correct order in backward pagination', () => {
 it('prevents overlapping of pagination on different arguments', () => {
   const Pagination = gql`
     query($skip: Number, $limit: Number, $filter: string) {
+      __typename
       persons(skip: $skip, limit: $limit, filter: $filter) {
         __typename
         id
diff --git a/exchanges/graphcache/src/offlineExchange.test.ts b/exchanges/graphcache/src/offlineExchange.test.ts
index 048e23a0..5c5dea54 100644
--- a/exchanges/graphcache/src/offlineExchange.test.ts
+++ b/exchanges/graphcache/src/offlineExchange.test.ts
@@ -33,6 +33,7 @@ const queryOne = gql`
     authors {
       id
       name
+      __typename
     }
   }
 `;
@@ -41,9 +42,9 @@ const queryOneData = {
   __typename: 'Query',
   authors: [
     {
-      __typename: 'Author',
       id: '123',
       name: 'Me',
+      __typename: 'Author',
     },
   ],
 };
@@ -189,7 +190,6 @@ describe('offline', () => {
     next(queryOp);
     expect(result).toBeCalledTimes(2);
     expect(result.mock.calls[1][0].data).toEqual({
-      __typename: 'Query',
       authors: [{ id: '123', name: 'URQL', __typename: 'Author' }],
     });
   });
diff --git a/exchanges/graphcache/src/operations/query.test.ts b/exchanges/graphcache/src/operations/query.test.ts
index 4eb14dfa..be81ebc3 100644
--- a/exchanges/graphcache/src/operations/query.test.ts
+++ b/exchanges/graphcache/src/operations/query.test.ts
@@ -55,7 +55,6 @@ describe('Query', () => {
     const result = query(store, { query: TODO_QUERY });
     expect(result.partial).toBe(true);
     expect(result.data).toEqual({
-      __typename: 'Query',
       todos: [
         {
           id: '0',
@@ -135,7 +134,9 @@ describe('Query', () => {
   it('should not crash for valid queries', () => {
     const VALID_QUERY = gql`
       query getTodos {
+        __typename
         todos {
+          __typename
           id
           text
         }
@@ -162,14 +163,17 @@ describe('Query', () => {
       todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }],
     });
 
-    expect(console.warn).not.toHaveBeenCalled();
+    // The warning should be called for `__typename`
+    expect(console.warn).toHaveBeenCalledTimes(1);
     expect(console.error).not.toHaveBeenCalled();
   });
 
   it('should respect altered root types', () => {
     const QUERY = gql`
       query getTodos {
+        __typename
         todos {
+          __typename
           id
           text
         }
@@ -203,7 +207,9 @@ describe('Query', () => {
   it('should allow subsequent read when first result was null', () => {
     const QUERY_WRITE = gql`
       query writeTodos {
+        __typename
         todos {
+          __typename
           ...ValidRead
         }
       }
@@ -215,10 +221,13 @@ describe('Query', () => {
 
     const QUERY_READ = gql`
       query getTodos {
+        __typename
         todos {
+          __typename
           ...MissingRead
         }
         todos {
+          __typename
           id
         }
       }
diff --git a/exchanges/graphcache/src/operations/write.test.ts b/exchanges/graphcache/src/operations/write.test.ts
index 44e65ce6..b6e4a4d4 100644
--- a/exchanges/graphcache/src/operations/write.test.ts
+++ b/exchanges/graphcache/src/operations/write.test.ts
@@ -148,24 +148,7 @@ describe('Query', () => {
       }
     );
     // Because of us indicating Todo:Writer as a scalar
-    expect(console.warn).toHaveBeenCalledTimes(1);
-    write(
-      store,
-      { query: INVALID_TODO_QUERY },
-      {
-        __typename: 'Mutation',
-        toggleTodo: {
-          __typename: 'Todo',
-          id: '0',
-          text: 'Teach',
-          writer: {
-            id: '0',
-          },
-        },
-      }
-    );
-
-    expect(console.warn).toHaveBeenCalledTimes(1);
+    expect(console.warn).toHaveBeenCalledTimes(2);
     expect((console.warn as any).mock.calls[0][0]).toMatch(
       /The field `writer` does not exist on `Todo`/
     );
diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index 60d00693..43c81648 100644
--- a/exchanges/graphcache/src/store/store.test.ts
+++ b/exchanges/graphcache/src/store/store.test.ts
@@ -2,6 +2,7 @@
 
 import gql from 'graphql-tag';
 import { minifyIntrospectionQuery } from '@urql/introspection';
+import { maskTypename } from '@urql/core';
 import { mocked } from 'ts-jest/utils';
 
 import { Data, StorageAdapter } from '../types';
@@ -14,6 +15,7 @@ import { getIntrospectionQuery, parse } from 'graphql';
 
 const Appointment = gql`
   query appointment($id: String) {
+    __typename
     appointment(id: $id) {
       __typename
       id
@@ -41,6 +43,7 @@ const Todos = gql`
 
 const TodosWithoutTypename = gql`
   query {
+    __typename
     todos {
       id
       text
@@ -87,7 +90,10 @@ describe('Store', () => {
     // NOTE: This is the query without __typename annotations
     write(store, { query: TodosWithoutTypename }, todosData);
     const result = query(store, { query: TodosWithoutTypename });
-    expect(result.data).toEqual(todosData);
+    expect(result.data).toEqual({
+      ...maskTypename(todosData),
+      __typename: 'Query',
+    });
   });
 });
 
@@ -473,6 +479,7 @@ describe('Store with OptimisticMutationConfig', () => {
           id
           text
           complete
+          __typename
         }
       `,
       { id: '0' }
@@ -714,6 +721,7 @@ describe('Store with storage', () => {
   it('should be able to persist embedded data', () => {
     const EmbeddedAppointment = gql`
       query appointment($id: String) {
+        __typename
         appointment(id: $id) {
           __typename
           info
@@ -803,7 +811,7 @@ describe('Store with storage', () => {
     InMemoryData.clearDataState();
   });
 
-  it("should warn if an optimistic field doesn't exist in the schema's mutations", function () {
+  it("should warn if an optimistic field doesn't exist in the schema's mutations", () => {
     new Store({
       schema: minifyIntrospectionQuery(
         require('../test-utils/simple_schema.json')
@@ -876,4 +884,35 @@ describe('Store with storage', () => {
     query(store, { query: parse(getIntrospectionQuery()) }, schema);
     expect(console.warn).toBeCalledTimes(0);
   });
+
+  it('should warn when __typename is missing when store.writeFragment is called', () => {
+    InMemoryData.initDataState('write', store.data, null);
+
+    store.writeFragment(
+      parse(`
+        fragment _ on Test {
+          __typename
+          id
+          sub {
+            id
+          }
+        }
+      `),
+      {
+        id: 'test',
+        sub: {
+          id: 'test',
+        },
+      }
+    );
+
+    InMemoryData.clearDataState();
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      "Couldn't find __typename when writing.\nIf you're writing to the cache manually have to pass a `__typename` property on each entity in your data."
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#14');
+  });
 });
diff --git a/exchanges/graphcache/src/test-utils/examples-1.test.ts b/exchanges/graphcache/src/test-utils/examples-1.test.ts
index 0dff0159..4378c408 100644
--- a/exchanges/graphcache/src/test-utils/examples-1.test.ts
+++ b/exchanges/graphcache/src/test-utils/examples-1.test.ts
@@ -63,6 +63,10 @@ const NestedClearNameTodo = gql`
   }
 `;
 
+afterEach(() => {
+  expect(console.warn).not.toHaveBeenCalled();
+});
+
 it('passes the "getting-started" example', () => {
   const store = new Store();
   const todosData = {
@@ -146,6 +150,7 @@ it('resolves missing, nullable arguments on fields', () => {
 
   const GetWithVariables = gql`
     query {
+      __typename
       todo(first: null) {
         __typename
         id
@@ -155,6 +160,7 @@ it('resolves missing, nullable arguments on fields', () => {
 
   const GetWithoutVariables = gql`
     query {
+      __typename
       todo {
         __typename
         id
@@ -203,7 +209,6 @@ it('should link entities', () => {
       id: '0',
       text: 'Go to the shops',
       complete: false,
-      __typename: 'Todo',
     },
   });
 });
diff --git a/exchanges/graphcache/src/test-utils/examples-2.test.ts b/exchanges/graphcache/src/test-utils/examples-2.test.ts
index 4eb674f2..982f3f6c 100644
--- a/exchanges/graphcache/src/test-utils/examples-2.test.ts
+++ b/exchanges/graphcache/src/test-utils/examples-2.test.ts
@@ -51,6 +51,10 @@ const Pagination = gql`
   }
 `;
 
+afterEach(() => {
+  expect(console.warn).not.toHaveBeenCalled();
+});
+
 it('allows custom resolvers to resolve nested, unkeyed data', () => {
   const store = new Store({
     resolvers: {
@@ -98,7 +102,6 @@ it('allows custom resolvers to resolve nested, unkeyed data', () => {
   expect(res.partial).toBe(false);
 
   expect(res.data).toEqual({
-    __typename: 'Query',
     todos: {
       __typename: 'TodosConnection',
       edges: [
@@ -161,7 +164,6 @@ it('allows custom resolvers to resolve nested, unkeyed data with embedded links'
   const res = query(store, { query: Pagination });
   expect(res.partial).toBe(false);
   expect(res.data).toEqual({
-    __typename: 'Query',
     todos: {
       __typename: 'TodosConnection',
       edges: [
@@ -186,6 +188,9 @@ it('allows custom resolvers to resolve nested, unkeyed data with embedded links'
 
 it('allows custom resolvers to resolve mixed data (keyable and unkeyable)', () => {
   const store = new Store({
+    keys: {
+      TodoDetails: () => null,
+    },
     resolvers: {
       Query: {
         todo: () => ({
@@ -224,7 +229,6 @@ it('allows custom resolvers to resolve mixed data (keyable and unkeyable)', () =
   expect(res.partial).toBe(false);
   expect(res.dependencies).toHaveProperty('Author:x', true);
   expect(res.data).toEqual({
-    __typename: 'Query',
     todo: {
       __typename: 'Todo',
       id: '1',
diff --git a/exchanges/graphcache/src/test-utils/examples-3.test.ts b/exchanges/graphcache/src/test-utils/examples-3.test.ts
index 906f02ca..7ed83020 100644
--- a/exchanges/graphcache/src/test-utils/examples-3.test.ts
+++ b/exchanges/graphcache/src/test-utils/examples-3.test.ts
@@ -2,6 +2,10 @@ import gql from 'graphql-tag';
 import { query, write } from '../operations';
 import { Store } from '../store';
 
+afterEach(() => {
+  expect(console.warn).not.toHaveBeenCalled();
+});
+
 it('allows viewer fields to overwrite the root Query data', () => {
   const store = new Store();
   const get = gql`
@@ -47,7 +51,6 @@ it('allows viewer fields to overwrite the root Query data', () => {
 
   expect(res.partial).toBe(false);
   expect(res.data).toEqual({
-    __typename: 'Query',
     int: 43,
   });
 });
diff --git a/exchanges/graphcache/src/test-utils/suite.test.ts b/exchanges/graphcache/src/test-utils/suite.test.ts
index 0a8fe5a5..1f8958bc 100644
--- a/exchanges/graphcache/src/test-utils/suite.test.ts
+++ b/exchanges/graphcache/src/test-utils/suite.test.ts
@@ -256,6 +256,7 @@ it('nested entity list on query', () => {
   expectCacheIntegrity({
     query: gql`
       {
+        __typename
         items {
           __typename
           id
@@ -377,6 +378,7 @@ it('embedded objects on entities', () => {
   expectCacheIntegrity({
     query: gql`
       {
+        __typename
         user {
           __typename
           id
diff --git a/packages/core/src/utils/typenames.test.ts b/packages/core/src/utils/typenames.test.ts
index 7d8c75ea..712b49d8 100755
--- a/packages/core/src/utils/typenames.test.ts
+++ b/packages/core/src/utils/typenames.test.ts
@@ -50,7 +50,7 @@ describe('formatTypeNames', () => {
   });
 
   it('preserves custom properties', () => {
-    const doc = parse(`{ id todos { id } }`) as any;
+    const doc = parse(`{ todos { id } }`) as any;
     doc.documentId = '123';
     expect((formatDocument(doc) as any).documentId).toBe(doc.documentId);
   });
@@ -85,6 +85,26 @@ describe('formatTypeNames', () => {
                   "
             `);
   });
+
+  it('does add typenames when it is aliased', () => {
+    expect(
+      formatTypeNames(`{
+      todos {
+        id
+        typename: __typename
+      }
+    }`)
+    ).toMatchInlineSnapshot(`
+                  "{
+                    todos {
+                      id
+                      typename: __typename
+                      __typename
+                    }
+                  }
+                  "
+            `);
+  });
 });
 
 describe('collectTypesFromResponse', () => {