Repository: FormidableLabs/urql
Pull request #1551 (2021-04-20 08:22:13)

(graphcache) - Add cache.link method for writing links
## Summary

This allows the user to pass in a rootKey or entity from which we'll derive the rootKey for instance Query or { __typename:'Todo', id: 1 }, secondly we accept a field, this can be for instance author which will be applied to the rootKey. As a thirth argument we accept the entity or entities to be written as keys to the rootKey.field, for example { __typename: 'Author', id: 1 } and as a last argument we accept fieldArguments, these will be applied to the fieldKey and will help with fields that have their own arguments for instance: Todo:1.authors({ first: 10 }).

Resolves: #1485

## Set of changes

- Adds `cache.link` to graphCache to be used in updaters


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: d03107ac90e49ad09384ccde3469ad85197b5e83

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/FormidableLabs/urql/new/graphcache-link?filename=.changeset/unlucky-ants-hug.md&value=---%0A%22%40urql%2Fexchange-graphcache%22%3A%20patch%0A---%0A%0A(graphCache)%20-%20add%20a%20link%20operation%20to%20the%20%60cache%60%20parameter%20of%20%60updates%60%0A)


kitten: The bundle size is going to change from `7.16kB` (main) to `7.24kB` minzipped. I think it's a little odd that this increase is a little larger in particular, but we could still look into that if we wanted to.

Patch:
diff --git a/.changeset/moody-mice-arrive.md b/.changeset/moody-mice-arrive.md
new file mode 100644
index 00000000..33b7bd36
--- /dev/null
+++ b/.changeset/moody-mice-arrive.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': minor
+---
+
+Add `cache.link(...)` method to Graphcache. This method may be used in updaters to update links in the cache. It is hence the writing-equivalent of `cache.resolve()`, which previously didn't have any equivalent as such, which meant that only `cache.updateQuery` or `cache.writeFragment` could be used, even to update simple relations.
diff --git a/docs/api/graphcache.md b/docs/api/graphcache.md
index 0b204566..2d61b81e 100644
--- a/docs/api/graphcache.md
+++ b/docs/api/graphcache.md
@@ -371,6 +371,39 @@ cache.readQuery({
 [Read more about using `readQuery` on the ["Local Resolvers"
 page.](../graphcache/local-resolvers.md#reading-a-query)
 
+### link
+
+Corresponding to [`cache.resolve`](#resolve), the `cache.link` method allows
+links in the cache to be updated. While the `cache.resolve` method reads both
+records and links from the cache, the `cache.link` method will only ever write
+links as fragments (See [`cache.writeFragment`](#writefragment) below) are more
+suitable for updating scalar data in the cache.
+
+The arguments for `cache.link` are identical to [`cache.resolve`](#resolve) and
+the field's arguments are optional. However, the last argument must always be
+a link, meaning `null`, an entity key, a keyable entity, or a list of these.
+
+In other words, `cache.link` accepts an entity to write to as its first argument,
+with the same arguments as `cache.keyOfEntity`. It then accepts one or two arguments
+that are passed to `cache.keyOfField` to get the targeted field key. And lastly,
+you may pass a list or a single entity (or an entity key).
+
+```js
+// Link Query.todo field to a todo item
+cache.link({ __typename: 'Query' }, 'todo', { __typename: 'Todo', id: 1 });
+
+// You may also pass arguments instead:
+cache.link({ __typename: 'Query' }, 'todo', { id: 1 }, { __typename: 'Todo', id: 1 });
+
+// Or use entity keys instead of the entities themselves:
+cache.link('Query', 'todo', cache.keyOfEntity({ __typename: 'Todo', id: 1 }));
+```
+
+The method may [output a
+warning](../graphcache/errors.md#12-cant-generate-a-key-for-writefragment-or-link) when any of the
+entities were passed as objects but aren't keyable, which is useful when a scalar or a non-keyable
+object have been passed to `cache.link` accidentally.
+
 ### writeFragment
 
 Corresponding to [`cache.readFragment`](#readfragments), the `cache.writeFragment` method allows
diff --git a/docs/graphcache/cache-updates.md b/docs/graphcache/cache-updates.md
index a5b9677f..46a49906 100644
--- a/docs/graphcache/cache-updates.md
+++ b/docs/graphcache/cache-updates.md
@@ -56,7 +56,7 @@ cacheExchange({
       },
     },
   },
-})
+});
 ```
 
 An "updater" may be attached to a `Mutation` or `Subscription` field and accepts four positional
@@ -86,7 +86,7 @@ for Graphcache to update said entity automatically. For instance, we may have a
 following:
 
 ```graphql
-mutation UpdateTodo ($todoId: ID!, $date: String!) {
+mutation UpdateTodo($todoId: ID!, $date: String!) {
   updateTodoDate(id: $todoId, date: $date)
 }
 ```
@@ -126,10 +126,7 @@ cacheExchange({
           }
         `;
 
-        cache.writeFragment(
-          fragment,
-          { id: args.id, updatedAt: args.date },
-        );
+        cache.writeFragment(fragment, { id: args.id, updatedAt: args.date });
       },
     },
   },
@@ -144,10 +141,10 @@ for a given fragment it instead writes data to the cache.
 > [the `gql` tag function](../api/core.md#gql) because `writeFragment` only accepts
 > GraphQL `DocumentNode`s as inputs, and not strings.
 
-### Cache Updates outside updates
+### Cache Updates outside updaters
 
-Cache updates are **not** possible outside `updates`. If we attempt to store the `cache` in a
-variable and call its methods outside any `updates` functions (or functions, like `resolvers`)
+Cache updates are **not** possible outside `updates`'s functions. If we attempt to store the `cache`
+in a variable and call its methods outside any `updates` functions (or functions, like `resolvers`)
 then Graphcache will throw an error.
 
 Methods like these cannot be called outside the `cacheExchange`'s `updates` functions, because
@@ -171,7 +168,7 @@ sending a rather large API result. For large amounts of pages this is especially
 Instead, most schemas opt to instead just return the entity that's just been created:
 
 ```graphql
-mutation NewTodo ($text: String!) {
+mutation NewTodo($text: String!) {
   createTodo(id: $todoId, text: $text) {
     id
     text
@@ -187,7 +184,13 @@ cacheExchange({
   updates: {
     Mutation: {
       updateTodoDate(result, _args, cache, _info) {
-        const TodoList = gql`{ todos { id } }`;
+        const TodoList = gql`
+          {
+            todos {
+              id
+            }
+          }
+        `;
 
         cache.updateQuery({ query: TodoList }, data => {
           data.todos.push(result.createTodo);
@@ -213,6 +216,37 @@ updaters. All resolvers are ignored, so it becomes impossible to accidentally co
 to our cache. We could safely add a resolver for `Todo.createdAt` and wouldn't have to worry about
 an updater accidentally writing it to the cache's internal data structure.
 
+### Writing links individually
+
+As long as we're only updating links (as in 'relations') then we may also use the [`cache.link`
+method](../api/graphcache.md#link). This method is the "write equivalent" of [the `cache.resolve`
+method, as seen on the "Local Resolvers" page before.](./local-resolvers.md#resolving-other-fields)
+
+We can use this method to update any relation in our cache, so the example above could also be
+rewritten to use `cache.link` and `cache.resolve` rather than `cache.updateQuery`.
+
+```js
+cacheExchange({
+  updates: {
+    Mutation: {
+      updateTodoDate(result, _args, cache, _info) {
+        const todos = cache.resolve('Query', 'todos');
+        if (Array.isArray(todos)) {
+          todos.push(result.createTodo);
+          cache.link('Query', 'todos', todos);
+        }
+      },
+    },
+  },
+});
+```
+
+This method can be combined with more than just `cache.resolve`, for instance, it's a good fit with
+`cache.inspectFields`. However, when you're writing records (as in 'scalar' values)
+`cache.writeFragment` and `cache.updateQuery` are still the only methods that you can use.
+But since this kind of data is often written automatically by the normalized cache, often updating a
+link is the only modification we may want to make.
+
 ## Updating many unknown links
 
 In the previous section we've seen how to update data, like a list, when a mutation result enters
@@ -226,7 +260,7 @@ we've already accessed. This knowledge in fact _shouldn't_ be available to Graph
 UI code.
 
 ```graphql
-mutation RemoveTodo ($id: ID!) {
+mutation RemoveTodo($id: ID!) {
   removeTodo(id: $id)
 }
 ```
@@ -236,7 +270,7 @@ of these items over many pages with separate queries being sent to our API, whic
 know the fields that should be checked:
 
 ```graphql
-query PaginatedTodos ($skip: Int) {
+query PaginatedTodos($skip: Int) {
   todos(skip: $skip) {
     id
     text
@@ -309,7 +343,7 @@ in a partial `Todo` entity just as well:
 ```js
 cache.inspectFields({
   __typename: 'Todo',
-  id: args.id
+  id: args.id,
 });
 ```
 
@@ -369,7 +403,8 @@ cacheExchange({
     Mutation: {
       updateTodo(_result, args, cache, _info) {
         const key = 'Query';
-        const fields = cache.inspectFields(key)
+        const fields = cache
+          .inspectFields(key)
           .filter(field => field.fieldName === 'todos')
           .forEach(field => {
             cache.invalidate(key, field.fieldKey);
@@ -469,7 +504,7 @@ additional variables, which we may want to pass from our UI code to the mutation
 a mutation like the following we may add more variables than the mutation specifies:
 
 ```graphql
-mutation UpdateTodo ($id: ID!, $text: ID!) {
+mutation UpdateTodo($id: ID!, $text: ID!) {
   updateTodo(id: $id, text: $text) {
     id
     text
diff --git a/docs/graphcache/errors.md b/docs/graphcache/errors.md
index 7f939f56..29a071af 100644
--- a/docs/graphcache/errors.md
+++ b/docs/graphcache/errors.md
@@ -171,19 +171,20 @@ is maybe empty or does not contain fragments.
 When you're calling a fragment method, please ensure that you're only passing fragments
 in your GraphQL document. The first fragment will be used to start writing data.
 
-## (12) Can't generate a key for writeFragment(...)
+## (12) Can't generate a key for writeFragment(...) or link(...)
 
-> Can't generate a key for writeFragment(...) data.
+> Can't generate a key for writeFragment(...) [or link(...) data.
 > You have to pass an `id` or `_id` field or create a custom `keys` config for `???`.
 
-You probably have called `cache.writeFragment` with data that the cache can't generate a
-key for.
+You probably have called `cache.writeFragment` or `cache.link` with data that the cache
+can't generate a key for.
 
 This may either happen because you're missing the `id` or `_id` field or some other
 fields for your custom `keys` config.
 
 Please make sure that you include enough properties on your data so that `writeFragment`
-can generate a key.
+or `cache.link` can generate a key. On `cache.link` the entities must either be
+an existing entity key, or a keyable entity.
 
 ## (13) Invalid undefined
 
@@ -196,8 +197,8 @@ know the part of your result that 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.
+> 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
diff --git a/examples/with-graphcache-pagination/README.md b/examples/with-graphcache-pagination/README.md
new file mode 100644
index 00000000..2b599072
--- /dev/null
+++ b/examples/with-graphcache-pagination/README.md
@@ -0,0 +1,22 @@
+# Integrating with React
+
+Integrating urql is as simple as:
+
+1. Install packages [getting started](https://formidable.com/open-source/urql/docs/basics/react-preact/)
+
+```sh
+yarn add urql graphql
+# or
+npm install --save urql graphql
+```
+
+2. Install [graphcache](https://formidable.com/open-source/urql/docs/graphcache/)
+```sh
+yarn add @urql/exchange-graphcache
+# or
+npm install --save @urql/exchange-graphcache
+```
+
+3. Setting up the Client [here](src/App.js)
+
+4. Execute the Query [here](src/pages/PaginatedNpmSearch.js)
diff --git a/examples/with-graphcache-pagination/package.json b/examples/with-graphcache-pagination/package.json
new file mode 100644
index 00000000..eaa34b7f
--- /dev/null
+++ b/examples/with-graphcache-pagination/package.json
@@ -0,0 +1,39 @@
+{
+  "name": "react-urql-query",
+  "version": "1.0.0",
+  "private": true,
+  "dependencies": {
+    "@urql/exchange-graphcache": "^4.0.0",
+    "graphql": "^15.5.0",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2",
+    "urql": "^2.0.2"
+  },
+  "devDependencies": {
+    "react-scripts": "4.0.3"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}
diff --git a/examples/with-graphcache-pagination/src/App.js b/examples/with-graphcache-pagination/src/App.js
new file mode 100644
index 00000000..835d1ff2
--- /dev/null
+++ b/examples/with-graphcache-pagination/src/App.js
@@ -0,0 +1,30 @@
+import { createClient, Provider, dedupExchange, fetchExchange } from "urql";
+import { cacheExchange } from '@urql/exchange-graphcache';
+import { relayPagination } from '@urql/exchange-graphcache/extras';
+
+import PaginatedNpmSearch from "./pages/PaginatedNpmSearch";
+
+const client = createClient({
+  url: "https://trygql.dev/graphql/relay-npm",
+  exchanges: [
+    dedupExchange,
+    cacheExchange({
+      resolvers: {
+        Query: {
+          search: relayPagination(),
+        },
+      },
+    }),
+    fetchExchange
+  ]
+});
+
+function App() {
+  return (
+    <Provider value={client}>
+      <PaginatedNpmSearch />
+    </Provider>
+  );
+}
+
+export default App;
diff --git a/examples/with-graphcache-pagination/src/index.css b/examples/with-graphcache-pagination/src/index.css
new file mode 100644
index 00000000..ec2585e8
--- /dev/null
+++ b/examples/with-graphcache-pagination/src/index.css
@@ -0,0 +1,13 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}
diff --git a/examples/with-graphcache-pagination/src/index.js b/examples/with-graphcache-pagination/src/index.js
new file mode 100644
index 00000000..62adcf3c
--- /dev/null
+++ b/examples/with-graphcache-pagination/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  document.getElementById('root')
+);
+
diff --git a/examples/with-graphcache-pagination/src/pages/PaginatedNpmSearch.js b/examples/with-graphcache-pagination/src/pages/PaginatedNpmSearch.js
new file mode 100644
index 00000000..01ee78b7
--- /dev/null
+++ b/examples/with-graphcache-pagination/src/pages/PaginatedNpmSearch.js
@@ -0,0 +1,60 @@
+import React, { useState } from "react";
+import { gql, useQuery } from "urql";
+
+const limit = 5;
+const query = "graphql";
+
+const NPM_SEARCH = gql`
+  query Search($query: String!, $first: Int!, $after: String) {
+    search(query: $query, first: $first, after: $after) {
+      edges {
+        node {
+          id
+          name
+        }
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+`;
+
+
+const PaginatedNpmSearch = () => {
+  const [after, setAfter] = useState("");
+
+  const [result] = useQuery({
+    query: NPM_SEARCH, 
+    variables: { query, first: limit, after } 
+  });
+
+  const { data, fetching, error } = result;
+
+  const searchResults = data?.search;
+
+  return (
+    <div>
+      {error && <p>Oh no... {error.message}</p>}
+
+      {fetching && <p>Loading...</p>}
+
+      {searchResults && (
+        <>
+          {searchResults.edges.map(({ node }) => (
+            <div key={node.id}>{node.id}: {node.name}</div>
+          ))}
+
+          {searchResults.pageInfo.hasNextPage && (
+            <button onClick={() => setAfter(searchResults.pageInfo.endCursor)}>
+              load more
+            </button>
+          )}
+        </>
+      )}
+    </div>
+  );
+};
+
+export default PaginatedNpmSearch;
diff --git a/examples/with-pagination/README.md b/examples/with-pagination/README.md
index d3da29c4..12d1c63b 100644
--- a/examples/with-pagination/README.md
+++ b/examples/with-pagination/README.md
@@ -12,4 +12,4 @@ npm install --save urql graphql
 
 2. Setting up the Client [here](src/App.js)
 
-3. Execute the Query [here](src/pages/PokemonList.js)
+3. Execute the Query [here](src/pages/PaginatedNpmSearch.js)
diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts
index d4b79e08..2b883440 100644
--- a/exchanges/graphcache/src/operations/shared.ts
+++ b/exchanges/graphcache/src/operations/shared.ts
@@ -18,9 +18,18 @@ import {
 import { warn, pushDebugNode, popDebugNode } from '../helpers/help';
 import { hasField } from '../store/data';
 import { Store, keyOfField } from '../store';
-import { Fragments, Variables, DataField, NullArray, Data } from '../types';
 import { getFieldArguments, shouldInclude, isInterfaceOfType } from '../ast';
 
+import {
+  Fragments,
+  Variables,
+  DataField,
+  NullArray,
+  Link,
+  Entity,
+  Data,
+} from '../types';
+
 export interface Context {
   store: Store;
   variables: Variables;
@@ -204,3 +213,27 @@ export const makeSelectionIterator = (
 
 export const ensureData = (x: DataField): Data | NullArray<Data> | null =>
   x === undefined ? null : (x as Data | NullArray<Data>);
+
+export const ensureLink = (store: Store, ref: Link<Entity>): Link => {
+  if (ref == null) {
+    return ref;
+  } else if (Array.isArray(ref)) {
+    const link = new Array(ref.length);
+    for (let i = 0, l = link.length; i < l; i++)
+      link[i] = ensureLink(store, ref[i]);
+    return link;
+  }
+
+  const link = store.keyOfEntity(ref);
+  if (!link && ref && typeof ref === 'object') {
+    warn(
+      "Can't generate a key for link(...) item." +
+        '\nYou have to pass an `id` or `_id` field or create a custom `keys` config for `' +
+        ref.__typename +
+        '`.',
+      12
+    );
+  }
+
+  return link;
+};
diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts
index 306f17c0..8dcfbddb 100644
--- a/exchanges/graphcache/src/store/store.ts
+++ b/exchanges/graphcache/src/store/store.ts
@@ -8,6 +8,7 @@ import {
   DataField,
   Variables,
   FieldArgs,
+  Link,
   Data,
   QueryInput,
   UpdatesConfig,
@@ -18,7 +19,7 @@ import {
 } from '../types';
 
 import { invariant } from '../helpers/help';
-import { contextRef } from '../operations/shared';
+import { contextRef, ensureLink } from '../operations/shared';
 import { read, readFragment } from '../operations/query';
 import { writeFragment, startWrite } from '../operations/write';
 import { invalidateEntity } from '../operations/invalidate';
@@ -203,4 +204,33 @@ export class Store implements Cache {
   ): void {
     writeFragment(this, formatDocument(fragment), data, variables as any);
   }
+
+  link(
+    entity: Entity,
+    field: string,
+    args: FieldArgs,
+    link: Link<Entity>
+  ): void;
+
+  link(entity: Entity, field: string, link: Link<Entity>): void;
+
+  link(
+    entity: Entity,
+    field: string,
+    argsOrLink: FieldArgs | Link<Entity>,
+    maybeLink?: Link<Entity>
+  ): void {
+    const args = (maybeLink !== undefined ? argsOrLink : null) as FieldArgs;
+    const link = (maybeLink !== undefined
+      ? maybeLink
+      : argsOrLink) as Link<Entity>;
+    const entityKey = ensureLink(this, entity);
+    if (typeof entityKey === 'string') {
+      InMemoryData.writeLink(
+        entityKey,
+        keyOfField(field, args),
+        ensureLink(this, link)
+      );
+    }
+  }
 }
diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts
index 1016e3fd..56ba4ecf 100644
--- a/exchanges/graphcache/src/types.ts
+++ b/exchanges/graphcache/src/types.ts
@@ -117,6 +117,16 @@ export interface Cache {
     data: T,
     variables?: V
   ): void;
+
+  /** link() can be used to update a given entity field to link to another entity or entities */
+  link(
+    entity: Entity,
+    field: string,
+    args: FieldArgs,
+    link: Link<Entity>
+  ): void;
+  /** link() can be used to update a given entity field to link to another entity or entities */
+  link(entity: Entity, field: string, value: Link<Entity>): void;
 }
 
 type ResolverResult =


Test patch:
diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index e155fc95..4be3ca6b 100644
--- a/exchanges/graphcache/src/store/store.test.ts
+++ b/exchanges/graphcache/src/store/store.test.ts
@@ -967,3 +967,87 @@ describe('Store with storage', () => {
     expect(warnMessage).toContain('https://bit.ly/2XbVrpR#14');
   });
 });
+
+it('should link up entities', () => {
+  const store = new Store();
+  const todo = gql`
+    query test {
+      todo(id: "1") {
+        id
+        title
+        __typename
+      }
+    }
+  `;
+  const author = gql`
+    query testAuthor {
+      author(id: "1") {
+        id
+        name
+        __typename
+      }
+    }
+  `;
+  write(
+    store,
+    {
+      query: todo,
+    },
+    {
+      todo: {
+        id: '1',
+        title: 'learn urql',
+        __typename: 'Todo',
+      },
+      __typename: 'Query',
+    } as any
+  );
+  let { data } = query(store, { query: todo });
+  expect((data as any).todo).toEqual({
+    id: '1',
+    title: 'learn urql',
+    __typename: 'Todo',
+  });
+  write(
+    store,
+    {
+      query: author,
+    },
+    {
+      author: { __typename: 'Author', id: '1', name: 'Formidable' },
+      __typename: 'Query',
+    } as any
+  );
+  InMemoryData.initDataState('write', store.data, null);
+  store.link((data as any).todo, 'author', {
+    __typename: 'Author',
+    id: '1',
+    name: 'Formidable',
+  });
+  InMemoryData.clearDataState();
+  const todoWithAuthor = gql`
+    query test {
+      todo(id: "1") {
+        id
+        title
+        __typename
+        author {
+          id
+          name
+          __typename
+        }
+      }
+    }
+  `;
+  ({ data } = query(store, { query: todoWithAuthor }));
+  expect((data as any).todo).toEqual({
+    id: '1',
+    title: 'learn urql',
+    __typename: 'Todo',
+    author: {
+      __typename: 'Author',
+      id: '1',
+      name: 'Formidable',
+    },
+  });
+});