Skip to content

Commit

Permalink
feat(paginator): convert Order type to enum
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Samuel Roy authored and benjamin658 committed Mar 20, 2022
1 parent f7144d8 commit 1f4bcb4
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 32 deletions.
74 changes: 46 additions & 28 deletions src/Paginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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()) {
Expand All @@ -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);
Expand All @@ -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 '=';
Expand Down Expand Up @@ -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);
}
Expand All @@ -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> {
Expand Down
4 changes: 2 additions & 2 deletions src/buildPaginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface PagingQuery {
afterCursor?: string;
beforeCursor?: string;
limit?: number;
order?: Order;
order?: Order | 'ASC' | 'DESC';
}

export interface PaginationOptions<Entity> {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions test/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -106,7 +106,7 @@ describe('TypeORM cursor-based pagination test', () => {
entity: User,
query: {
limit: 1,
order: 'DESC',
order: Order.DESC,
},
});

Expand Down

0 comments on commit 1f4bcb4

Please sign in to comment.