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 mergeMode to simplePagination helper (#1174)

* Add mergeMode to Simple Pagination

* Add documentation for Simple Pagination mergeMode

* Add changeset

* Update exchanges/graphcache/src/extras/simplePagination.ts

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

* Change mergeMode to before | after

* Update Changelog

* Update changeset

* Update Computed Queries documentation

* Add mergeMode option to graphcache documentation

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

authored by

Hoang
Jovi De Croock
and committed by
GitHub
2f8db845 446e01d8

+203 -41
+5
.changeset/little-crabs-sell.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + Add a `mergeMode: 'before' | 'after'` option to the `simplePagination` helper to define whether pages are merged before or after preceding ones when pagination, similar to `relayPagination`'s option
+6 -5
docs/api/graphcache.md
··· 314 314 `, 315 315 { id: 1 }, // this identifies the fragment (User) entity 316 316 { groupId: 5 } // any additional field variables 317 - ) 317 + ); 318 318 ``` 319 319 320 320 [Read more about using `readFragment` on the ["Computed Queries" ··· 473 473 Accepts a single object of optional options and returns a resolver that can be inserted into the 474 474 [`cacheExchange`'s](#cacheexchange) [`resolvers` configuration.](#resolvers-option) 475 475 476 - | Argument | Type | Description | 477 - | ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 478 - | `offsetArgument` | `?string` | The field arguments' property, as passed to the resolver, that contains the current offset, i.e. the number of items to be skipped. Defaults to `'skip'`. | 479 - | `limitArgument` | `?string` | The field arguments' property, as passed to the resolver, that contains the current page size limit, i.e. the number of items on each page. Defaults to `'limit'`. | 476 + | Argument | Type | Description | 477 + | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 478 + | `offsetArgument` | `?string` | The field arguments' property, as passed to the resolver, that contains the current offset, i.e. the number of items to be skipped. Defaults to `'skip'`. | 479 + | `limitArgument` | `?string` | The field arguments' property, as passed to the resolver, that contains the current page size limit, i.e. the number of items on each page. Defaults to `'limit'`. | 480 + | `mergeMode` | `'after' \| 'before'` | This option defines whether pages are merged before or after preceding ones when paginating. Defaults to `'after'`. | 480 481 481 482 Once set up, the resulting resolver is able to automatically concatenate all pages of a given field 482 483 automatically. Queries to this resolvers will from then on only return the infinite, combined list
+43 -19
docs/graphcache/computed-queries.md
··· 6 6 # Computed Queries 7 7 8 8 When dealing with data we could have special cases where we want to transform 9 - the data between the API and frontend logic, for example: 9 + the data between the API and frontend logic. For example: 10 10 11 11 - alter the format of a date, perhaps from a UNIX timestamp to a `Date` object. 12 - - if we have a list of a certain entity in the cache and next we want to query a 12 + - if we have a list of a certain entity in the cache and then want to query a 13 13 specific entity, chances are this will already be (partially) available in the 14 14 cache's list. 15 15 ··· 60 60 61 61 - `entity` – This can either be an object containing a `__typename` and an `id` or 62 62 `_id` field _or_ a string key leading to a cached entity. 63 - - `field` – The field you want data for. This can be a relation or a single property. 63 + - `field` – The field we want data for. This can be a relation or a single property. 64 64 - `arguments` – The arguments to include on the field. 65 65 66 66 To get a better grasp let's look at a few examples, ··· 106 106 ``` 107 107 108 108 This can help solve practical use cases like date formatting, 109 - where you would query the date and then convert it in your resolver. 109 + where we would query the date and then convert it in our resolver. 110 110 111 - You can also link entities that come from a list, imagine the scenario where 111 + We can also link entities that come from a list, imagine the scenario where 112 112 we have queried `todos` but now want the detailView of a single `todo`. 113 113 114 114 ```js ··· 124 124 125 125 Note that resolving from a list to details can lead to partial data, this will result in 126 126 a network-request to get the full data when fields are missing. 127 - When graphcache isn't [aware of your schema](./schema-awareness.md) it won't show partial data. 127 + When graphcache isn't [aware of our schema](./schema-awareness.md) it won't show partial data. 128 128 129 129 ### Reading a query 130 130 131 - Another method the cache allows is to let you read a full query, this method 131 + Another method the cache allows is to let us read a full query, this method 132 132 accepts an object of `query` and optionally `variables`. 133 133 134 134 ```js ··· 169 169 170 170 ### Simple Pagination 171 171 172 - Given you have a schema that uses some form of `offset` and `limit` based pagination you can use the 172 + Given we have a schema that uses some form of `offset` and `limit` based pagination, we can use the 173 173 `simplePagination` exported from `@urql/exchange-graphcache/extras` to achieve an endless scroller. 174 174 175 175 This helper will concatenate all queries performed to one long data structure. ··· 187 187 }); 188 188 ``` 189 189 190 - This form of pagination accepts an object as an argument, you can specify two 190 + This form of pagination accepts an object as an argument, we can specify two 191 191 options in here `limitArgument` and `offsetArgument` these will default to `limit` 192 - and `skip` respectively. This way you can use the keywords that you are using in 193 - your queries. 192 + and `skip` respectively. This way we can use the keywords that are in our queries. 193 + 194 + We may also add the `mergeMode` option, which defaults to `'after'` and can otherwise 195 + be set to `'before'`. This will handle in which order pages are merged when paginating. 196 + The default `after` mode assumes that pages that come in last should be merged 197 + _after_ the first pages. The `'before'` mode assumes that pages that come in last 198 + should be merged _before_ the first pages, which can be helpful in a reverse 199 + endless scroller (E.g. Chat App). 200 + 201 + Example series of requests: 202 + 203 + ``` 204 + // An example where mergeMode: after works better 205 + skip: 0, limit: 3 => 1, 2, 3 206 + skip: 3, limit: 3 => 4, 5, 6 207 + 208 + mergeMode: after => 1, 2, 3, 4, 5, 6 ✔️ 209 + mergeMode: before => 4, 5, 6, 1, 2, 3 210 + 211 + // An example where mergeMode: before works better 212 + skip: 0, limit: 3 => 4, 5, 6 213 + skip: 3, limit: 3 => 1, 2, 3 214 + 215 + mergeMode: after => 4, 5, 6, 1, 2, 3 216 + mergeMode: before => 1, 2, 3, 4, 5, 6 ✔️ 217 + ``` 194 218 195 219 ### Relay Pagination 196 220 197 - Given you have a [relay-compatible schema](https://facebook.github.io/relay/graphql/connections.htm) 198 - on your backend we offer the possibility of endless data resolving. 199 - This means that when you fetch the next page in your data 200 - received in `useQuery` you'll see the previous pages as well. This is useful for 221 + Given we have a [relay-compatible schema](https://facebook.github.io/relay/graphql/connections.htm) 222 + on our backend, we can offer the possibility of endless data resolving. 223 + This means that when we fetch the next page in our data 224 + received in `useQuery` we'll see the previous pages as well. This is useful for 201 225 endless scrolling. 202 226 203 - You can achieve this by importing `relayPagination` from `@urql/exchange-graphcache/extras`. 227 + We can achieve this by importing `relayPagination` from `@urql/exchange-graphcache/extras`. 204 228 205 229 ```js 206 230 import { cacheExchange } from '@urql/exchange-graphcache'; ··· 217 241 218 242 `relayPagination` accepts an object of options, for now we are offering one 219 243 option and that is the `mergeMode`. This defaults to `inwards` and can otherwise 220 - be set to `outwards`. This will handle how pages are merged when you paginate 244 + be set to `outwards`. This will handle how pages are merged when we paginate 221 245 forwards and backwards at the same time. outwards pagination assumes that pages 222 246 that come in last should be merged before the first pages, so that the list 223 247 grows outwards in both directions. The default inwards pagination assumes that ··· 237 261 With inwards merging the nodes will be in this order: `[1, 2, ..., 89, 99]` 238 262 And with outwards merging: `[..., 89, 99, 1, 2, ...]` 239 263 240 - The helper happily supports schemata that return nodes rather than 241 - individually-cursored edges. For each paginated type, you must either 264 + The helper happily supports schema that return nodes rather than 265 + individually-cursored edges. For each paginated type, we must either 242 266 always request nodes, or always request edges -- otherwise the lists 243 267 cannot be stiched together. 244 268
+131 -3
exchanges/graphcache/src/extras/simplePagination.test.ts
··· 3 3 import { Store } from '../store'; 4 4 import { simplePagination } from './simplePagination'; 5 5 6 - it('works with simple pagination', () => { 6 + it('works with forward pagination', () => { 7 7 const Pagination = gql` 8 8 query($skip: Number, $limit: Number) { 9 9 persons(skip: $skip, limit: $limit) { ··· 73 73 expect(pageThreeResult.data).toEqual(null); 74 74 }); 75 75 76 + it('works with backwards pagination', () => { 77 + const Pagination = gql` 78 + query($skip: Number, $limit: Number) { 79 + persons(skip: $skip, limit: $limit) { 80 + __typename 81 + id 82 + name 83 + } 84 + } 85 + `; 86 + 87 + const store = new Store({ 88 + resolvers: { 89 + Query: { 90 + persons: simplePagination({ mergeMode: 'before' }), 91 + }, 92 + }, 93 + }); 94 + 95 + const pageOne = { 96 + __typename: 'Query', 97 + persons: [ 98 + { id: 7, name: 'Jovi', __typename: 'Person' }, 99 + { id: 8, name: 'Phil', __typename: 'Person' }, 100 + { id: 9, name: 'Andy', __typename: 'Person' }, 101 + ], 102 + }; 103 + 104 + const pageTwo = { 105 + __typename: 'Query', 106 + persons: [ 107 + { id: 4, name: 'Kadi', __typename: 'Person' }, 108 + { id: 5, name: 'Dom', __typename: 'Person' }, 109 + { id: 6, name: 'Sofia', __typename: 'Person' }, 110 + ], 111 + }; 112 + 113 + write( 114 + store, 115 + { query: Pagination, variables: { skip: 0, limit: 3 } }, 116 + pageOne 117 + ); 118 + const pageOneResult = query(store, { 119 + query: Pagination, 120 + variables: { skip: 0, limit: 3 }, 121 + }); 122 + expect(pageOneResult.data).toEqual(pageOne); 123 + 124 + write( 125 + store, 126 + { query: Pagination, variables: { skip: 3, limit: 3 } }, 127 + pageTwo 128 + ); 129 + 130 + const pageTwoResult = query(store, { 131 + query: Pagination, 132 + variables: { skip: 3, limit: 3 }, 133 + }); 134 + expect((pageTwoResult.data as any).persons).toEqual([ 135 + ...pageTwo.persons, 136 + ...pageOne.persons, 137 + ]); 138 + 139 + const pageThreeResult = query(store, { 140 + query: Pagination, 141 + variables: { skip: 6, limit: 3 }, 142 + }); 143 + expect(pageThreeResult.data).toEqual(null); 144 + }); 145 + 76 146 it('handles duplicates', () => { 77 147 const Pagination = gql` 78 148 query($skip: Number, $limit: Number) { ··· 182 252 expect(res.data).toEqual({ __typename: 'Query', persons: [] }); 183 253 }); 184 254 185 - it('should preserve the correct order', () => { 255 + it('should preserve the correct order in forward pagination', () => { 186 256 const Pagination = gql` 187 257 query($skip: Number, $limit: Number) { 188 258 persons(skip: $skip, limit: $limit) { ··· 196 266 const store = new Store({ 197 267 resolvers: { 198 268 Query: { 199 - persons: simplePagination(), 269 + persons: simplePagination({ mergeMode: 'after' }), 200 270 }, 201 271 }, 202 272 }); ··· 237 307 expect(result.data).toEqual({ 238 308 __typename: 'Query', 239 309 persons: [...pageOne.persons, ...pageTwo.persons], 310 + }); 311 + }); 312 + 313 + it('should preserve the correct order in backward pagination', () => { 314 + const Pagination = gql` 315 + query($skip: Number, $limit: Number) { 316 + persons(skip: $skip, limit: $limit) { 317 + __typename 318 + id 319 + name 320 + } 321 + } 322 + `; 323 + 324 + const store = new Store({ 325 + resolvers: { 326 + Query: { 327 + persons: simplePagination({ mergeMode: 'before' }), 328 + }, 329 + }, 330 + }); 331 + 332 + const pageOne = { 333 + __typename: 'Query', 334 + persons: [ 335 + { id: 7, name: 'Jovi', __typename: 'Person' }, 336 + { id: 8, name: 'Phil', __typename: 'Person' }, 337 + { id: 9, name: 'Andy', __typename: 'Person' }, 338 + ], 339 + }; 340 + 341 + const pageTwo = { 342 + __typename: 'Query', 343 + persons: [ 344 + { id: 4, name: 'Kadi', __typename: 'Person' }, 345 + { id: 5, name: 'Dom', __typename: 'Person' }, 346 + { id: 6, name: 'Sofia', __typename: 'Person' }, 347 + ], 348 + }; 349 + 350 + write( 351 + store, 352 + { query: Pagination, variables: { skip: 3, limit: 3 } }, 353 + pageTwo 354 + ); 355 + write( 356 + store, 357 + { query: Pagination, variables: { skip: 0, limit: 3 } }, 358 + pageOne 359 + ); 360 + 361 + const result = query(store, { 362 + query: Pagination, 363 + variables: { skip: 3, limit: 3 }, 364 + }); 365 + expect(result.data).toEqual({ 366 + __typename: 'Query', 367 + persons: [...pageTwo.persons, ...pageOne.persons], 240 368 }); 241 369 }); 242 370
+18 -14
exchanges/graphcache/src/extras/simplePagination.ts
··· 1 1 import { stringifyVariables } from '@urql/core'; 2 2 import { Resolver, Variables, NullArray } from '../types'; 3 3 4 + export type MergeMode = 'before' | 'after'; 5 + 4 6 export interface PaginationParams { 5 7 offsetArgument?: string; 6 8 limitArgument?: string; 9 + mergeMode?: MergeMode; 7 10 } 8 11 9 12 export const simplePagination = ({ 10 13 offsetArgument = 'skip', 11 14 limitArgument = 'limit', 15 + mergeMode = 'after', 12 16 }: PaginationParams = {}): Resolver => { 13 17 const compareArgs = ( 14 18 fieldArgs: Variables, ··· 74 78 continue; 75 79 } 76 80 77 - if (!prevOffset || currentOffset > prevOffset) { 78 - for (let j = 0; j < links.length; j++) { 79 - const link = links[j]; 80 - if (visited.has(link)) continue; 81 - result.push(link); 82 - visited.add(link); 83 - } 81 + const tempResult: NullArray<string> = []; 82 + 83 + for (let j = 0; j < links.length; j++) { 84 + const link = links[j]; 85 + if (visited.has(link)) continue; 86 + tempResult.push(link); 87 + visited.add(link); 88 + } 89 + 90 + if ( 91 + (!prevOffset || currentOffset > prevOffset) === 92 + (mergeMode === 'after') 93 + ) { 94 + result = [...result, ...tempResult]; 84 95 } else { 85 - const tempResult: NullArray<string> = []; 86 - for (let j = 0; j < links.length; j++) { 87 - const link = links[j]; 88 - if (visited.has(link)) continue; 89 - tempResult.push(link); 90 - visited.add(link); 91 - } 92 96 result = [...tempResult, ...result]; 93 97 } 94 98