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

Configure Feed

Select the types of activity you want to include in your feed.

(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>

authored by

Jovi De Croock
Phil Pluckthun
and committed by
GitHub
fd9dcf62 dc5e9d77

+256 -25
+5
.changeset/moody-mice-arrive.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + 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
··· 371 371 [Read more about using `readQuery` on the ["Local Resolvers" 372 372 page.](../graphcache/local-resolvers.md#reading-a-query) 373 373 374 + ### link 375 + 376 + Corresponding to [`cache.resolve`](#resolve), the `cache.link` method allows 377 + links in the cache to be updated. While the `cache.resolve` method reads both 378 + records and links from the cache, the `cache.link` method will only ever write 379 + links as fragments (See [`cache.writeFragment`](#writefragment) below) are more 380 + suitable for updating scalar data in the cache. 381 + 382 + The arguments for `cache.link` are identical to [`cache.resolve`](#resolve) and 383 + the field's arguments are optional. However, the last argument must always be 384 + a link, meaning `null`, an entity key, a keyable entity, or a list of these. 385 + 386 + In other words, `cache.link` accepts an entity to write to as its first argument, 387 + with the same arguments as `cache.keyOfEntity`. It then accepts one or two arguments 388 + that are passed to `cache.keyOfField` to get the targeted field key. And lastly, 389 + you may pass a list or a single entity (or an entity key). 390 + 391 + ```js 392 + // Link Query.todo field to a todo item 393 + cache.link({ __typename: 'Query' }, 'todo', { __typename: 'Todo', id: 1 }); 394 + 395 + // You may also pass arguments instead: 396 + cache.link({ __typename: 'Query' }, 'todo', { id: 1 }, { __typename: 'Todo', id: 1 }); 397 + 398 + // Or use entity keys instead of the entities themselves: 399 + cache.link('Query', 'todo', cache.keyOfEntity({ __typename: 'Todo', id: 1 })); 400 + ``` 401 + 402 + The method may [output a 403 + warning](../graphcache/errors.md#12-cant-generate-a-key-for-writefragment-or-link) when any of the 404 + entities were passed as objects but aren't keyable, which is useful when a scalar or a non-keyable 405 + object have been passed to `cache.link` accidentally. 406 + 374 407 ### writeFragment 375 408 376 409 Corresponding to [`cache.readFragment`](#readfragments), the `cache.writeFragment` method allows
+51 -16
docs/graphcache/cache-updates.md
··· 56 56 }, 57 57 }, 58 58 }, 59 - }) 59 + }); 60 60 ``` 61 61 62 62 An "updater" may be attached to a `Mutation` or `Subscription` field and accepts four positional ··· 86 86 following: 87 87 88 88 ```graphql 89 - mutation UpdateTodo ($todoId: ID!, $date: String!) { 89 + mutation UpdateTodo($todoId: ID!, $date: String!) { 90 90 updateTodoDate(id: $todoId, date: $date) 91 91 } 92 92 ``` ··· 126 126 } 127 127 `; 128 128 129 - cache.writeFragment( 130 - fragment, 131 - { id: args.id, updatedAt: args.date }, 132 - ); 129 + cache.writeFragment(fragment, { id: args.id, updatedAt: args.date }); 133 130 }, 134 131 }, 135 132 }, ··· 144 141 > [the `gql` tag function](../api/core.md#gql) because `writeFragment` only accepts 145 142 > GraphQL `DocumentNode`s as inputs, and not strings. 146 143 147 - ### Cache Updates outside updates 144 + ### Cache Updates outside updaters 148 145 149 - Cache updates are **not** possible outside `updates`. If we attempt to store the `cache` in a 150 - variable and call its methods outside any `updates` functions (or functions, like `resolvers`) 146 + Cache updates are **not** possible outside `updates`'s functions. If we attempt to store the `cache` 147 + in a variable and call its methods outside any `updates` functions (or functions, like `resolvers`) 151 148 then Graphcache will throw an error. 152 149 153 150 Methods like these cannot be called outside the `cacheExchange`'s `updates` functions, because ··· 171 168 Instead, most schemas opt to instead just return the entity that's just been created: 172 169 173 170 ```graphql 174 - mutation NewTodo ($text: String!) { 171 + mutation NewTodo($text: String!) { 175 172 createTodo(id: $todoId, text: $text) { 176 173 id 177 174 text ··· 187 184 updates: { 188 185 Mutation: { 189 186 updateTodoDate(result, _args, cache, _info) { 190 - const TodoList = gql`{ todos { id } }`; 187 + const TodoList = gql` 188 + { 189 + todos { 190 + id 191 + } 192 + } 193 + `; 191 194 192 195 cache.updateQuery({ query: TodoList }, data => { 193 196 data.todos.push(result.createTodo); ··· 213 216 to our cache. We could safely add a resolver for `Todo.createdAt` and wouldn't have to worry about 214 217 an updater accidentally writing it to the cache's internal data structure. 215 218 219 + ### Writing links individually 220 + 221 + As long as we're only updating links (as in 'relations') then we may also use the [`cache.link` 222 + method](../api/graphcache.md#link). This method is the "write equivalent" of [the `cache.resolve` 223 + method, as seen on the "Local Resolvers" page before.](./local-resolvers.md#resolving-other-fields) 224 + 225 + We can use this method to update any relation in our cache, so the example above could also be 226 + rewritten to use `cache.link` and `cache.resolve` rather than `cache.updateQuery`. 227 + 228 + ```js 229 + cacheExchange({ 230 + updates: { 231 + Mutation: { 232 + updateTodoDate(result, _args, cache, _info) { 233 + const todos = cache.resolve('Query', 'todos'); 234 + if (Array.isArray(todos)) { 235 + todos.push(result.createTodo); 236 + cache.link('Query', 'todos', todos); 237 + } 238 + }, 239 + }, 240 + }, 241 + }); 242 + ``` 243 + 244 + This method can be combined with more than just `cache.resolve`, for instance, it's a good fit with 245 + `cache.inspectFields`. However, when you're writing records (as in 'scalar' values) 246 + `cache.writeFragment` and `cache.updateQuery` are still the only methods that you can use. 247 + But since this kind of data is often written automatically by the normalized cache, often updating a 248 + link is the only modification we may want to make. 249 + 216 250 ## Updating many unknown links 217 251 218 252 In the previous section we've seen how to update data, like a list, when a mutation result enters ··· 226 260 UI code. 227 261 228 262 ```graphql 229 - mutation RemoveTodo ($id: ID!) { 263 + mutation RemoveTodo($id: ID!) { 230 264 removeTodo(id: $id) 231 265 } 232 266 ``` ··· 236 270 know the fields that should be checked: 237 271 238 272 ```graphql 239 - query PaginatedTodos ($skip: Int) { 273 + query PaginatedTodos($skip: Int) { 240 274 todos(skip: $skip) { 241 275 id 242 276 text ··· 309 343 ```js 310 344 cache.inspectFields({ 311 345 __typename: 'Todo', 312 - id: args.id 346 + id: args.id, 313 347 }); 314 348 ``` 315 349 ··· 369 403 Mutation: { 370 404 updateTodo(_result, args, cache, _info) { 371 405 const key = 'Query'; 372 - const fields = cache.inspectFields(key) 406 + const fields = cache 407 + .inspectFields(key) 373 408 .filter(field => field.fieldName === 'todos') 374 409 .forEach(field => { 375 410 cache.invalidate(key, field.fieldKey); ··· 469 504 a mutation like the following we may add more variables than the mutation specifies: 470 505 471 506 ```graphql 472 - mutation UpdateTodo ($id: ID!, $text: ID!) { 507 + mutation UpdateTodo($id: ID!, $text: ID!) { 473 508 updateTodo(id: $id, text: $text) { 474 509 id 475 510 text
+8 -7
docs/graphcache/errors.md
··· 171 171 When you're calling a fragment method, please ensure that you're only passing fragments 172 172 in your GraphQL document. The first fragment will be used to start writing data. 173 173 174 - ## (12) Can't generate a key for writeFragment(...) 174 + ## (12) Can't generate a key for writeFragment(...) or link(...) 175 175 176 - > Can't generate a key for writeFragment(...) data. 176 + > Can't generate a key for writeFragment(...) [or link(...) data. 177 177 > You have to pass an `id` or `_id` field or create a custom `keys` config for `???`. 178 178 179 - You probably have called `cache.writeFragment` with data that the cache can't generate a 180 - key for. 179 + You probably have called `cache.writeFragment` or `cache.link` with data that the cache 180 + can't generate a key for. 181 181 182 182 This may either happen because you're missing the `id` or `_id` field or some other 183 183 fields for your custom `keys` config. 184 184 185 185 Please make sure that you include enough properties on your data so that `writeFragment` 186 - can generate a key. 186 + or `cache.link` can generate a key. On `cache.link` the entities must either be 187 + an existing entity key, or a keyable entity. 187 188 188 189 ## (13) Invalid undefined 189 190 ··· 196 197 197 198 ## (14) Couldn't find \_\_typename when writing. 198 199 199 - > Couldn't find **typename when writing. 200 - > If you're writing to the cache manually have to pass a `**typename` property on each entity in your data. 200 + > Couldn't find `__typename` when writing. 201 + > If you're writing to the cache manually have to pass a `__typename` property on each entity in your data. 201 202 202 203 You probably have called `cache.writeFragment` or `cache.updateQuery` with data that is missing a 203 204 `__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
··· 18 18 import { warn, pushDebugNode, popDebugNode } from '../helpers/help'; 19 19 import { hasField } from '../store/data'; 20 20 import { Store, keyOfField } from '../store'; 21 - import { Fragments, Variables, DataField, NullArray, Data } from '../types'; 22 21 import { getFieldArguments, shouldInclude, isInterfaceOfType } from '../ast'; 22 + 23 + import { 24 + Fragments, 25 + Variables, 26 + DataField, 27 + NullArray, 28 + Link, 29 + Entity, 30 + Data, 31 + } from '../types'; 23 32 24 33 export interface Context { 25 34 store: Store; ··· 204 213 205 214 export const ensureData = (x: DataField): Data | NullArray<Data> | null => 206 215 x === undefined ? null : (x as Data | NullArray<Data>); 216 + 217 + export const ensureLink = (store: Store, ref: Link<Entity>): Link => { 218 + if (ref == null) { 219 + return ref; 220 + } else if (Array.isArray(ref)) { 221 + const link = new Array(ref.length); 222 + for (let i = 0, l = link.length; i < l; i++) 223 + link[i] = ensureLink(store, ref[i]); 224 + return link; 225 + } 226 + 227 + const link = store.keyOfEntity(ref); 228 + if (!link && ref && typeof ref === 'object') { 229 + warn( 230 + "Can't generate a key for link(...) item." + 231 + '\nYou have to pass an `id` or `_id` field or create a custom `keys` config for `' + 232 + ref.__typename + 233 + '`.', 234 + 12 235 + ); 236 + } 237 + 238 + return link; 239 + };
+84
exchanges/graphcache/src/store/store.test.ts
··· 967 967 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#14'); 968 968 }); 969 969 }); 970 + 971 + it('should link up entities', () => { 972 + const store = new Store(); 973 + const todo = gql` 974 + query test { 975 + todo(id: "1") { 976 + id 977 + title 978 + __typename 979 + } 980 + } 981 + `; 982 + const author = gql` 983 + query testAuthor { 984 + author(id: "1") { 985 + id 986 + name 987 + __typename 988 + } 989 + } 990 + `; 991 + write( 992 + store, 993 + { 994 + query: todo, 995 + }, 996 + { 997 + todo: { 998 + id: '1', 999 + title: 'learn urql', 1000 + __typename: 'Todo', 1001 + }, 1002 + __typename: 'Query', 1003 + } as any 1004 + ); 1005 + let { data } = query(store, { query: todo }); 1006 + expect((data as any).todo).toEqual({ 1007 + id: '1', 1008 + title: 'learn urql', 1009 + __typename: 'Todo', 1010 + }); 1011 + write( 1012 + store, 1013 + { 1014 + query: author, 1015 + }, 1016 + { 1017 + author: { __typename: 'Author', id: '1', name: 'Formidable' }, 1018 + __typename: 'Query', 1019 + } as any 1020 + ); 1021 + InMemoryData.initDataState('write', store.data, null); 1022 + store.link((data as any).todo, 'author', { 1023 + __typename: 'Author', 1024 + id: '1', 1025 + name: 'Formidable', 1026 + }); 1027 + InMemoryData.clearDataState(); 1028 + const todoWithAuthor = gql` 1029 + query test { 1030 + todo(id: "1") { 1031 + id 1032 + title 1033 + __typename 1034 + author { 1035 + id 1036 + name 1037 + __typename 1038 + } 1039 + } 1040 + } 1041 + `; 1042 + ({ data } = query(store, { query: todoWithAuthor })); 1043 + expect((data as any).todo).toEqual({ 1044 + id: '1', 1045 + title: 'learn urql', 1046 + __typename: 'Todo', 1047 + author: { 1048 + __typename: 'Author', 1049 + id: '1', 1050 + name: 'Formidable', 1051 + }, 1052 + }); 1053 + });
+31 -1
exchanges/graphcache/src/store/store.ts
··· 8 8 DataField, 9 9 Variables, 10 10 FieldArgs, 11 + Link, 11 12 Data, 12 13 QueryInput, 13 14 UpdatesConfig, ··· 18 19 } from '../types'; 19 20 20 21 import { invariant } from '../helpers/help'; 21 - import { contextRef } from '../operations/shared'; 22 + import { contextRef, ensureLink } from '../operations/shared'; 22 23 import { read, readFragment } from '../operations/query'; 23 24 import { writeFragment, startWrite } from '../operations/write'; 24 25 import { invalidateEntity } from '../operations/invalidate'; ··· 202 203 variables?: V 203 204 ): void { 204 205 writeFragment(this, formatDocument(fragment), data, variables as any); 206 + } 207 + 208 + link( 209 + entity: Entity, 210 + field: string, 211 + args: FieldArgs, 212 + link: Link<Entity> 213 + ): void; 214 + 215 + link(entity: Entity, field: string, link: Link<Entity>): void; 216 + 217 + link( 218 + entity: Entity, 219 + field: string, 220 + argsOrLink: FieldArgs | Link<Entity>, 221 + maybeLink?: Link<Entity> 222 + ): void { 223 + const args = (maybeLink !== undefined ? argsOrLink : null) as FieldArgs; 224 + const link = (maybeLink !== undefined 225 + ? maybeLink 226 + : argsOrLink) as Link<Entity>; 227 + const entityKey = ensureLink(this, entity); 228 + if (typeof entityKey === 'string') { 229 + InMemoryData.writeLink( 230 + entityKey, 231 + keyOfField(field, args), 232 + ensureLink(this, link) 233 + ); 234 + } 205 235 } 206 236 }
+10
exchanges/graphcache/src/types.ts
··· 117 117 data: T, 118 118 variables?: V 119 119 ): void; 120 + 121 + /** link() can be used to update a given entity field to link to another entity or entities */ 122 + link( 123 + entity: Entity, 124 + field: string, 125 + args: FieldArgs, 126 + link: Link<Entity> 127 + ): void; 128 + /** link() can be used to update a given entity field to link to another entity or entities */ 129 + link(entity: Entity, field: string, value: Link<Entity>): void; 120 130 } 121 131 122 132 type ResolverResult =