Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: benjamin658/typeorm-cursor-pagination
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.8.0
Choose a base ref
...
head repository: benjamin658/typeorm-cursor-pagination
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.8.1
Choose a head ref
  • 3 commits
  • 7 files changed
  • 2 contributors

Commits on Mar 20, 2022

  1. feat(paginator): convert Order type to enum

    Avoid using string literals while allowing a developer to use
    the type at runtime as an enum
    
    [x] Tests are passing + update
    [x] No breaking changes
    [x] Make Order type available as enum at runtime
    Samuel Roy authored and benjamin658 committed Mar 20, 2022
    Copy the full SHA
    1f4bcb4 View commit details
  2. fix(tests): make float column test deterministic

    Sometimes the test case would fail due to the random balances
    chosen for each user in prepareData.
    
    The test is now deterministic by using an ordered fixed list of
    balances.
    Samuel Roy authored and benjamin658 committed Mar 20, 2022
    Copy the full SHA
    a021235 View commit details

Commits on Mar 21, 2022

  1. chore(release): 0.8.1

    benjamin658 committed Mar 21, 2022
    Copy the full SHA
    8d81ad8 View commit details
Showing with 68 additions and 40 deletions.
  1. +12 −0 CHANGELOG.md
  2. +1 −1 package-lock.json
  3. +1 −1 package.json
  4. +46 −28 src/Paginator.ts
  5. +2 −2 src/buildPaginator.ts
  6. +4 −2 test/pagination.ts
  7. +2 −6 test/utils/prepareData.ts
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,18 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### [0.8.1](https://github.com/benjamin658/typeorm-cursor-pagination/compare/v0.8.0...v0.8.1) (2022-03-21)


### Features

* **paginator:** convert Order type to enum ([1f4bcb4](https://github.com/benjamin658/typeorm-cursor-pagination/commit/1f4bcb447956ca497bcbada997c722d5899e77ae))


### Bug Fixes

* **tests:** make float column test deterministic ([a021235](https://github.com/benjamin658/typeorm-cursor-pagination/commit/a021235cd95e415e2732efac8f89adf5f4258448))

## [0.8.0](https://github.com/benjamin658/typeorm-cursor-pagination/compare/v0.7.0...v0.8.0) (2022-03-18)

## [0.7.0](https://github.com/benjamin658/typeorm-cursor-pagination/compare/v0.6.0...v0.7.0) (2022-02-21)
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "typeorm-cursor-pagination",
"version": "0.8.0",
"version": "0.8.1",
"description": "Cursor-based pagination works with TypeORM.",
"main": "lib/index.js",
"scripts": {
74 changes: 46 additions & 28 deletions src/Paginator.ts
Original file line number Diff line number Diff line change
@@ -14,7 +14,10 @@ import {
pascalToUnderscore,
} from './utils';

export type Order = 'ASC' | 'DESC';
export enum Order {
ASC = 'ASC',
DESC = 'DESC',
}

export type EscapeFn = (name: string) => string;

@@ -45,13 +48,13 @@ export default class Paginator<Entity> {

private limit = 100;

private order: Order = 'DESC';
private order: Order = Order.DESC;

public constructor(
private entity: ObjectType<Entity>,
private paginationKeys: Extract<keyof Entity, string>[],
private paginationUniqueKey: Extract<keyof Entity, string>,
) { }
) {}

public setAlias(alias: string): void {
this.alias = alias;
@@ -73,7 +76,9 @@ export default class Paginator<Entity> {
this.order = order;
}

public async paginate(builder: SelectQueryBuilder<Entity>): Promise<PagingResult<Entity>> {
public async paginate(
builder: SelectQueryBuilder<Entity>,
): Promise<PagingResult<Entity>> {
const entities = await this.appendPagingQuery(builder).getMany();
const hasMore = entities.length > this.limit;

@@ -107,7 +112,9 @@ export default class Paginator<Entity> {
};
}

private appendPagingQuery(builder: SelectQueryBuilder<Entity>): SelectQueryBuilder<Entity> {
private appendPagingQuery(
builder: SelectQueryBuilder<Entity>,
): SelectQueryBuilder<Entity> {
const cursors: CursorParam = {};

if (this.hasAfterCursor()) {
@@ -117,7 +124,9 @@ export default class Paginator<Entity> {
}

if (Object.keys(cursors).length > 0) {
builder.andWhere(new Brackets((where) => this.buildCursorQuery(where, cursors)));
builder.andWhere(
new Brackets((where) => this.buildCursorQuery(where, cursors)),
);
}

builder.take(this.limit + 1);
@@ -126,31 +135,36 @@ export default class Paginator<Entity> {
return builder;
}

private buildCursorQuery(where: WhereExpressionBuilder, cursors: CursorParam): void {
private buildCursorQuery(
where: WhereExpressionBuilder,
cursors: CursorParam,
): void {
const operator = this.getOperator();
const params: CursorParam = {};
this.paginationKeys.forEach((key) => {
params[key] = cursors[key];
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);
}
}));
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);
}
}),
);
});
}

private getOperator(): string {
if (this.hasAfterCursor()) {
return this.order === 'ASC' ? '>' : '<';
return this.order === Order.ASC ? '>' : '<';
}

if (this.hasBeforeCursor()) {
return this.order === 'ASC' ? '<' : '>';
return this.order === Order.ASC ? '<' : '>';
}

return '=';
@@ -180,11 +194,13 @@ export default class Paginator<Entity> {
}

private encode(entity: Entity): string {
const payload = this.paginationKeys.map((key) => {
const type = this.getEntityPropertyType(key);
const value = encodeByType(type, entity[key]);
return `${key}:${value}`;
}).join(',');
const payload = this.paginationKeys
.map((key) => {
const type = this.getEntityPropertyType(key);
const value = encodeByType(type, entity[key]);
return `${key}:${value}`;
})
.join(',');

return btoa(payload);
}
@@ -203,13 +219,15 @@ export default class Paginator<Entity> {
}

private getEntityPropertyType(key: string): string {
return Reflect.getMetadata('design:type', this.entity.prototype, key).name.toLowerCase();
return Reflect.getMetadata(
'design:type',
this.entity.prototype,
key,
).name.toLowerCase();
}

private flipOrder(order: Order): Order {
return order === 'ASC'
? 'DESC'
: 'ASC';
return order === Order.ASC ? Order.DESC : Order.ASC;
}

private toPagingResult<Entity>(entities: Entity[]): PagingResult<Entity> {
4 changes: 2 additions & 2 deletions src/buildPaginator.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ export interface PagingQuery {
afterCursor?: string;
beforeCursor?: string;
limit?: number;
order?: Order;
order?: Order | 'ASC' | 'DESC';
}

export interface PaginationOptions<Entity> {
@@ -43,7 +43,7 @@ export function buildPaginator<Entity>(options: PaginationOptions<Entity>): Pagi
}

if (query.order) {
paginator.setOrder(query.order);
paginator.setOrder(query.order as Order);
}

return paginator;
6 changes: 4 additions & 2 deletions test/pagination.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { createQueryBuilder } from './utils/createQueryBuilder';
import { prepareData } from './utils/prepareData';
import { User } from './entities/User';
import { Photo } from './entities/Photo';
import { buildPaginator } from '../src/index';
import { buildPaginator, Order } from '../src/index';

describe('TypeORM cursor-based pagination test', () => {
before(async () => {
@@ -91,6 +91,8 @@ describe('TypeORM cursor-based pagination test', () => {

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

it('should return entities with given order', async () => {
@@ -106,7 +108,7 @@ describe('TypeORM cursor-based pagination test', () => {
entity: User,
query: {
limit: 1,
order: 'DESC',
order: Order.DESC,
},
});

8 changes: 2 additions & 6 deletions test/utils/prepareData.ts
Original file line number Diff line number Diff line change
@@ -9,16 +9,12 @@ 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);
}
const balances = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2];

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