-
-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bidirectional cursor pagination (#846)
* remove direction from refetch object * more store definitions * fix build errors * add missing methods to query store * add e2e test * fix initial args in bidirectional pagination test * add previous button * fix connection implementation on server * tidy up docs * treat updates as a list of strings to match against * start on query modification * only generate pagination queries that match the api * fix query arguments when loading pages * cache respects page info when handling cursor updates * bidirectional e2e query tests pass 🎉 * add tests for bidirectional fragments * tests pass * changesets * check pagination direction * linter * update snapshots * tidy up docs * release notes * remove logs * tidy up release notes * add license
- Loading branch information
1 parent
308ed34
commit 3245233
Showing
37 changed files
with
1,553 additions
and
453 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'houdini-svelte': major | ||
--- | ||
|
||
Added support for bidirectional pagination when using connections and remove the config values for specify custom stores for a specific direction |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'houdini': patch | ||
--- | ||
|
||
Fixed pageInfo behavior when prepending and append values |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* This file is copied from graphql-relay-js: https://github.com/graphql/graphql-relay-js/blob/main/src/connection/arrayConnection.ts | ||
* It's licensed under the MIT license found at the bottom of the file (per the project's agreement) | ||
*/ | ||
|
||
export function connectionFromArray(data, args) { | ||
return connectionFromArraySlice(data, args, { | ||
sliceStart: 0, | ||
arrayLength: data.length, | ||
}) | ||
} | ||
|
||
function connectionFromArraySlice(arraySlice, args, meta) { | ||
const { after, before, first, last } = args | ||
const { sliceStart, arrayLength } = meta | ||
const sliceEnd = sliceStart + arraySlice.length | ||
|
||
let startOffset = Math.max(sliceStart, 0) | ||
let endOffset = Math.min(sliceEnd, arrayLength) | ||
|
||
const afterOffset = getOffsetWithDefault(after, -1) | ||
if (0 <= afterOffset && afterOffset < arrayLength) { | ||
startOffset = Math.max(startOffset, afterOffset + 1) | ||
} | ||
|
||
const beforeOffset = getOffsetWithDefault(before, endOffset) | ||
if (0 <= beforeOffset && beforeOffset < arrayLength) { | ||
endOffset = Math.min(endOffset, beforeOffset) | ||
} | ||
|
||
if (typeof first === 'number') { | ||
if (first < 0) { | ||
throw new Error('Argument "first" must be a non-negative integer') | ||
} | ||
|
||
endOffset = Math.min(endOffset, startOffset + first) | ||
} | ||
if (typeof last === 'number') { | ||
if (last < 0) { | ||
throw new Error('Argument "last" must be a non-negative integer') | ||
} | ||
|
||
startOffset = Math.max(startOffset, endOffset - last) | ||
} | ||
|
||
// If supplied slice is too large, trim it down before mapping over it. | ||
const slice = arraySlice.slice(startOffset - sliceStart, endOffset - sliceStart) | ||
|
||
const edges = slice.map((value, index) => ({ | ||
cursor: offsetToCursor(startOffset + index), | ||
node: value, | ||
})) | ||
|
||
const firstEdge = edges[0] | ||
const lastEdge = edges[edges.length - 1] | ||
const lowerBound = 0 | ||
const upperBound = arrayLength | ||
|
||
return { | ||
edges, | ||
pageInfo: { | ||
startCursor: firstEdge ? firstEdge.cursor : null, | ||
endCursor: lastEdge ? lastEdge.cursor : null, | ||
hasPreviousPage: startOffset > lowerBound, | ||
hasNextPage: endOffset < upperBound, | ||
}, | ||
} | ||
} | ||
const PREFIX = 'arrayconnection:' | ||
|
||
/** | ||
* Creates the cursor string from an offset. | ||
*/ | ||
export function offsetToCursor(offset) { | ||
return base64(PREFIX + offset.toString()) | ||
} | ||
|
||
/** | ||
* Extracts the offset from the cursor string. | ||
*/ | ||
export function cursorToOffset(cursor) { | ||
return parseInt(unbase64(cursor).substring(PREFIX.length), 10) | ||
} | ||
|
||
/** | ||
* Return the cursor associated with an object in an array. | ||
*/ | ||
export function cursorForObjectInConnection(data, object) { | ||
const offset = data.indexOf(object) | ||
if (offset === -1) { | ||
return null | ||
} | ||
return offsetToCursor(offset) | ||
} | ||
|
||
/** | ||
* Given an optional cursor and a default offset, returns the offset | ||
* to use; if the cursor contains a valid offset, that will be used, | ||
* otherwise it will be the default. | ||
*/ | ||
export function getOffsetWithDefault(cursor, defaultOffset) { | ||
if (typeof cursor !== 'string') { | ||
return defaultOffset | ||
} | ||
const offset = cursorToOffset(cursor) | ||
return isNaN(offset) ? defaultOffset : offset | ||
} | ||
|
||
function base64(str) { | ||
return btoa(str) | ||
} | ||
|
||
function unbase64(str) { | ||
return atob(str) | ||
} | ||
|
||
/** | ||
* | ||
MIT License | ||
Copyright (c) GraphQL Contributors | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
e2e/sveltekit/src/routes/pagination/fragment/bidirectional-cursor/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<script lang="ts"> | ||
import { CachePolicy, paginatedFragment, graphql } from '$houdini'; | ||
const queryResult = graphql(` | ||
query UserFragmentBidirectionalCursorQuery @load { | ||
user(id: "1", snapshot: "pagination-fragment-backwards-cursor") { | ||
...BidirectionalCursorFragment | ||
} | ||
} | ||
`); | ||
const fragmentResult = paginatedFragment( | ||
$queryResult.data!.user!, | ||
graphql(` | ||
fragment BidirectionalCursorFragment on User { | ||
usersConnection(after: "YXJyYXljb25uZWN0aW9uOjE=", first: 2) @paginate { | ||
edges { | ||
node { | ||
name | ||
} | ||
} | ||
} | ||
} | ||
`) | ||
); | ||
</script> | ||
|
||
<div id="result"> | ||
{$fragmentResult.data?.usersConnection.edges.map(({ node }) => node?.name).join(', ')} | ||
</div> | ||
|
||
<div id="pageInfo"> | ||
{JSON.stringify($fragmentResult.pageInfo)} | ||
</div> | ||
|
||
<button id="previous" on:click={() => fragmentResult.loadPreviousPage()}>previous</button> | ||
<button id="next" on:click={() => fragmentResult.loadNextPage()}>next</button> | ||
|
||
<button id="refetch" on:click={() => fragmentResult.fetch({ policy: CachePolicy.NetworkOnly })} | ||
>refetch</button | ||
> |
113 changes: 113 additions & 0 deletions
113
e2e/sveltekit/src/routes/pagination/fragment/bidirectional-cursor/spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { expect, test } from '@playwright/test'; | ||
import { routes } from '../../../../lib/utils/routes.js'; | ||
import { | ||
expect_1_gql, | ||
expect_0_gql, | ||
expectToBe, | ||
expectToContain, | ||
goto | ||
} from '../../../../lib/utils/testsHelper.js'; | ||
|
||
test.describe('bidirectional cursor paginated fragment', () => { | ||
test('backwards and then forwards', async ({ page }) => { | ||
await goto(page, routes.Pagination_fragment_bidirectional_cursor); | ||
|
||
await expectToBe(page, 'Morgan Freeman, Tom Hanks'); | ||
|
||
/// Click on the previous button | ||
|
||
// load the previous page and wait for the response | ||
await expect_1_gql(page, 'button[id=previous]'); | ||
|
||
// make sure we got the new content | ||
await expectToBe(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks'); | ||
|
||
// there should be a next page | ||
await expectToContain(page, `"hasNextPage":true`); | ||
// there should be no previous page | ||
await expectToContain(page, `"hasPreviousPage":false`); | ||
|
||
/// Click on the next button | ||
|
||
// load the next page and wait for the response | ||
await expect_1_gql(page, 'button[id=next]'); | ||
|
||
// there should be no previous page | ||
await expectToContain(page, `"hasPreviousPage":false`); | ||
// there should be a next page | ||
await expectToContain(page, `"hasNextPage":true`); | ||
|
||
// make sure we got the new content | ||
await expectToBe( | ||
page, | ||
'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford' | ||
); | ||
|
||
/// Click on the next button | ||
|
||
// load the next page and wait for the response | ||
await expect_1_gql(page, 'button[id=next]'); | ||
|
||
// there should be no previous page | ||
await expectToContain(page, `"hasPreviousPage":false`); | ||
// there should be a next page | ||
await expectToContain(page, `"hasNextPage":false`); | ||
|
||
// make sure we got the new content | ||
await expectToBe( | ||
page, | ||
'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood' | ||
); | ||
}); | ||
|
||
test('forwards then backwards and then forwards again', async ({ page }) => { | ||
await goto(page, routes.Pagination_fragment_bidirectional_cursor); | ||
|
||
await expectToBe(page, 'Morgan Freeman, Tom Hanks'); | ||
|
||
/// Click on the next button | ||
|
||
// load the next page and wait for the response | ||
await expect_1_gql(page, 'button[id=next]'); | ||
|
||
// there should be no previous page | ||
await expectToContain(page, `"hasPreviousPage":true`); | ||
// there should be a next page | ||
await expectToContain(page, `"hasNextPage":true`); | ||
|
||
// make sure we got the new content | ||
await expectToBe(page, 'Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford'); | ||
|
||
/// Click on the previous button | ||
|
||
// load the previous page and wait for the response | ||
await expect_1_gql(page, 'button[id=previous]'); | ||
|
||
// make sure we got the new content | ||
await expectToBe( | ||
page, | ||
'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford' | ||
); | ||
|
||
// there should be a next page | ||
await expectToContain(page, `"hasNextPage":true`); | ||
// there should be no previous page | ||
await expectToContain(page, `"hasPreviousPage":false`); | ||
|
||
/// Click on the next button | ||
|
||
// load the next page and wait for the response | ||
await expect_1_gql(page, 'button[id=next]'); | ||
|
||
// there should be no previous page | ||
await expectToContain(page, `"hasPreviousPage":false`); | ||
// there should be a next page | ||
await expectToContain(page, `"hasNextPage":false`); | ||
|
||
// make sure we got the new content | ||
await expectToBe( | ||
page, | ||
'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood' | ||
); | ||
}); | ||
}); |
34 changes: 34 additions & 0 deletions
34
e2e/sveltekit/src/routes/pagination/query/bidirectional-cursor/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<script lang="ts"> | ||
import { CachePolicy, graphql } from '$houdini'; | ||
const result = graphql(` | ||
query BidirectionalCursorPaginationQuery @load { | ||
usersConnection( | ||
after: "YXJyYXljb25uZWN0aW9uOjE=" | ||
first: 2 | ||
snapshot: "pagination-query-bdiriectional-cursor" | ||
) @paginate { | ||
edges { | ||
node { | ||
name | ||
} | ||
} | ||
} | ||
} | ||
`); | ||
</script> | ||
|
||
<div id="result"> | ||
{$result.data?.usersConnection.edges.map(({ node }) => node?.name).join(', ')} | ||
</div> | ||
|
||
<div id="pageInfo"> | ||
{JSON.stringify($result.pageInfo)} | ||
</div> | ||
|
||
<button id="previous" on:click={() => result.loadPreviousPage()}>previous</button> | ||
<button id="next" on:click={() => result.loadNextPage()}>next</button> | ||
|
||
<button id="refetch" on:click={() => result.fetch({ policy: CachePolicy.NetworkOnly })} | ||
>refetch</button | ||
> |
Oops, something went wrong.