Skip to content

Commit dbf3ed0

Browse files
authored
feat: add cursor pagination (#1050)
1 parent d859852 commit dbf3ed0

File tree

6 files changed

+519
-22
lines changed

6 files changed

+519
-22
lines changed

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Pagination and filtering helper method for TypeORM repositories or query builder
1717
- Filter using operators (`$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`, `$contains`)
1818
- Include relations and nested relations
1919
- Virtual column support
20+
- Cursor-based pagination
2021

2122
## Installation
2223

@@ -93,6 +94,61 @@ http://localhost:3000/cats?limit=5&page=2&sortBy=color:DESC&search=i&filter.age=
9394
}
9495
```
9596

97+
### Example (Cursor-based Pagination)
98+
99+
The following code exposes a route using cursor-based pagination:
100+
101+
#### Endpoint
102+
103+
```url
104+
http://localhost:3000/cats?limit=5&cursor=2022-12-20T10:00:00.000Z&cursorColumn=lastVetVisit&cursorDirection=after
105+
```
106+
107+
#### Result
108+
109+
```json
110+
{
111+
"data": [
112+
{
113+
"id": 3,
114+
"name": "Shadow",
115+
"lastVetVisit": "2022-12-21T10:00:00.000Z"
116+
},
117+
{
118+
"id": 4,
119+
"name": "Luna",
120+
"lastVetVisit": "2022-12-22T10:00:00.000Z"
121+
},
122+
{
123+
"id": 5,
124+
"name": "Pepper",
125+
"lastVetVisit": "2022-12-23T10:00:00.000Z"
126+
},
127+
{
128+
"id": 6,
129+
"name": "Simba",
130+
"lastVetVisit": "2022-12-24T10:00:00.000Z"
131+
},
132+
{
133+
"id": 7,
134+
"name": "Tiger",
135+
"lastVetVisit": "2022-12-25T10:00:00.000Z"
136+
}
137+
],
138+
"meta": {
139+
"itemsPerPage": 5,
140+
"cursor": "2022-12-20T10:00:00.000Z",
141+
"firstCursor": "2022-12-21T10:00:00.000Z",
142+
"lastCursor": "2022-12-25T10:00:00.000Z"
143+
},
144+
"links": {
145+
"previous": "http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:ASC&cursor=2022-12-21T10:00:00.000Z&cursorColumn=lastVetVisit&cursorDirection=before",
146+
"current": "http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:ASC&cursor=2022-12-20T10:00:00.000Z&cursorColumn=lastVetVisit&cursorDirection=after",
147+
"next": "http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:ASC&cursor=2022-12-25T10:00:00.000Z&cursorColumn=lastVetVisit&cursorDirection=after"
148+
}
149+
}
150+
```
151+
96152
#### Code
97153

98154
```ts
@@ -232,6 +288,17 @@ const paginateConfig: PaginateConfig<CatEntity> {
232288
*/
233289
filterableColumns: { age: [FilterOperator.EQ, FilterOperator.IN] },
234290

291+
/**
292+
* Required: false
293+
* Type: (keyof CatEntity)[]
294+
* Default: None
295+
* Description: Columns that can be used as cursors for cursor-based pagination.
296+
* Typically used with date or unique & sequential columns like 'lastVetVisit' or 'id'.
297+
* If `cursorColumn` is not provided in the query, the first column in this array is used as the default.
298+
* If `cursorDirection` is not provided in the query, 'before' is used as the default direction.
299+
*/
300+
cursorableColumns: ['lastVetVisit'],
301+
235302
/**
236303
* Required: false
237304
* Type: RelationColumn<CatEntity>
@@ -259,8 +326,10 @@ const paginateConfig: PaginateConfig<CatEntity> {
259326
/**
260327
* Required: false
261328
* Type: string
262-
* Description: Allow user to choose between limit/offset and take/skip.
329+
* Description: Allow user to choose between limit/offset and take/skip, or cursor-based pagination.
263330
* Default: PaginationType.TAKE_AND_SKIP
331+
* Options: PaginationType.LIMIT_AND_OFFSET, PaginationType.TAKE_AND_SKIP, PaginationType.CURSOR
332+
* Note: CURSOR requires `cursorableColumns` to be defined.
264333
*
265334
* However, using limit/offset can cause problems with relations.
266335
*/

src/__tests__/cat.entity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
OneToOne,
1212
PrimaryGeneratedColumn,
1313
} from 'typeorm'
14-
import { CatToyEntity } from './cat-toy.entity'
1514
import { CatHomeEntity } from './cat-home.entity'
15+
import { CatToyEntity } from './cat-toy.entity'
1616
import { SizeEmbed } from './size.embed'
1717

1818
export enum CutenessLevel {

src/decorator.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ describe('Decorator', () => {
108108
searchBy: undefined,
109109
filter: undefined,
110110
select: undefined,
111+
cursor: undefined,
112+
cursorColumn: undefined,
113+
cursorDirection: undefined,
111114
path: 'http://localhost/items',
112115
})
113116
})
@@ -125,6 +128,9 @@ describe('Decorator', () => {
125128
searchBy: undefined,
126129
filter: undefined,
127130
select: undefined,
131+
cursor: undefined,
132+
cursorColumn: undefined,
133+
cursorDirection: undefined,
128134
path: 'http://localhost/items',
129135
})
130136
})
@@ -138,6 +144,9 @@ describe('Decorator', () => {
138144
'filter.name': '$not:$eq:Kitty',
139145
'filter.createdAt': ['$gte:2020-01-01', '$lte:2020-12-31'],
140146
select: ['name', 'createdAt'],
147+
cursor: 'abc123',
148+
cursorColumn: 'id',
149+
cursorDirection: 'after',
141150
})
142151

143152
const result: PaginateQuery = decoratorfactory(null, context)
@@ -157,6 +166,9 @@ describe('Decorator', () => {
157166
name: '$not:$eq:Kitty',
158167
createdAt: ['$gte:2020-01-01', '$lte:2020-12-31'],
159168
},
169+
cursor: 'abc123',
170+
cursorColumn: 'id',
171+
cursorDirection: 'after',
160172
})
161173
})
162174

@@ -169,6 +181,9 @@ describe('Decorator', () => {
169181
'filter.name': '$not:$eq:Kitty',
170182
'filter.createdAt': ['$gte:2020-01-01', '$lte:2020-12-31'],
171183
select: ['name', 'createdAt'],
184+
cursor: 'abc123',
185+
cursorColumn: 'id',
186+
cursorDirection: 'after',
172187
})
173188

174189
const result: PaginateQuery = decoratorfactory(null, context)
@@ -188,6 +203,9 @@ describe('Decorator', () => {
188203
createdAt: ['$gte:2020-01-01', '$lte:2020-12-31'],
189204
},
190205
select: ['name', 'createdAt'],
206+
cursor: 'abc123',
207+
cursorColumn: 'id',
208+
cursorDirection: 'after',
191209
})
192210
})
193211
})

src/decorator.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export interface PaginateQuery {
1919
search?: string
2020
filter?: { [column: string]: string | string[] }
2121
select?: string[]
22+
cursor?: string
23+
cursorColumn?: string
24+
cursorDirection?: 'before' | 'after'
2225
path: string
2326
}
2427

@@ -101,6 +104,10 @@ export const Paginate = createParamDecorator((_data: unknown, ctx: ExecutionCont
101104
searchBy,
102105
filter: Object.keys(filter).length ? filter : undefined,
103106
select,
107+
cursor: query.cursor ? query.cursor.toString() : undefined,
108+
cursorColumn: query.cursorColumn ? query.cursorColumn.toString() : undefined,
109+
cursorDirection:
110+
query.cursorDirection === 'after' || query.cursorDirection === 'before' ? query.cursorDirection : undefined,
104111
path,
105112
}
106113
})

0 commit comments

Comments
 (0)