Skip to content

Commit 3d2c20f

Browse files
Samuel Roybenjamin658
Samuel Roy
authored andcommitted
fix(paginator) allow composite pagination keys with float columns
This fix allows advanced pagination keys where the unique column key doesn't have to be called `id` and can be specified through a new attribute called `paginationUniqueKey` `paginationUniqueKey` defaults to `id`
1 parent 61aa563 commit 3d2c20f

File tree

7 files changed

+54
-5
lines changed

7 files changed

+54
-5
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const { data, cursor } = await paginator.paginate(queryBuilder);
4747
* `entity` [required]: TypeORM entity.
4848
* `alias` [optional]: alias of the query builder.
4949
* `paginationKeys` [optional]: array of the fields to be used for the pagination, **default is `id`**.
50+
* `paginationUniqueKey` [optional]: field to be used as a unique descriminator for the pagination, **default is `id`**.
5051
* `query` [optional]:
5152
* `limit`: limit the number of records returned, **default is 100**.
5253
* `order`: **ASC** or **DESC**, **default is DESC**.

src/Paginator.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default class Paginator<Entity> {
5050
public constructor(
5151
private entity: ObjectType<Entity>,
5252
private paginationKeys: Extract<keyof Entity, string>[],
53+
private paginationUniqueKey: Extract<keyof Entity, string>,
5354
) { }
5455

5556
public setAlias(alias: string): void {
@@ -128,11 +129,18 @@ export default class Paginator<Entity> {
128129
private buildCursorQuery(where: WhereExpressionBuilder, cursors: CursorParam): void {
129130
const operator = this.getOperator();
130131
const params: CursorParam = {};
131-
let query = '';
132132
this.paginationKeys.forEach((key) => {
133133
params[key] = cursors[key];
134-
where.orWhere(`${query}${this.alias}.${key} ${operator} :${key}`, params);
135-
query = `${query}${this.alias}.${key} = :${key} AND `;
134+
where.andWhere(new Brackets((qb) => {
135+
const paramsHolder = {
136+
[`${key}_1`]: params[key],
137+
[`${key}_2`]: params[key],
138+
};
139+
qb.where(`${this.alias}.${key} ${operator} :${key}_1`, paramsHolder);
140+
if (this.paginationUniqueKey !== key) {
141+
qb.orWhere(`${this.alias}.${key} = :${key}_2`, paramsHolder);
142+
}
143+
}));
136144
});
137145
}
138146

src/buildPaginator.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface PaginationOptions<Entity> {
1414
alias?: string;
1515
query?: PagingQuery;
1616
paginationKeys?: Extract<keyof Entity, string>[];
17+
paginationUniqueKey?: Extract<keyof Entity, string>;
1718
}
1819

1920
export function buildPaginator<Entity>(options: PaginationOptions<Entity>): Paginator<Entity> {
@@ -22,9 +23,10 @@ export function buildPaginator<Entity>(options: PaginationOptions<Entity>): Pagi
2223
query = {},
2324
alias = entity.name.toLowerCase(),
2425
paginationKeys = ['id' as any],
26+
paginationUniqueKey = 'id' as any,
2527
} = options;
2628

27-
const paginator = new Paginator(entity, paginationKeys);
29+
const paginator = new Paginator(entity, paginationKeys, paginationUniqueKey);
2830

2931
paginator.setAlias(alias);
3032

src/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function decodeByType(type: string, value: string): string | number | Dat
5050
}
5151

5252
case 'number': {
53-
const num = parseInt(value, 10);
53+
const num = parseFloat(value);
5454

5555
if (Number.isNaN(num)) {
5656
throw new Error('number column in cursor should be a valid number');

test/entities/User.ts

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export class User {
2020
})
2121
public name!: string;
2222

23+
@Column({
24+
type: 'float',
25+
nullable: false,
26+
})
27+
public balance!: number;
28+
2329
@Column({
2430
type: 'timestamp',
2531
nullable: false,

test/pagination.ts

+25
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ describe('TypeORM cursor-based pagination test', () => {
6868
expect(prevPageResult.data[0].id).to.eq(10);
6969
});
7070

71+
it('should paginate correctly with a float column in pagination keys', async () => {
72+
const queryBuilder = createQueryBuilder(User, 'user');
73+
const firstPagePaginator = buildPaginator({
74+
entity: User,
75+
paginationKeys: ['balance', 'id'],
76+
query: {
77+
limit: 2,
78+
},
79+
});
80+
const firstPageResult = await firstPagePaginator.paginate(queryBuilder.clone());
81+
82+
const nextPagePaginator = buildPaginator({
83+
entity: User,
84+
paginationKeys: ['balance', 'id'],
85+
query: {
86+
limit: 2,
87+
afterCursor: firstPageResult.cursor.afterCursor as string,
88+
},
89+
});
90+
const nextPageResult = await nextPagePaginator.paginate(queryBuilder.clone());
91+
92+
expect(firstPageResult.data[1].id).to.not.eq(nextPageResult.data[0].id);
93+
expect(firstPageResult.data[1].balance).to.be.above(nextPageResult.data[0].balance);
94+
});
95+
7196
it('should return entities with given order', async () => {
7297
const queryBuilder = createQueryBuilder(User, 'user');
7398
const ascPaginator = buildPaginator({

test/utils/prepareData.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ function setTimestamp(i: number): Date {
99
return now;
1010
}
1111

12+
function getRandomFloat(min: number, max: number): number {
13+
const str = (Math.random() * (max - min) + min).toFixed(2);
14+
15+
return parseFloat(str);
16+
}
17+
1218
export async function prepareData(): Promise<void> {
1319
const data = [...Array(10).keys()].map((i) => ({
1420
name: `user${i}`,
21+
balance: getRandomFloat(1, 2),
1522
camelCaseColumn: setTimestamp(i),
1623
photos: [
1724
{

0 commit comments

Comments
 (0)