Skip to content

feat: add options to include cursor #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
22 changes: 19 additions & 3 deletions src/Paginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export default class Paginator<Entity extends ObjectLiteral> {

private beforeCursor: string | null = null;

private includeCursor: boolean | null = null;

private nextAfterCursor: string | null = null;

private nextBeforeCursor: string | null = null;
Expand Down Expand Up @@ -68,6 +70,10 @@ export default class Paginator<Entity extends ObjectLiteral> {
this.beforeCursor = cursor;
}

public setIncludeCursor(includeCursor: boolean): void {
this.includeCursor = includeCursor;
}

public setLimit(limit: number): void {
this.limit = limit;
}
Expand Down Expand Up @@ -148,15 +154,21 @@ export default class Paginator<Entity extends ObjectLiteral> {
}

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 {
Expand All @@ -182,6 +194,10 @@ export default class Paginator<Entity extends ObjectLiteral> {
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) => {
Expand Down
5 changes: 5 additions & 0 deletions src/buildPaginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Paginator, { Order } from './Paginator';
export interface PagingQuery {
afterCursor?: string;
beforeCursor?: string;
includeCursor?: boolean;
limit?: number;
order?: Order | 'ASC' | 'DESC';
}
Expand Down Expand Up @@ -38,6 +39,10 @@ export function buildPaginator<Entity extends ObjectLiteral>(
paginator.setBeforeCursor(query.beforeCursor);
}

if (query.includeCursor) {
paginator.setIncludeCursor(query.includeCursor);
}

if (query.limit) {
paginator.setLimit(query.limit);
}
Expand Down
76 changes: 76 additions & 0 deletions test/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down