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.

fix(graphcache): Prevent cache writes during query operations (#2978)

authored by

Phil Pluckthun and committed by
GitHub
9eb12f27 e06ba6f9

+65 -34
+5
.changeset/wet-buses-hide.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Add `invariant` to data layer that prevents cache writes during cache query operations. This prevents `cache.writeFragment`, `cache.updateQuery`, and `cache.link` from being called in `resolvers` for instance.
+14
docs/graphcache/errors.md
··· 395 395 implemented type for these. 396 396 397 397 Check the type mentioned and change it to one of the specific types. 398 + 399 + ## (27) Invalid Cache write 400 + 401 + > Invalid Cache write: You may not write to the cache during cache reads. 402 + > Accesses to `cache.writeFragment`, `cache.updateQuery`, and `cache.link` may 403 + > not be made inside `resolvers` for instance. 404 + 405 + If you're using the `Cache` inside your `cacheExchange` config you receive it 406 + either inside callbacks that are called when the cache is queried (e.g. 407 + `resolvers`) or when data is written to the cache (e.g. `updates`). You may not 408 + write to the cache when it's being queried. 409 + 410 + Please make sure that you're not calling `cache.updateQuery`, 411 + `cache.writeFragment`, or `cache.link` inside `resolvers`.
+23 -28
docs/graphcache/local-resolvers.md
··· 65 65 to transform records, like dates in our previous example, or to imitate server-side logic to allow 66 66 Graphcache to retrieve more data from its cache without sending a query to our API. 67 67 68 + Furthermore, while we see on this page that we get access to methods like `cache.resolve` and other 69 + methods to read from our cache, only ["Cache Updates"](./cache-updates.md) get to write and change 70 + the cache. If you call `cache.updateQuery`, `cache.writeFragment`, or `cache.link` in resolvers, 71 + you‘ll get an error, since it‘s not possible to update the cache while reading from it. 72 + 68 73 ## Transforming Records 69 74 70 75 As we've explored in the ["Normalized Caching" page's section on ··· 101 106 operating on. Hence, we can create a reusable resolver like so: 102 107 103 108 ```js 104 - const transformToDate = (parent, _args, _cache, info) => 105 - new Date(parent[info.fieldName]); 109 + const transformToDate = (parent, _args, _cache, info) => new Date(parent[info.fieldName]); 106 110 107 111 cacheExchange({ 108 112 resolvers: { ··· 124 128 resolvers: { 125 129 Todo: { 126 130 text: (parent, args) => { 127 - return args.capitalize && parent.text 128 - ? parent.text.toUpperCase() 129 - : parent.text; 131 + return args.capitalize && parent.text ? parent.text.toUpperCase() : parent.text; 130 132 }, 131 133 }, 132 134 }, ··· 178 180 The `__typename` field is required. Graphcache will [use its keying 179 181 logic](./normalized-caching.md#custom-keys-and-non-keyable-entities), and your custom `keys` 180 182 configuration to generate a key for this entity and will then be able to look this entity up in its 181 - local cache. As with regular queries, the resolver is known to return a link since the `todo(id: 182 - $id) { id }` will be used with a selection set, querying fields on the entity. 183 + local cache. As with regular queries, the resolver is known to return a link since the `todo(id: $id) { id }` will be used with a selection set, querying fields on the entity. 183 184 184 185 ### Resolving by keys 185 186 ··· 198 199 cacheExchange({ 199 200 resolvers: { 200 201 Query: { 201 - todo: (_, args, cache) => 202 - cache.keyOfEntity({ __typename: 'Todo', id: args.id }), 202 + todo: (_, args, cache) => cache.keyOfEntity({ __typename: 'Todo', id: args.id }), 203 203 }, 204 204 }, 205 205 }); ··· 253 253 cacheExchange({ 254 254 resolvers: { 255 255 Todo: { 256 - updatedAt: (parent, _args, cache) => 257 - new Date(cache.resolve(parent, "updatedAt")), 256 + updatedAt: (parent, _args, cache) => new Date(cache.resolve(parent, 'updatedAt')), 258 257 }, 259 258 }, 260 259 }); ··· 279 278 cacheExchange({ 280 279 resolvers: { 281 280 Todo: { 282 - updatedAt: (parent, _args, cache) => 283 - parent.updatedAt || cache.resolve(parent, "createdAt") 281 + updatedAt: (parent, _args, cache) => parent.updatedAt || cache.resolve(parent, 'createdAt'), 284 282 }, 285 283 }, 286 284 }); ··· 299 297 resolvers: { 300 298 Todo: { 301 299 createdAt: (parent, _args, cache) => 302 - cache.resolve( 303 - cache.resolve(parent, "author"), /* "Author:1" */ 304 - "createdAt" 305 - ) 300 + cache.resolve(cache.resolve(parent, 'author') /* "Author:1" */, 'createdAt'), 306 301 }, 307 302 }, 308 303 }); ··· 387 382 388 383 ```js 389 384 import { gql } from '@urql/core'; 390 - import { cacheExchange } from '@urql/exchange-graphcache'; 385 + import { cacheExchange } from '@urql/exchange-graphcache'; 391 386 392 387 const cache = cacheExchange({ 393 388 updates: { 394 389 Mutation: { 395 390 addTodo: (result, args, cache) => { 396 391 const data = cache.readQuery({ query: Todos, variables: { from: 0, limit: 10 } }); 397 - } 398 - } 399 - } 400 - }) 392 + }, 393 + }, 394 + }, 395 + }); 401 396 ``` 402 397 403 398 This way we'll get the stored data for the `TodosQuery` for the given `variables`. ··· 411 406 412 407 ```js 413 408 import { gql } from '@urql/core'; 414 - import { cacheExchange } from '@urql/exchange-graphcache'; 409 + import { cacheExchange } from '@urql/exchange-graphcache'; 415 410 416 411 const cache = cacheExchange({ 417 412 resolvers: { ··· 426 421 `, 427 422 { id: 1 } 428 423 ); 429 - } 430 - } 431 - } 432 - }) 424 + }, 425 + }, 426 + }, 427 + }); 433 428 ``` 434 429 435 430 > **Note:** In the above example, we've used ··· 528 523 // Or if the pagination happens in a nested field: 529 524 User: { 530 525 todos: relayPagination(), 531 - } 526 + }, 532 527 }, 533 528 }); 534 529 ```
+4
docs/graphcache/normalized-caching.md
··· 445 445 affect other parts of the cache (like `Query.todos` here) beyond the automatic updates that a 446 446 normalized cache is expected to perform. 447 447 448 + We get methods like `cache.updateQuery`, `cache.writeFragment`, and `cache.link` in our updater 449 + functions, which aren't available to us in local resolvers, and can only be used in these `updates` 450 + entries to change the data that the cache holds. 451 + 448 452 [Read more about writing cache updates on the "Cache Updates" page.](./cache-updates.md) 449 453 450 454 ## Deterministic Cache Updates
+2 -1
exchanges/graphcache/src/helpers/help.ts
··· 31 31 | 23 32 32 | 24 33 33 | 25 34 - | 26; 34 + | 26 35 + | 27; 35 36 36 37 type DebugNode = ExecutableDefinitionNode | InlineFragmentNode; 37 38
+11
exchanges/graphcache/src/store/data.ts
··· 255 255 fieldKey: string, 256 256 value: T 257 257 ) => { 258 + if (process.env.NODE_ENV !== 'production') { 259 + invariant( 260 + currentOperation !== 'read', 261 + 'Invalid Cache write: You may not write to the cache during cache reads. ' + 262 + ' Accesses to `cache.writeFragment`, `cache.updateQuery`, and `cache.link` may ' + 263 + ' not be made inside `resolvers` for instance.', 264 + 27 265 + ); 266 + } 267 + 258 268 // Optimistic values are written to a map in the optimistic dict 259 269 // All other values are written to the base map 260 270 const keymap: KeyMap<Dict<T | undefined>> = currentOptimisticKey ··· 550 560 // Hide current dependencies from squashing operations 551 561 const previousDependencies = currentDependencies; 552 562 currentDependencies = new Set(); 563 + currentOperation = 'write'; 553 564 554 565 const links = currentData!.links.optimistic.get(layerKey); 555 566 if (links) {
+6 -5
exchanges/graphcache/src/store/store.test.ts
··· 468 468 ); 469 469 let { data } = query(store, { query: connection }); 470 470 471 - InMemoryData.initDataState('read', store.data, null); 471 + InMemoryData.initDataState('write', store.data, null); 472 472 expect((data as any).exercisesConnection).toEqual(null); 473 473 const fields = store.inspectFields({ __typename: 'Query' }); 474 474 fields.forEach(({ fieldName, arguments: args }) => { ··· 483 483 }); 484 484 485 485 it('should be able to write a fragment', () => { 486 - InMemoryData.initDataState('read', store.data, null); 486 + InMemoryData.initDataState('write', store.data, null); 487 487 488 488 store.writeFragment( 489 489 gql` ··· 520 520 }); 521 521 522 522 it('should be able to write a fragment by name', () => { 523 - InMemoryData.initDataState('read', store.data, null); 523 + InMemoryData.initDataState('write', store.data, null); 524 524 525 525 store.writeFragment( 526 526 gql` ··· 626 626 }); 627 627 628 628 it('should be able to update a query', () => { 629 - InMemoryData.initDataState('read', store.data, null); 629 + InMemoryData.initDataState('write', store.data, null); 630 630 store.updateQuery({ query: Todos }, data => ({ 631 631 ...data, 632 632 todos: [ ··· 686 686 } 687 687 ); 688 688 689 - InMemoryData.initDataState('read', store.data, null); 689 + InMemoryData.initDataState('write', store.data, null); 690 690 store.updateQuery({ query: Appointment, variables: { id: '1' } }, data => ({ 691 691 ...data, 692 692 appointment: { ··· 827 827 828 828 describe('Invalidating an entity', () => { 829 829 it('removes an entity from a list.', () => { 830 + InMemoryData.initDataState('write', store.data, null); 830 831 store.invalidate(todosData.todos[1]); 831 832 const { data } = query(store, { query: Todos }); 832 833 expect(data).toBe(null);