diff --git a/README.md b/README.md index 943c95b..f09b48f 100644 --- a/README.md +++ b/README.md @@ -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**. diff --git a/src/Paginator.ts b/src/Paginator.ts index c870595..5b7332f 100644 --- a/src/Paginator.ts +++ b/src/Paginator.ts @@ -50,6 +50,7 @@ export default class Paginator { public constructor( private entity: ObjectType, private paginationKeys: Extract[], + private paginationUniqueKey: Extract, ) { } public setAlias(alias: string): void { @@ -128,11 +129,18 @@ export default class Paginator { 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); + } + })); }); } diff --git a/src/buildPaginator.ts b/src/buildPaginator.ts index 96c5049..5df6bf7 100644 --- a/src/buildPaginator.ts +++ b/src/buildPaginator.ts @@ -14,6 +14,7 @@ export interface PaginationOptions { alias?: string; query?: PagingQuery; paginationKeys?: Extract[]; + paginationUniqueKey?: Extract; } export function buildPaginator(options: PaginationOptions): Paginator { @@ -22,9 +23,10 @@ export function buildPaginator(options: PaginationOptions): 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); diff --git a/src/utils.ts b/src/utils.ts index ce80e75..2e6963d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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'); diff --git a/test/entities/User.ts b/test/entities/User.ts index a31b061..188bfbb 100644 --- a/test/entities/User.ts +++ b/test/entities/User.ts @@ -20,6 +20,12 @@ export class User { }) public name!: string; + @Column({ + type: 'float', + nullable: false, + }) + public balance!: number; + @Column({ type: 'timestamp', nullable: false, diff --git a/test/pagination.ts b/test/pagination.ts index 4fa4d1a..5f3409a 100644 --- a/test/pagination.ts +++ b/test/pagination.ts @@ -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({ diff --git a/test/utils/prepareData.ts b/test/utils/prepareData.ts index f2edc05..4c3081c 100644 --- a/test/utils/prepareData.ts +++ b/test/utils/prepareData.ts @@ -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 { const data = [...Array(10).keys()].map((i) => ({ name: `user${i}`, + balance: getRandomFloat(1, 2), camelCaseColumn: setTimestamp(i), photos: [ {