Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

(graphcache) - Add cache.link method for writing links (#1551)

* Add store.link API to Graphcache

* Implement full link resolution and warning for cache.link

* Add cache.link to Graphcache's API docs

* Add changeset

* Update "Cache Updates" page

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

Changed files
+256 -25
.changeset
docs
exchanges
graphcache
src
+5
.changeset/moody-mice-arrive.md
···
+
---
+
'@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.
+33
docs/api/graphcache.md
···
[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
+51 -16
docs/graphcache/cache-updates.md
···
},
},
},
-
})
+
});
```
An "updater" may be attached to a `Mutation` or `Subscription` field and accepts four positional
···
following:
```graphql
-
mutation UpdateTodo ($todoId: ID!, $date: String!) {
+
mutation UpdateTodo($todoId: ID!, $date: String!) {
updateTodoDate(id: $todoId, date: $date)
}
```
···
}
`;
-
cache.writeFragment(
-
fragment,
-
{ id: args.id, updatedAt: args.date },
-
);
+
cache.writeFragment(fragment, { id: args.id, updatedAt: args.date });
},
},
},
···
> [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
···
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
···
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);
···
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
···
UI code.
```graphql
-
mutation RemoveTodo ($id: ID!) {
+
mutation RemoveTodo($id: ID!) {
removeTodo(id: $id)
}
```
···
know the fields that should be checked:
```graphql
-
query PaginatedTodos ($skip: Int) {
+
query PaginatedTodos($skip: Int) {
todos(skip: $skip) {
id
text
···
```js
cache.inspectFields({
__typename: 'Todo',
-
id: args.id
+
id: args.id,
});
```
···
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);
···
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
+8 -7
docs/graphcache/errors.md
···
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
···
## (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
+34 -1
exchanges/graphcache/src/operations/shared.ts
···
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;
···
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;
+
};
+84
exchanges/graphcache/src/store/store.test.ts
···
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',
+
},
+
});
+
});
+31 -1
exchanges/graphcache/src/store/store.ts
···
DataField,
Variables,
FieldArgs,
+
Link,
Data,
QueryInput,
UpdatesConfig,
···
} 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';
···
variables?: V
): 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)
+
);
+
}
}
}
+10
exchanges/graphcache/src/types.ts
···
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 =