diff --git a/README.md b/README.md index f1f7319..8f120f5 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ const { data, cursor } = await paginator.paginate(queryBuilder); * `order`: **ASC** or **DESC**, **default is DESC**. * `beforeCursor`: the before cursor. * `afterCursor`: the after cursor. + * `includeCursor` [optional]: include the cursor record, **default is false**. **`paginator.paginate(queryBuilder)` returns the entities and cursor for the next iteration** diff --git a/src/Paginator.ts b/src/Paginator.ts index 09320d9..a31426a 100644 --- a/src/Paginator.ts +++ b/src/Paginator.ts @@ -41,6 +41,8 @@ export default class Paginator { private beforeCursor: string | null = null; + private includeCursor: boolean | null = null; + private nextAfterCursor: string | null = null; private nextBeforeCursor: string | null = null; @@ -68,6 +70,10 @@ export default class Paginator { this.beforeCursor = cursor; } + public setIncludeCursor(includeCursor: boolean): void { + this.includeCursor = includeCursor; + } + public setLimit(limit: number): void { this.limit = limit; } @@ -148,15 +154,21 @@ export default class Paginator { } private getOperator(): string { + let operator = '='; + if (this.hasAfterCursor()) { - return this.order === Order.ASC ? '>' : '<'; + operator = this.order === Order.ASC ? '>' : '<'; } if (this.hasBeforeCursor()) { - return this.order === Order.ASC ? '<' : '>'; + operator = this.order === Order.ASC ? '<' : '>'; } - return '='; + if (this.shouldIncludeCursor()) { + operator += '=' + } + + return operator; } private buildOrder(): OrderByCondition { @@ -182,6 +194,10 @@ export default class Paginator { return this.beforeCursor !== null; } + private shouldIncludeCursor(): boolean { + return this.includeCursor !== null ? this.includeCursor : false; + } + private encode(entity: Entity): string { const payload = this.paginationKeys .map((key) => { diff --git a/src/buildPaginator.ts b/src/buildPaginator.ts index be1a2f5..54adfcd 100644 --- a/src/buildPaginator.ts +++ b/src/buildPaginator.ts @@ -5,6 +5,7 @@ import Paginator, { Order } from './Paginator'; export interface PagingQuery { afterCursor?: string; beforeCursor?: string; + includeCursor?: boolean; limit?: number; order?: Order | 'ASC' | 'DESC'; } @@ -38,6 +39,10 @@ export function buildPaginator( paginator.setBeforeCursor(query.beforeCursor); } + if (query.includeCursor) { + paginator.setIncludeCursor(query.includeCursor); + } + if (query.limit) { paginator.setLimit(query.limit); } diff --git a/test/pagination.ts b/test/pagination.ts index 448ecc2..93dd652 100644 --- a/test/pagination.ts +++ b/test/pagination.ts @@ -145,6 +145,82 @@ describe('TypeORM cursor-based pagination test', () => { expect(result.cursor.afterCursor).to.eq(null); }); + it('should correctly include cursor record', async () => { + const queryBuilder = createQueryBuilder(User, 'user').leftJoinAndSelect('user.photos', 'photo'); + const firstPagePaginator = buildPaginator({ + entity: User, + paginationKeys: ['id', 'name'], + query: { + limit: 3, + }, + }); + const firstPageResult = await firstPagePaginator.paginate(queryBuilder.clone()); + + const nextPagePaginator = buildPaginator({ + entity: User, + paginationKeys: ['id', 'name'], + query: { + limit: 3, + afterCursor: firstPageResult.cursor.afterCursor as string, + includeCursor: true, + }, + }); + const nextPageResult = await nextPagePaginator.paginate(queryBuilder.clone()); + + const afterPagePaginator = buildPaginator({ + entity: User, + paginationKeys: ['id', 'name'], + query: { + limit: 3, + afterCursor: nextPageResult.cursor.afterCursor as string, + includeCursor: true, + }, + }); + const afterPageResult = await afterPagePaginator.paginate(queryBuilder.clone()); + + const prevPagePaginator = buildPaginator({ + entity: User, + paginationKeys: ['id', 'name'], + query: { + limit: 3, + beforeCursor: afterPageResult.cursor.beforeCursor as string, + includeCursor: true, + }, + }); + const prevPageResult = await prevPagePaginator.paginate(queryBuilder.clone()); + + const beforePagePaginator = buildPaginator({ + entity: User, + paginationKeys: ['id', 'name'], + query: { + limit: 3, + beforeCursor: prevPageResult.cursor.beforeCursor as string, + includeCursor: true, + }, + }); + const beforePageResult = await beforePagePaginator.paginate(queryBuilder.clone()); + + expect(firstPageResult.data[0].id).to.eq(10); + expect(firstPageResult.data[1].id).to.eq(9); + expect(firstPageResult.data[2].id).to.eq(8); + + expect(nextPageResult.data[0].id).to.eq(8); + expect(nextPageResult.data[1].id).to.eq(7); + expect(nextPageResult.data[2].id).to.eq(6); + + expect(afterPageResult.data[0].id).to.eq(6); + expect(afterPageResult.data[1].id).to.eq(5); + expect(afterPageResult.data[2].id).to.eq(4); + + expect(prevPageResult.data[0].id).to.eq(8); + expect(prevPageResult.data[1].id).to.eq(7); + expect(prevPageResult.data[2].id).to.eq(6); + + expect(beforePageResult.data[0].id).to.eq(10); + expect(beforePageResult.data[1].id).to.eq(9); + expect(beforePageResult.data[2].id).to.eq(8); + }); + after(async () => { await getConnection().query('TRUNCATE TABLE users RESTART IDENTITY CASCADE;'); await getConnection().close();