diff --git a/README.md b/README.md index cfd250e4..52c01c71 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Are you running a service, using an SQL database, and want to support cursor sty ## How it works -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. +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 (`before`/`after`), along with a `setup` object which contains the sort config. 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`/`startCursor`/`endCursor` properties. @@ -16,9 +16,9 @@ Cursor pagination was made popular by GraphQL, and this library conforms to the - First you specify the sort config. This contains a list of field names with their orders. It must contain a unique key. - Then you request how many items you would like to fetch with `first`. - Each item you get back also contains an opaque string cursor. The cursor is an encrypted string that contains the names of the fields in the sort config, alongside their values. -- To fetch the next set of items you make a new request with `first` and `afterCursor` being the cursor of the last item you received. +- To fetch the next set of items you make a new request with `first` and `after` being the cursor of the last item you received. -If you want to fetch items in reverse order you can use `last` and `beforeCursor` instead. +If you want to fetch items in reverse order you can use `last` and `before` instead. The use of cursors means if items are added/removed between requests, the user will never see the same item twice. @@ -59,24 +59,24 @@ async function fetchUsers(userInput: { admins: boolean; first?: number; last?: number; - beforeCursor?: string; - afterCursor?: string; + before?: string; + after?: string; }) { const query = db('users').where('admin', userInput.admins); const { edges, pageInfo } = await withPagination({ query: { + first: userInput.first, + last: userInput.last, + before: userInput.before, + after: userInput.after, + }, + setup: { sortFields: [ { field: 'first_name', order: userInput.order }, { field: 'last_name', order: userInput.order }, { field: 'id', order: userInput.order }, ], - first: userInput.first, - last: userInput.last, - beforeCursor: userInput.beforeCursor, - afterCursor: userInput.afterCursor, - }, - setup: { // generate one with `npx -p sql-cursor-pagination generate-secret` cursorSecret: buildCursorSecret('somethingSecret'), queryName: 'users', @@ -139,23 +139,23 @@ E.g. ### Query -| Property | Type | Required | Description | -| -------------- | --------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `first` | `number` | If `last` isn't present. | The number of rows to fetch from the start of the window. | -| `last` | `number` | If `first` isn't present. | The number of rows to fetch from the end of the window. | -| `sortFields` | `{ field: string, order: 'asc' \| 'desc' }[]` | Yes | This takes an array of objects which have `field` and `order` properties. There must be at least one entry and you must include an entry that maps to a unique key, otherwise it's possible for there to be cursor collisions, which will result in an exception. | -| `afterCursor` | `string` | No | The window will cover the row after the provided cursor, and later rows. This takes the string `cursor` from a previous result`. | -| `beforeCursor` | `string` | No | The window will cover the row before the provided cursor, and earlier rows. This takes the string `cursor` from a previous result. | +| Property | Type | Required | Description | +| -------- | -------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `first` | `number` | If `last` isn't present. | The number of rows to fetch from the start of the window. | +| `last` | `number` | If `first` isn't present. | The number of rows to fetch from the end of the window. | +| `after` | `string` | No | The window will cover the row after the provided cursor, and later rows. This takes the string `cursor` from a previous result`. | +| `before` | `string` | No | The window will cover the row before the provided cursor, and earlier rows. This takes the string `cursor` from a previous result. | ### Setup -| Property | Type | Required | Description | -| ----------------------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `runQuery` | `function` | Yes | This function is responsible for running the database query, and returning the array of rows. It is provided with a `QueryContent` object which contains a `WHERE` fragment, `ORDER BY` fragment and `limit`, which must be included in the query. | -| `queryName` | `string` | Yes | A name for this query. It should be unique to the query, and is used to bind the cursors to it. This prevents a cursor that was created for another query being used for this one. | -| `cursorSecret` | `CursorSecret` | Yes | The secret that is used to encrypt the cursor, created from `buildCursorSecret(secret: string)`. Must be at least 30 characters. Generate one with `npx -p sql-cursor-pagination generate-secret`. | -| `maxNodes` | `number` | No | The maximum number of allowed rows in the response before the `ErrTooManyNodes` error is thrown. _Default: 100_ | -| `cursorGenerationConcurrency` | `number` | No | The maximum number of cursors to generate in parallel. _Default: 10_ | +| Property | Type | Required | Description | +| ----------------------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `runQuery` | `function` | Yes | This function is responsible for running the database query, and returning the array of rows. It is provided with a `QueryContent` object which contains a `WHERE` fragment, `ORDER BY` fragment and `limit`, which must be included in the query. | +| `queryName` | `string` | Yes | A name for this query. It should be unique to the query, and is used to bind the cursors to it. This prevents a cursor that was created for another query being used for this one. | +| `sortFields` | `{ field: string, order: 'asc' \| 'desc' }[]` | Yes | This takes an array of objects which have `field` and `order` properties. There must be at least one entry and you must include an entry that maps to a unique key, otherwise it's possible for there to be cursor collisions, which will result in an exception. | +| `cursorSecret` | `CursorSecret` | Yes | The secret that is used to encrypt the cursor, created from `buildCursorSecret(secret: string)`. Must be at least 30 characters. Generate one with `npx -p sql-cursor-pagination generate-secret`. | +| `maxNodes` | `number` | No | The maximum number of allowed rows in the response before the `ErrTooManyNodes` error is thrown. _Default: 100_ | +| `cursorGenerationConcurrency` | `number` | No | The maximum number of cursors to generate in parallel. _Default: 10_ | ## Query Fragments @@ -166,9 +166,9 @@ The `whereFragmentBuilder`/`orderByFragmentBuilder` objects provide the followin ## Errors -This library exports various error objects. `SqlCursorPaginationQueryError` will be thrown if the `first`/`last`/`beforeCursor`/`afterCursor` properties are the correct javascript type, but the contents is not valid. +This library exports various error objects. `SqlCursorPaginationQueryError` will be thrown if the `first`/`last`/`before`/`after` properties are the correct javascript type, but the contents is not valid. -E.g. `ErrFirstNotInteger` is thrown if `first` was a `number`, but not an integer. `ErrBeforeCursorWrongQuery` is thrown if the provided `beforeCursor` was a valid cursor, but for a different query. You may want to map these errors to HTTP 400 responses. +E.g. `ErrFirstNotInteger` is thrown if `first` was a `number`, but not an integer. `ErrBeforeCursorWrongQuery` is thrown if the provided `before` was a valid cursor, but for a different query. You may want to map these errors to HTTP 400 responses. ## I want the raw cursor @@ -179,6 +179,6 @@ const edgesWithRawCursor = res[edgesWithRawCursorSymbol]; console.log(edgesWithRawCursor[0].rawCursor); ``` -This can then be provided to `beforeCursor`/`afterCursor` by wrapping the object with `rawCursor(object)`. +This can then be provided to `before`/`after` by wrapping the object with `rawCursor(object)`. You can also omit the `cursorSecret` and `cursor` will not be generated. diff --git a/src/__snapshots__/sql-cursor-pagination.test.ts.snap b/src/__snapshots__/sql-cursor-pagination.test.ts.snap index 7a2b9efa..92ac54d0 100644 --- a/src/__snapshots__/sql-cursor-pagination.test.ts.snap +++ b/src/__snapshots__/sql-cursor-pagination.test.ts.snap @@ -1,94 +1,94 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 1`] = `Infinity`; +exports[`SqlCursorPagination > accepts a raw \`after\` 1`] = `Infinity`; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 2`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 2`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 3`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 3`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 4`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 4`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 5`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 5`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 6`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 6`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 7`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 7`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 8`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 8`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 9`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 9`] = ` { "bindings": {}, "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 10`] = `2`; +exports[`SqlCursorPagination > accepts a raw \`after\` 10`] = `2`; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 11`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 11`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 12`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 12`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 13`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 13`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 14`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 14`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 15`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 15`] = ` { "bindings": [ "Cooper", @@ -102,7 +102,7 @@ exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 15`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 16`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 16`] = ` { "bindings": [ "Cooper", @@ -116,7 +116,7 @@ exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 16`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 17`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 17`] = ` { "bindings": [ "Cooper", @@ -130,7 +130,7 @@ exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 17`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 18`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 18`] = ` { "bindings": { ":0": "Cooper", @@ -141,7 +141,7 @@ exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 18`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 19`] = ` +exports[`SqlCursorPagination > accepts a raw \`after\` 19`] = ` { "edges": [ { @@ -188,95 +188,95 @@ exports[`SqlCursorPagination > accepts a raw \`afterCursor\` 19`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 1`] = `Infinity`; +exports[`SqlCursorPagination > accepts a raw \`before\` 1`] = `Infinity`; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 2`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 2`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 3`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 3`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 4`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 4`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 5`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 5`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 6`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 6`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 7`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 7`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 8`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 8`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 9`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 9`] = ` { "bindings": {}, "sql": "1", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 10`] = `2`; +exports[`SqlCursorPagination > accepts a raw \`before\` 10`] = `2`; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 11`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 11`] = ` { "bindings": [], "sql": "\`first_name\` desc, \`last_name\` asc, \`id\` desc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 12`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 12`] = ` { "bindings": [], "sql": "\`first_name\` desc, \`last_name\` asc, \`id\` desc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 13`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 13`] = ` { "bindings": [], "sql": "\`first_name\` desc, \`last_name\` asc, \`id\` desc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 14`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 14`] = ` { "bindings": {}, "sql": "\`first_name\` desc, \`last_name\` asc, \`id\` desc", } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 15`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 15`] = ` { "bindings": [ "Jermaine", @@ -290,7 +290,7 @@ exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 15`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 16`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 16`] = ` { "bindings": [ "Jermaine", @@ -304,7 +304,7 @@ exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 16`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 17`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 17`] = ` { "bindings": [ "Jermaine", @@ -318,7 +318,7 @@ exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 17`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 18`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 18`] = ` { "bindings": { ":0": "Jermaine", @@ -329,7 +329,7 @@ exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 18`] = ` } `; -exports[`SqlCursorPagination > accepts a raw \`beforeCursor\` 19`] = ` +exports[`SqlCursorPagination > accepts a raw \`before\` 19`] = ` { "edges": [ { @@ -434,232 +434,232 @@ exports[`SqlCursorPagination > allows the \`cursorSecret\` to be omitted 9`] = ` } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 1`] = `Infinity`; +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 1`] = `Infinity`; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 2`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 2`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 3`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 3`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 4`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 4`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 5`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 5`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 6`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 6`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 7`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 7`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 8`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 8`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` is for wrong query 9`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` is for wrong query 9`] = ` { "bindings": {}, "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 1`] = `Infinity`; +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 1`] = `Infinity`; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 2`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 2`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 3`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 3`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 4`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 4`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 5`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 5`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 6`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 6`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 7`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 7`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 8`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 8`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`afterCursor\` was for a different sort config 9`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`after\` was for a different sort config 9`] = ` { "bindings": {}, "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 1`] = `Infinity`; +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 1`] = `Infinity`; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 2`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 2`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 3`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 3`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 4`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 4`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 5`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 5`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 6`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 6`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 7`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 7`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 8`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 8`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` is for wrong query 9`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` is for wrong query 9`] = ` { "bindings": {}, "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 1`] = `Infinity`; +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 1`] = `Infinity`; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 2`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 2`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 3`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 3`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 4`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 4`] = ` { "bindings": [], "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 5`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 5`] = ` { "bindings": {}, "sql": "\`first_name\` asc, \`last_name\` desc, \`id\` asc", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 6`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 6`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 7`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 7`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 8`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 8`] = ` { "bindings": [], "sql": "1", } `; -exports[`SqlCursorPagination > errors > throws an error if \`beforeCursor\` was for a different sort config 9`] = ` +exports[`SqlCursorPagination > errors > throws an error if \`before\` was for a different sort config 9`] = ` { "bindings": {}, "sql": "1", diff --git a/src/errors.ts b/src/errors.ts index 4e32d5b4..52b75cbf 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -51,13 +51,13 @@ export class ErrFirstNotGreaterThanLast extends SqlCursorPaginationQueryError { export class ErrBeforeCursorInvalid extends SqlCursorPaginationQueryError { constructor() { - super('ErrBeforeCursorInvalid', '`beforeCursor` invalid'); + super('ErrBeforeCursorInvalid', '`before` invalid'); } } export class ErrAfterCursorInvalid extends SqlCursorPaginationQueryError { constructor() { - super('ErrAfterCursorInvalid', '`afterCursor` invalid'); + super('ErrAfterCursorInvalid', '`after` invalid'); } } @@ -72,19 +72,13 @@ export class ErrTooManyNodes extends SqlCursorPaginationQueryError { export class ErrBeforeCursorWrongQuery extends SqlCursorPaginationQueryError { constructor() { - super( - 'ErrBeforeCursorWrongQuery', - '`beforeCursor` created for different query', - ); + super('ErrBeforeCursorWrongQuery', '`before` created for different query'); } } export class ErrAfterCursorWrongQuery extends SqlCursorPaginationQueryError { constructor() { - super( - 'ErrAfterCursorWrongQuery', - '`afterCursor` created for different query', - ); + super('ErrAfterCursorWrongQuery', '`after` created for different query'); } } @@ -92,7 +86,7 @@ export class ErrBeforeCursorWrongSortConfig extends SqlCursorPaginationQueryErro constructor() { super( 'ErrBeforeCursorWrongSortConfig', - '`beforeCursor` cursor created for different sort configuration', + '`before` cursor created for different sort configuration', ); } } @@ -101,7 +95,7 @@ export class ErrAfterCursorWrongSortConfig extends SqlCursorPaginationQueryError constructor() { super( 'ErrAfterCursorWrongSortConfig', - '`afterCursor` cursor created for different sort configuration', + '`after` cursor created for different sort configuration', ); } } diff --git a/src/sql-cursor-pagination.test.ts b/src/sql-cursor-pagination.test.ts index 646cd169..106aba75 100644 --- a/src/sql-cursor-pagination.test.ts +++ b/src/sql-cursor-pagination.test.ts @@ -175,19 +175,17 @@ describe('SqlCursorPagination', () => { setup?: Partial>; }): Promise> { const input: WithPaginationInput = { - query: { - sortFields: [ - { field: 'first_name', order: Asc }, - { field: 'last_name', order: Desc }, - { field: 'id', order: Asc }, - ], - ...query, - }, + query, setup: { cursorSecret: mockCursorSecret, maxNodes: Infinity, queryName: mockQueryName, runQuery: buildRunQuery(), + sortFields: [ + { field: 'first_name', order: Asc }, + { field: 'last_name', order: Desc }, + { field: 'id', order: Asc }, + ], ...setup, }, }; @@ -250,7 +248,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[1].cursor, + after: all.edges[1].cursor, first: 1, }, }); @@ -271,7 +269,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - beforeCursor: all.edges[2].cursor, + before: all.edges[2].cursor, last: 1, }, }); @@ -292,7 +290,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - beforeCursor: all.edges[2].cursor, + before: all.edges[2].cursor, last: 2, }, }); @@ -327,7 +325,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - beforeCursor: all.edges[2].cursor, + before: all.edges[2].cursor, last: 1, }, }); @@ -348,7 +346,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[all.edges.length - 1].cursor, + after: all.edges[all.edges.length - 1].cursor, first: 1, }, }); @@ -370,7 +368,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[2].cursor, + after: all.edges[2].cursor, first: 2, }, }); @@ -435,8 +433,8 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[0].cursor, - beforeCursor: all.edges[2].cursor, + after: all.edges[0].cursor, + before: all.edges[2].cursor, first: Infinity, }, }); @@ -457,8 +455,8 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[0].cursor, - beforeCursor: all.edges[3].cursor, + after: all.edges[0].cursor, + before: all.edges[3].cursor, first: Infinity, }, }); @@ -480,8 +478,8 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[0].cursor, - beforeCursor: all.edges[3].cursor, + after: all.edges[0].cursor, + before: all.edges[3].cursor, first: 1, }, }); @@ -502,8 +500,8 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: all.edges[0].cursor, - beforeCursor: all.edges[3].cursor, + after: all.edges[0].cursor, + before: all.edges[3].cursor, last: 1, }, }); @@ -515,7 +513,7 @@ describe('SqlCursorPagination', () => { expect(res).toMatchSnapshot(); }); - it('accepts a raw `beforeCursor`', async () => { + it('accepts a raw `before`', async () => { const all = await go({ query: { first: Infinity, @@ -524,7 +522,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - beforeCursor: rawCursor(all[edgesWithRawCursorSymbol][3].rawCursor), + before: rawCursor(all[edgesWithRawCursorSymbol][3].rawCursor), last: 1, }, }); @@ -536,7 +534,7 @@ describe('SqlCursorPagination', () => { expect(res).toMatchSnapshot(); }); - it('accepts a raw `afterCursor`', async () => { + it('accepts a raw `after`', async () => { const all = await go({ query: { first: Infinity, @@ -545,7 +543,7 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { - afterCursor: rawCursor(all[edgesWithRawCursorSymbol][2].rawCursor), + after: rawCursor(all[edgesWithRawCursorSymbol][2].rawCursor), first: 1, }, }); @@ -576,7 +574,7 @@ describe('SqlCursorPagination', () => { await go({ query: { // @ts-expect-error: cursor cannot be string when no secret - beforeCursor: '', + before: '', first: Infinity, }, setup: { @@ -592,6 +590,8 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { first: 1, + }, + setup: { sortFields: [{ field: 'users.id', order: Asc }], }, }); @@ -605,6 +605,8 @@ describe('SqlCursorPagination', () => { const res = await go({ query: { first: 1, + }, + setup: { sortFields: [ { field: { alias: 'email_alias', name: 'email' }, order: Asc }, ], @@ -617,7 +619,7 @@ describe('SqlCursorPagination', () => { }); describe('errors', () => { - it('throws an error if `afterCursor` was for a different sort config', async () => { + it('throws an error if `after` was for a different sort config', async () => { const all = await go({ query: { first: Infinity, @@ -628,15 +630,17 @@ describe('SqlCursorPagination', () => { async () => await go({ query: { - afterCursor: all.edges[0].cursor, + after: all.edges[0].cursor, first: 1, + }, + setup: { sortFields: [{ field: 'email', order: Asc }], }, }), ).rejects.toThrowError(ErrAfterCursorWrongSortConfig); }); - it('throws an error if `beforeCursor` was for a different sort config', async () => { + it('throws an error if `before` was for a different sort config', async () => { const all = await go({ query: { first: Infinity, @@ -647,8 +651,10 @@ describe('SqlCursorPagination', () => { async () => await go({ query: { - beforeCursor: all.edges[0].cursor, + before: all.edges[0].cursor, last: 1, + }, + setup: { sortFields: [ { field: 'email', order: Asc }, { field: 'last_name', order: Asc }, @@ -724,24 +730,24 @@ describe('SqlCursorPagination', () => { ).rejects.toThrowError(ErrFirstNotGreaterThanLast); }); - it('throws an error if `beforeCursor` is invalid', async () => { + it('throws an error if `before` is invalid', async () => { await expect( async () => await go({ query: { - beforeCursor: 'invalid', + before: 'invalid', last: 1, }, }), ).rejects.toThrowError(ErrBeforeCursorInvalid); }); - it('throws an error if `afterCursor` is invalid', async () => { + it('throws an error if `after` is invalid', async () => { await expect( async () => await go({ query: { - afterCursor: 'invalid', + after: 'invalid', first: 1, }, }), @@ -760,7 +766,7 @@ describe('SqlCursorPagination', () => { await go({ query: { // @ts-expect-error raw cursor not wrapped - afterCursor: all[edgesWithRawCursorSymbol][0].rawCursor, + after: all[edgesWithRawCursorSymbol][0].rawCursor, first: 1, }, }), @@ -773,7 +779,7 @@ describe('SqlCursorPagination', () => { await go({ query: { // @ts-expect-error raw cursor not wrapped - beforeCursor: all[edgesWithRawCursorSymbol][0].rawCursor, + before: all[edgesWithRawCursorSymbol][0].rawCursor, last: 1, }, }), @@ -782,7 +788,7 @@ describe('SqlCursorPagination', () => { ); }); - it('throws an error if `beforeCursor` is for wrong query', async () => { + it('throws an error if `before` is for wrong query', async () => { const all = await go({ query: { first: Infinity, @@ -793,7 +799,7 @@ describe('SqlCursorPagination', () => { async () => await go({ query: { - beforeCursor: all.edges[0].cursor, + before: all.edges[0].cursor, last: 1, }, setup: { @@ -803,7 +809,7 @@ describe('SqlCursorPagination', () => { ).rejects.toThrowError(ErrBeforeCursorWrongQuery); }); - it('throws an error if `afterCursor` is for wrong query', async () => { + it('throws an error if `after` is for wrong query', async () => { const all = await go({ query: { first: Infinity, @@ -814,7 +820,7 @@ describe('SqlCursorPagination', () => { async () => await go({ query: { - afterCursor: all.edges[0].cursor, + after: all.edges[0].cursor, first: 1, }, setup: { @@ -845,6 +851,8 @@ describe('SqlCursorPagination', () => { await go({ query: { first: 1, + }, + setup: { sortFields: [], }, }), @@ -855,6 +863,8 @@ describe('SqlCursorPagination', () => { await go({ query: { first: 1, + }, + setup: { sortFields: [{ field: '!', order: Asc }], }, }), @@ -865,6 +875,8 @@ describe('SqlCursorPagination', () => { await go({ query: { first: 1, + }, + setup: { // @ts-expect-error invalid order sortFields: [{ field: 'a', order: 'oops' }], }, @@ -878,6 +890,8 @@ describe('SqlCursorPagination', () => { await go({ query: { first: 1, + }, + setup: { sortFields: [ { field: 'first_name', order: 'asc' }, { field: 'first_name', order: 'asc' }, @@ -893,6 +907,8 @@ describe('SqlCursorPagination', () => { await go({ query: { first: Infinity, + }, + setup: { sortFields: [{ field: 'first_name', order: Asc }], }, }), diff --git a/src/sql-cursor-pagination.ts b/src/sql-cursor-pagination.ts index be2af1a3..067daef4 100644 --- a/src/sql-cursor-pagination.ts +++ b/src/sql-cursor-pagination.ts @@ -50,35 +50,20 @@ export type WithPaginationInputQuery = { first?: number | null; /* The number of rows to fetch from the end of the window. */ last?: number | null; - /** - * This takes an array of objects which have `field` and `order` properties. - * - * The order can be `asc` or `desc`. - * - * There must be at least one entry and you must include an entry that maps to a unique key, - * otherwise it's possible for there to be cursor collisions, which will result in an exception. - */ - sortFields: readonly FieldWithOrder[]; /** * The window will cover the row after the provided cursor, and later rows. * * This takes the string `cursor` from a previous result, or you can provide a raw cursor * object by wrapping the object with `rawCursor(object)`. */ - afterCursor?: - | (TGenerateCursor extends true ? string : never) - | RawCursor - | null; + after?: (TGenerateCursor extends true ? string : never) | RawCursor | null; /** * The window will cover the row before the provided cursor, and earlier rows. * * This takes the string `cursor` from a previous result, or you can provide a raw cursor * object by wrapping the object with `rawCursor(object)`. */ - beforeCursor?: - | (TGenerateCursor extends true ? string : never) - | RawCursor - | null; + before?: (TGenerateCursor extends true ? string : never) | RawCursor | null; }; export type WithPaginationInputSetup< @@ -98,6 +83,15 @@ export type WithPaginationInputSetup< * which must be included in the query. */ runQuery: (queryContent: QueryContent) => Promise; + /** + * This takes an array of objects which have `field` and `order` properties. + * + * The order can be `asc` or `desc`. + * + * There must be at least one entry and you must include an entry that maps to a unique key, + * otherwise it's possible for there to be cursor collisions, which will result in an exception. + */ + sortFields: readonly FieldWithOrder[]; /** * A name for this query. * @@ -207,15 +201,15 @@ export async function withPagination< query: { first = null, last = null, - beforeCursor: beforeCursorInput = null, - afterCursor: afterCursorInput = null, - sortFields: _sortFields, + before: beforeInput = null, + after: afterInput = null, }, setup: { cursorGenerationConcurrency: _cursorGenerationConcurrency = 10, cursorSecret = null, maxNodes = 100, runQuery, + sortFields: _sortFields, queryName: _queryName, }, }: WithPaginationInput): Promise< @@ -256,22 +250,22 @@ export async function withPagination< } const resolvedBeforeCursor = await resolveCursor({ - cursor: beforeCursorInput, + cursor: beforeInput, cursorSecret, }); if (!resolvedBeforeCursor.success) { throw new ErrBeforeCursorInvalid(); } - const beforeCursor = resolvedBeforeCursor.cursor; + const before = resolvedBeforeCursor.cursor; const resolvedAfterCursor = await resolveCursor({ - cursor: afterCursorInput, + cursor: afterInput, cursorSecret, }); if (!resolvedAfterCursor.success) { throw new ErrAfterCursorInvalid(); } - const afterCursor = resolvedAfterCursor.cursor; + const after = resolvedAfterCursor.cursor; const requestedCount = first !== null ? first : notNull(last); if (requestedCount > maxNodes) { @@ -296,8 +290,8 @@ export async function withPagination< const whereQuery = new QueryBuilder(); for (const [type, cursor] of [ - ['before', beforeCursor], - ['after', afterCursor], + ['before', before], + ['after', after], ] as const) { if (!cursor) continue; @@ -324,7 +318,7 @@ export async function withPagination< } } - if (beforeCursor || afterCursor) { + if (before || after) { whereQuery.appendText(`(`); const build = (type: 'after' | 'before', c: Cursor): void => { @@ -351,15 +345,15 @@ export async function withPagination< } }; - if (afterCursor) { + if (after) { whereQuery.appendText(`(`); - build('after', afterCursor); + build('after', after); whereQuery.appendText(`)`); } - if (beforeCursor) { - if (afterCursor) whereQuery.appendText(` AND `); + if (before) { + if (after) whereQuery.appendText(` AND `); whereQuery.appendText(`(`); - build('before', beforeCursor); + build('before', before); whereQuery.appendText(`)`); } whereQuery.appendText(`)`);