From fa456a5afc68f0eac58d01771ab4b4bac7de077a Mon Sep 17 00:00:00 2001 From: Tom Jenkinson Date: Sat, 13 Jan 2024 12:54:39 +0000 Subject: [PATCH] Include `startCursor`/`endCursor` in `PageInfo` --- README.md | 8 +- .../sql-cursor-pagination.test.ts.snap | 192 ++++++++++++++++++ src/assert.ts | 8 +- src/sql-cursor-pagination.test.ts | 26 +++ src/sql-cursor-pagination.ts | 25 ++- 5 files changed, 245 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ccd969c1..cfd250e4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Are you running a service, using an SQL database, and want to support cursor sty 1. When a request comes in you call the library with a `query` object containing how many items to fetch (`first`/`last`), where to fetch from (`beforeCursor`/`afterCursor`) and the sort config (`sortFields`), along with a `setup` object. 2. The `runQuery` function you provided in `setup` is invoked, and provided with a `limit`, `whereFragmentBuilder` and `orderByFragmentBuilder`. You integrate these into your query, run it, and then return the results. -3. The library takes the results, and for each one it generates a unique `cursor`, which it then returns alongside each row. It also returns `hasNextPage`/`hasPreviousPage` properties. +3. The library takes the results, and for each one it generates a unique `cursor`, which it then returns alongside each row. It also returns `hasNextPage`/`hasPreviousPage`/`startCursor`/`endCursor` properties. ## What is cursor pagination? @@ -109,7 +109,7 @@ The result is a promise that resolves with an object containing `edges` and `pag `edges` is an array of objects containing `cursor` and `node` properties, where `cursor` is the generated cursor for the `node`, and `node` is the object you returned for the row from `runQuery`. -`pageInfo` contains `hasNextPage` and `hasPreviousPage` properties. +`pageInfo` contains `hasNextPage`/`hasPreviousPage`/`startCursor`/`endCursor` properties. E.g. @@ -128,7 +128,9 @@ E.g. ], "pageInfo": { "hasNextPage": true, - "hasPreviousPage": false + "hasPreviousPage": false, + "startCursor": "l1X624m67Z5aYShVOLrThEcP7c-ezmCc4C48Dvxtt98.x7zYjxX9VEWDA1KAnJii8zyw5DP_OdIRnSkXATGhwTy6Wf0SSkjdjq6pTl9qxhp87EI-85pUJW9Thz_A6F_8BzlgccgDV-hXWjEj3CsGl96tSaA-X0_qNWBu425Mt6t5j3wNSdk8sSArBQ", + "endCursor": "l1X624m67Z5aYShVOLrThEcP7c-ezmCc4C48Dvxtt98.x7zYjxX9VEWDA1KAnJii8zyw5DP_OdIRnSkXATGhwTy6Wf0SSkjdjq6pTl9qxhp87EI-85pUJW9Thz_A6F_8BzlgccgDV-hXWjEj3CsGl96tSaA-X0_qNWBu425Mt6t5j3wNSdk8sSArBQ" } } ``` diff --git a/src/__snapshots__/sql-cursor-pagination.test.ts.snap b/src/__snapshots__/sql-cursor-pagination.test.ts.snap index aafea035..7a2b9efa 100644 --- a/src/__snapshots__/sql-cursor-pagination.test.ts.snap +++ b/src/__snapshots__/sql-cursor-pagination.test.ts.snap @@ -158,8 +158,10 @@ exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 19`] = ` }, ], "pageInfo": { + "endCursor": "l1X624m67Z5aYShVOLrThEcP7c-ezmCc4C48Dvxtt98.x7zYjxX9VEWDA1KAnJii8zyw5DP_OdIRnSkXATGhwTy6Wf0SSkjdjq6pTl9qxhp87EI-85pUJW9Thz_A6F_8BzlgccgDV-hXWjEj3CsGl96tSaA-X0_qNWBu425Mt6t5j3wNSdk8sSArBQ", "hasNextPage": true, "hasPreviousPage": false, + "startCursor": "l1X624m67Z5aYShVOLrThEcP7c-ezmCc4C48Dvxtt98.x7zYjxX9VEWDA1KAnJii8zyw5DP_OdIRnSkXATGhwTy6Wf0SSkjdjq6pTl9qxhp87EI-85pUJW9Thz_A6F_8BzlgccgDV-hXWjEj3CsGl96tSaA-X0_qNWBu425Mt6t5j3wNSdk8sSArBQ", }, Symbol(edgesWithRawCursor): [ { @@ -344,8 +346,10 @@ exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 19`] = ` }, ], "pageInfo": { + "endCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", "hasNextPage": false, "hasPreviousPage": true, + "startCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", }, Symbol(edgesWithRawCursor): [ { @@ -778,6 +782,160 @@ exports[`SqlCursorPagination > errors > throws an error if raw cursor passed dir } `; +exports[`SqlCursorPagination > returns nothing when selecting after the last row 1`] = `Infinity`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 2`] = ` +{ + "bindings": [], + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 3`] = ` +{ + "bindings": [], + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 4`] = ` +{ + "bindings": [], + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 5`] = ` +{ + "bindings": {}, + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 6`] = ` +{ + "bindings": [], + "sql": "1", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 7`] = ` +{ + "bindings": [], + "sql": "1", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 8`] = ` +{ + "bindings": [], + "sql": "1", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 9`] = ` +{ + "bindings": {}, + "sql": "1", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 10`] = `2`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 11`] = ` +{ + "bindings": [], + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 12`] = ` +{ + "bindings": [], + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 13`] = ` +{ + "bindings": [], + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 14`] = ` +{ + "bindings": {}, + "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 15`] = ` +{ + "bindings": [ + "Joseph", + "Joseph", + "Rhodes", + "Joseph", + "Rhodes", + "3", + ], + "sql": "(((\`first_name\`>?) OR (\`first_name\`=? AND \`last_name\`?)))", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 16`] = ` +{ + "bindings": [ + "Joseph", + "Joseph", + "Rhodes", + "Joseph", + "Rhodes", + "3", + ], + "sql": "(((\`first_name\`>X) OR (\`first_name\`=X AND \`last_name\`X)))", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 17`] = ` +{ + "bindings": [ + "Joseph", + "Joseph", + "Rhodes", + "Joseph", + "Rhodes", + "3", + ], + "sql": "(((\`first_name\`>:0) OR (\`first_name\`=:1 AND \`last_name\`<:2) OR (\`first_name\`=:3 AND \`last_name\`=:4 AND \`id\`>:5)))", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 18`] = ` +{ + "bindings": { + ":0": "Joseph", + ":1": "Rhodes", + ":2": "3", + }, + "sql": "(((\`first_name\`>:0) OR (\`first_name\`=:0 AND \`last_name\`<:1) OR (\`first_name\`=:0 AND \`last_name\`=:1 AND \`id\`>:2)))", +} +`; + +exports[`SqlCursorPagination > returns nothing when selecting after the last row 19`] = ` +{ + "edges": [], + "pageInfo": { + "endCursor": null, + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": null, + }, + Symbol(edgesWithRawCursor): [], +} +`; + exports[`SqlCursorPagination > selects first 2 rows when selecting 2 before the third row 1`] = `Infinity`; exports[`SqlCursorPagination > selects first 2 rows when selecting 2 before the third row 2`] = ` @@ -948,8 +1106,10 @@ exports[`SqlCursorPagination > selects first 2 rows when selecting 2 before the }, ], "pageInfo": { + "endCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", "hasNextPage": false, "hasPreviousPage": false, + "startCursor": "gmWf5PUciGkaS3yuWvnYlCXra1kqrlsaGbbNJfxaF7I.IR52jEVV-zw_W7sRw5HtYQfTQhWbA4dLQw_stenfwgBDIAMZvc78sR5MEvBaLasXRLLv-Z0hxSu0HzZIczNC0zm-ltJ7NRLnQD09498KNFpdBeQBGnEJuskupvdR5TyG0l4Mjbg", }, Symbol(edgesWithRawCursor): [ { @@ -1166,8 +1326,10 @@ exports[`SqlCursorPagination > selects last 2 rows when selecting 2 after the th }, ], "pageInfo": { + "endCursor": "hxq6aLoYC69MhcHj_7nAHEAOYBgQgK-lilrZZK8PlUQ.UMAa_PpPVW-1oDBX3oeT2v8hWra5lGac55zuXzuQPAXXrGcc4PnbIIZJWTcqzXAOOCzqPER9aVjPNQGXtFgCBw1vyZNIcFTVMsMnQyCuvr71p4VYGlQF7qgFIwtCUU7xF1tQA8zK", "hasNextPage": false, "hasPreviousPage": false, + "startCursor": "l1X624m67Z5aYShVOLrThEcP7c-ezmCc4C48Dvxtt98.x7zYjxX9VEWDA1KAnJii8zyw5DP_OdIRnSkXATGhwTy6Wf0SSkjdjq6pTl9qxhp87EI-85pUJW9Thz_A6F_8BzlgccgDV-hXWjEj3CsGl96tSaA-X0_qNWBu425Mt6t5j3wNSdk8sSArBQ", }, Symbol(edgesWithRawCursor): [ { @@ -1359,8 +1521,10 @@ exports[`SqlCursorPagination > selects rows 2-3 when requesting the last 2 of fi }, ], "pageInfo": { + "endCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", "hasNextPage": true, "hasPreviousPage": true, + "startCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", }, Symbol(edgesWithRawCursor): [ { @@ -1552,8 +1716,10 @@ exports[`SqlCursorPagination > selects rows 4-5 when requesting the last 2 of fi }, ], "pageInfo": { + "endCursor": "hxq6aLoYC69MhcHj_7nAHEAOYBgQgK-lilrZZK8PlUQ.UMAa_PpPVW-1oDBX3oeT2v8hWra5lGac55zuXzuQPAXXrGcc4PnbIIZJWTcqzXAOOCzqPER9aVjPNQGXtFgCBw1vyZNIcFTVMsMnQyCuvr71p4VYGlQF7qgFIwtCUU7xF1tQA8zK", "hasNextPage": false, "hasPreviousPage": true, + "startCursor": "l1X624m67Z5aYShVOLrThEcP7c-ezmCc4C48Dvxtt98.x7zYjxX9VEWDA1KAnJii8zyw5DP_OdIRnSkXATGhwTy6Wf0SSkjdjq6pTl9qxhp87EI-85pUJW9Thz_A6F_8BzlgccgDV-hXWjEj3CsGl96tSaA-X0_qNWBu425Mt6t5j3wNSdk8sSArBQ", }, Symbol(edgesWithRawCursor): [ { @@ -1699,8 +1865,10 @@ exports[`SqlCursorPagination > selects the first 3 items 10`] = ` }, ], "pageInfo": { + "endCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", "hasNextPage": true, "hasPreviousPage": false, + "startCursor": "gmWf5PUciGkaS3yuWvnYlCXra1kqrlsaGbbNJfxaF7I.IR52jEVV-zw_W7sRw5HtYQfTQhWbA4dLQw_stenfwgBDIAMZvc78sR5MEvBaLasXRLLv-Z0hxSu0HzZIczNC0zm-ltJ7NRLnQD09498KNFpdBeQBGnEJuskupvdR5TyG0l4Mjbg", }, Symbol(edgesWithRawCursor): [ { @@ -1890,8 +2058,10 @@ exports[`SqlCursorPagination > selects the first infinity (all) items 10`] = ` }, ], "pageInfo": { + "endCursor": "hxq6aLoYC69MhcHj_7nAHEAOYBgQgK-lilrZZK8PlUQ.UMAa_PpPVW-1oDBX3oeT2v8hWra5lGac55zuXzuQPAXXrGcc4PnbIIZJWTcqzXAOOCzqPER9aVjPNQGXtFgCBw1vyZNIcFTVMsMnQyCuvr71p4VYGlQF7qgFIwtCUU7xF1tQA8zK", "hasNextPage": false, "hasPreviousPage": false, + "startCursor": "gmWf5PUciGkaS3yuWvnYlCXra1kqrlsaGbbNJfxaF7I.IR52jEVV-zw_W7sRw5HtYQfTQhWbA4dLQw_stenfwgBDIAMZvc78sR5MEvBaLasXRLLv-Z0hxSu0HzZIczNC0zm-ltJ7NRLnQD09498KNFpdBeQBGnEJuskupvdR5TyG0l4Mjbg", }, Symbol(edgesWithRawCursor): [ { @@ -2097,8 +2267,10 @@ exports[`SqlCursorPagination > selects the last 3 items 10`] = ` }, ], "pageInfo": { + "endCursor": "hxq6aLoYC69MhcHj_7nAHEAOYBgQgK-lilrZZK8PlUQ.UMAa_PpPVW-1oDBX3oeT2v8hWra5lGac55zuXzuQPAXXrGcc4PnbIIZJWTcqzXAOOCzqPER9aVjPNQGXtFgCBw1vyZNIcFTVMsMnQyCuvr71p4VYGlQF7qgFIwtCUU7xF1tQA8zK", "hasNextPage": false, "hasPreviousPage": true, + "startCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", }, Symbol(edgesWithRawCursor): [ { @@ -2288,8 +2460,10 @@ exports[`SqlCursorPagination > selects the last infinity (all) items 10`] = ` }, ], "pageInfo": { + "endCursor": "hxq6aLoYC69MhcHj_7nAHEAOYBgQgK-lilrZZK8PlUQ.UMAa_PpPVW-1oDBX3oeT2v8hWra5lGac55zuXzuQPAXXrGcc4PnbIIZJWTcqzXAOOCzqPER9aVjPNQGXtFgCBw1vyZNIcFTVMsMnQyCuvr71p4VYGlQF7qgFIwtCUU7xF1tQA8zK", "hasNextPage": false, "hasPreviousPage": false, + "startCursor": "gmWf5PUciGkaS3yuWvnYlCXra1kqrlsaGbbNJfxaF7I.IR52jEVV-zw_W7sRw5HtYQfTQhWbA4dLQw_stenfwgBDIAMZvc78sR5MEvBaLasXRLLv-Z0hxSu0HzZIczNC0zm-ltJ7NRLnQD09498KNFpdBeQBGnEJuskupvdR5TyG0l4Mjbg", }, Symbol(edgesWithRawCursor): [ { @@ -2587,8 +2761,10 @@ exports[`SqlCursorPagination > selects the second and third row when selecting a }, ], "pageInfo": { + "endCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", "hasNextPage": false, "hasPreviousPage": false, + "startCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", }, Symbol(edgesWithRawCursor): [ { @@ -2813,8 +2989,10 @@ exports[`SqlCursorPagination > selects the second row when selecting after the f }, ], "pageInfo": { + "endCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", "hasNextPage": false, "hasPreviousPage": false, + "startCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", }, Symbol(edgesWithRawCursor): [ { @@ -2999,8 +3177,10 @@ exports[`SqlCursorPagination > selects the second row when selecting one before }, ], "pageInfo": { + "endCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", "hasNextPage": false, "hasPreviousPage": true, + "startCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", }, Symbol(edgesWithRawCursor): [ { @@ -3185,8 +3365,10 @@ exports[`SqlCursorPagination > selects the second row when selecting one before }, ], "pageInfo": { + "endCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", "hasNextPage": false, "hasPreviousPage": true, + "startCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", }, Symbol(edgesWithRawCursor): [ { @@ -3392,8 +3574,10 @@ exports[`SqlCursorPagination > selects the second row when selecting the first o }, ], "pageInfo": { + "endCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", "hasNextPage": true, "hasPreviousPage": false, + "startCursor": "Vl8ajMnWgEXNNlI9TZNEzkzZT5fPgD7qdP_dIQBcjlM.nGHtSUghlMl64TM8BjfzsV4r2v-SL_TlQyYbwTz0nSbnzpetCeSw81VKEzJhkakKw3yRUHxi6ijfpwZEvyprnzBJzZ1gh070hs4CqayTv-g_KwpwjHGWUxUBkKA5S40-_oaeVS8", }, Symbol(edgesWithRawCursor): [ { @@ -3578,8 +3762,10 @@ exports[`SqlCursorPagination > selects the third row when selecting one after th }, ], "pageInfo": { + "endCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", "hasNextPage": true, "hasPreviousPage": false, + "startCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", }, Symbol(edgesWithRawCursor): [ { @@ -3785,8 +3971,10 @@ exports[`SqlCursorPagination > selects the third row when selecting the last one }, ], "pageInfo": { + "endCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", "hasNextPage": false, "hasPreviousPage": true, + "startCursor": "BF4zwhmn3i_aUO8fMntuvbJbVfFA77xjg4HWAbCqZ8s.TcLZaSN-rn8Zij9ZZWUQgs3n6YG4t0yuwODBikGsWGnL3td0c3YCwQt99Dml_IfofoAbMsPQTVoiu1gWjQ3cm4xA5Pm019RFK3kSzgV3TUHM7EjIkmTbnmjYlTsGT-xYHt8V5QmM", }, Symbol(edgesWithRawCursor): [ { @@ -3888,8 +4076,10 @@ exports[`SqlCursorPagination > supports field aliases 10`] = ` }, ], "pageInfo": { + "endCursor": "eTi5IVXuus3g1sablUWFGWkZBbE8pKn1NpODYFyekPk.6pGQw7mGK7nGKpaVhAjxa5hfy_yJlJReRXPrGHHITChuwCzaKubo2hHL6LSHAr2y0SwB2lWFNN5nNOPzQrDYKiI7FwFSRtJhZT_698sW8J3n52yGVehDWOA", "hasNextPage": true, "hasPreviousPage": false, + "startCursor": "eTi5IVXuus3g1sablUWFGWkZBbE8pKn1NpODYFyekPk.6pGQw7mGK7nGKpaVhAjxa5hfy_yJlJReRXPrGHHITChuwCzaKubo2hHL6LSHAr2y0SwB2lWFNN5nNOPzQrDYKiI7FwFSRtJhZT_698sW8J3n52yGVehDWOA", }, Symbol(edgesWithRawCursor): [ { @@ -3989,8 +4179,10 @@ exports[`SqlCursorPagination > supports fully qualified column names 10`] = ` }, ], "pageInfo": { + "endCursor": "6ivs-1J-XTxms7CXt1QscsdUQ9LOZ2_k1WKxcNCh4uA.dZuwTEQegp2jJQAGcUOOYX04RyrK8r11IMU6Mwol870lhImHOcGc3BAExnrn51cTb8lzGmvZlXQQHlg", "hasNextPage": true, "hasPreviousPage": false, + "startCursor": "6ivs-1J-XTxms7CXt1QscsdUQ9LOZ2_k1WKxcNCh4uA.dZuwTEQegp2jJQAGcUOOYX04RyrK8r11IMU6Mwol870lhImHOcGc3BAExnrn51cTb8lzGmvZlXQQHlg", }, Symbol(edgesWithRawCursor): [ { diff --git a/src/assert.ts b/src/assert.ts index 5ceaba75..fbac476f 100644 --- a/src/assert.ts +++ b/src/assert.ts @@ -1,5 +1,7 @@ -export function notNull(input: T | null): T { - /* c8 ignore next */ - if (input === null) throw new Error('Input was undefined'); +export function notNull(input: T | null | undefined): T { + /* c8 ignore next 3 */ + if (input === null || input === undefined) { + throw new Error('Input was undefined'); + } return input; } diff --git a/src/sql-cursor-pagination.test.ts b/src/sql-cursor-pagination.test.ts index 15768954..646cd169 100644 --- a/src/sql-cursor-pagination.test.ts +++ b/src/sql-cursor-pagination.test.ts @@ -207,6 +207,8 @@ describe('SqlCursorPagination', () => { expect(res.edges[0].node.first_name).toBe('Anika'); expect(res.pageInfo.hasPreviousPage).toBe(false); expect(res.pageInfo.hasNextPage).toBe(false); + expect(res.pageInfo.startCursor).toBe(res.edges[0].cursor); + expect(res.pageInfo.endCursor).toBe(res.edges[res.edges.length - 1].cursor); expect(res).toMatchSnapshot(); }); @@ -337,6 +339,28 @@ describe('SqlCursorPagination', () => { expect(res).toMatchSnapshot(); }); + it('returns nothing when selecting after the last row', async () => { + const all = await go({ + query: { + first: Infinity, + }, + }); + + const res = await go({ + query: { + afterCursor: all.edges[all.edges.length - 1].cursor, + first: 1, + }, + }); + + expect(res.edges).toHaveLength(0); + expect(res.pageInfo.hasPreviousPage).toBe(false); + expect(res.pageInfo.hasNextPage).toBe(false); + expect(res.pageInfo.startCursor).toBe(null); + expect(res.pageInfo.endCursor).toBe(null); + expect(res).toMatchSnapshot(); + }); + it('selects last 2 rows when selecting 2 after the third row', async () => { const all = await go({ query: { @@ -544,6 +568,8 @@ describe('SqlCursorPagination', () => { }); expect('cursor' in all.edges[0]).toBe(false); + expect('startCursor' in all.pageInfo).toBe(false); + expect('endCursor' in all.pageInfo).toBe(false); await expect( async () => diff --git a/src/sql-cursor-pagination.ts b/src/sql-cursor-pagination.ts index 33648f77..be2af1a3 100644 --- a/src/sql-cursor-pagination.ts +++ b/src/sql-cursor-pagination.ts @@ -146,7 +146,7 @@ export type WithPaginationResultEdge< ? { rawCursor: Cursor } : { rawCursor?: undefined }); -export type WithPaginationResultPageInfo = { +export type WithPaginationResultPageInfo = { /** * `true` if there are more items following the last one and the request was for `first`. * @@ -154,18 +154,20 @@ export type WithPaginationResultPageInfo = { */ hasNextPage: boolean; /** - * `true` if there are more items before the first oneand the request was for `last`. + * `true` if there are more items before the first one and the request was for `last`. * * Otherwise this will be `false`. */ hasPreviousPage: boolean; -}; +} & (TIncludeCursor extends true + ? { startCursor: string | null; endCursor: string | null } + : { startCursor?: undefined; endCursor?: undefined }); export type WithPaginationResult = { /** * Contains `hasNextPage`/`hasPreviousPage`. */ - pageInfo: WithPaginationResultPageInfo; + pageInfo: WithPaginationResultPageInfo; /** * An entry for each row in the result, that contains the row and cursor. */ @@ -461,12 +463,19 @@ export async function withPagination< return { ...(cursor !== undefined ? { cursor } : {}), node }; }); + const pageInfo: WithPaginationResultPageInfo = { + hasNextPage, + hasPreviousPage, + ...(cursorSecret !== null && { + endCursor: + edges.length > 0 ? notNull(edges[edges.length - 1].cursor) : null, + startCursor: edges.length > 0 ? notNull(edges[0].cursor) : null, + }), + } as WithPaginationResultPageInfo; + return { edges, [edgesWithRawCursorSymbol]: edgesWithRawCursor, - pageInfo: { - hasNextPage, - hasPreviousPage, - }, + pageInfo, }; }