Skip to content

Commit

Permalink
fix(paginator) allow composite pagination keys with float columns
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
Samuel Roy authored and benjamin658 committed Mar 18, 2022
1 parent 61aa563 commit 3d2c20f
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const { data, cursor } = await paginator.paginate(queryBuilder);
* `entity` [required]: TypeORM entity.
* `alias` [optional]: alias of the query builder.
* `paginationKeys` [optional]: array of the fields to be used for the pagination, **default is `id`**.
* `paginationUniqueKey` [optional]: field to be used as a unique descriminator for the pagination, **default is `id`**.
* `query` [optional]:
* `limit`: limit the number of records returned, **default is 100**.
* `order`: **ASC** or **DESC**, **default is DESC**.
Expand Down
14 changes: 11 additions & 3 deletions src/Paginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default class Paginator<Entity> {
public constructor(
private entity: ObjectType<Entity>,
private paginationKeys: Extract<keyof Entity, string>[],
private paginationUniqueKey: Extract<keyof Entity, string>,
) { }

public setAlias(alias: string): void {
Expand Down Expand Up @@ -128,11 +129,18 @@ export default class Paginator<Entity> {
private buildCursorQuery(where: WhereExpressionBuilder, cursors: CursorParam): void {
const operator = this.getOperator();
const params: CursorParam = {};
let query = '';
this.paginationKeys.forEach((key) => {
params[key] = cursors[key];
where.orWhere(`${query}${this.alias}.${key} ${operator} :${key}`, params);
query = `${query}${this.alias}.${key} = :${key} AND `;
where.andWhere(new Brackets((qb) => {
const paramsHolder = {
[`${key}_1`]: params[key],
[`${key}_2`]: params[key],
};
qb.where(`${this.alias}.${key} ${operator} :${key}_1`, paramsHolder);
if (this.paginationUniqueKey !== key) {
qb.orWhere(`${this.alias}.${key} = :${key}_2`, paramsHolder);
}
}));
});
}

Expand Down
4 changes: 3 additions & 1 deletion src/buildPaginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface PaginationOptions<Entity> {
alias?: string;
query?: PagingQuery;
paginationKeys?: Extract<keyof Entity, string>[];
paginationUniqueKey?: Extract<keyof Entity, string>;
}

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

const paginator = new Paginator(entity, paginationKeys);
const paginator = new Paginator(entity, paginationKeys, paginationUniqueKey);

paginator.setAlias(alias);

Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function decodeByType(type: string, value: string): string | number | Dat
}

case 'number': {
const num = parseInt(value, 10);
const num = parseFloat(value);

if (Number.isNaN(num)) {
throw new Error('number column in cursor should be a valid number');
Expand Down
6 changes: 6 additions & 0 deletions test/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export class User {
})
public name!: string;

@Column({
type: 'float',
nullable: false,
})
public balance!: number;

@Column({
type: 'timestamp',
nullable: false,
Expand Down
25 changes: 25 additions & 0 deletions test/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,31 @@ describe('TypeORM cursor-based pagination test', () => {
expect(prevPageResult.data[0].id).to.eq(10);
});

it('should paginate correctly with a float column in pagination keys', async () => {
const queryBuilder = createQueryBuilder(User, 'user');
const firstPagePaginator = buildPaginator({
entity: User,
paginationKeys: ['balance', 'id'],
query: {
limit: 2,
},
});
const firstPageResult = await firstPagePaginator.paginate(queryBuilder.clone());

const nextPagePaginator = buildPaginator({
entity: User,
paginationKeys: ['balance', 'id'],
query: {
limit: 2,
afterCursor: firstPageResult.cursor.afterCursor as string,
},
});
const nextPageResult = await nextPagePaginator.paginate(queryBuilder.clone());

expect(firstPageResult.data[1].id).to.not.eq(nextPageResult.data[0].id);
expect(firstPageResult.data[1].balance).to.be.above(nextPageResult.data[0].balance);
});

it('should return entities with given order', async () => {
const queryBuilder = createQueryBuilder(User, 'user');
const ascPaginator = buildPaginator({
Expand Down
7 changes: 7 additions & 0 deletions test/utils/prepareData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ function setTimestamp(i: number): Date {
return now;
}

function getRandomFloat(min: number, max: number): number {
const str = (Math.random() * (max - min) + min).toFixed(2);

return parseFloat(str);
}

export async function prepareData(): Promise<void> {
const data = [...Array(10).keys()].map((i) => ({
name: `user${i}`,
balance: getRandomFloat(1, 2),
camelCaseColumn: setTimestamp(i),
photos: [
{
Expand Down

0 comments on commit 3d2c20f

Please sign in to comment.