Skip to content

Instantly share code, notes, and snippets.

@VojtaSim
Last active December 21, 2022 17:19
Show Gist options
  • Save VojtaSim/6b03466f1964a6c81a3dbf1f8cec8d5c to your computer and use it in GitHub Desktop.
Save VojtaSim/6b03466f1964a6c81a3dbf1f8cec8d5c to your computer and use it in GitHub Desktop.
TypeORM + TypeGraphQL cursor pagination
import { ObjectType, Field, ClassType, Int, ArgsType } from 'type-graphql';
import { SelectQueryBuilder } from 'typeorm';
import Cursor, { TCursor } from 'scalar/cursor';
@ArgsType()
export class CursorPaginationArgs {
@Field({ nullable: true })
after?: TCursor;
@Field({ nullable: true })
before?: TCursor;
@Field(type => Int)
limit?: number = 10;
}
export class CursorPagination<TEntity> {
protected resultsQuery: SelectQueryBuilder<TEntity>;
protected countQuery: SelectQueryBuilder<TEntity>;
protected args: CursorPaginationArgs;
protected tableName: string;
protected cursorColumn: string;
protected results: TEntity[];
constructor(
query: SelectQueryBuilder<TEntity>,
args: CursorPaginationArgs,
tableName?: string,
cursorColumn?: string
) {
this.tableName = query.escape(tableName);
this.cursorColumn = query.escape(cursorColumn);
this.args = args;
let selectiveCondition: [string, Object?] = [`${this.cursorColumn} >= 0`];
if (args.after) {
selectiveCondition = [`${this.tableName}.${this.cursorColumn} > :cursor`, { cursor: args.after }];
} else if (args.before) {
selectiveCondition = [`${this.tableName}.${this.cursorColumn} < :cursor`, { cursor: args.before }];
}
this.countQuery = query.clone();
this.resultsQuery = this.applyWhereConditionToQuery(query, selectiveCondition)
.orderBy(`${this.tableName}.${this.cursorColumn}`, 'ASC')
.limit(args.limit);
}
public async buildResponse(): Promise<any> {
const results = await this.getResults();
const edges = this.createEdges(results);
const startCursor = edges[0].cursor;
const endCursor = edges[edges.length - 1].cursor;
return {
edges: edges,
startCursor,
endCursor,
...this.getCount(startCursor, endCursor)
};
}
public async getResults(): Promise<TEntity[]> {
if (!this.results) {
this.results = (await this.resultsQuery.getMany());
}
return this.results;
}
protected async getCount(startCursor: number, endCursor: number) {
const totalCountQuery = this.stipLimitationsFromQuery(this.countQuery);
const beforeCountQuery = totalCountQuery.clone()
.select(`COUNT(DISTINCT(${this.tableName}.${this.cursorColumn})) as \"count\"`);
const afterCountQuery = beforeCountQuery.clone();
const beforeCountResult = await (this.applyWhereConditionToQuery(
beforeCountQuery,
[`${this.tableName}.${this.cursorColumn} < :cursor`, { cursor: startCursor }]
).getRawOne());
const afterCountResult = await (this.applyWhereConditionToQuery(
afterCountQuery,
[`${this.tableName}.${this.cursorColumn} > :cursor`, { cursor: endCursor }]
).getRawOne());
return {
totalCount: await totalCountQuery.getCount(),
moreAfter: afterCountResult['count'],
moreBefore: beforeCountResult['count']
};
}
protected createEdges(results: TEntity[]) {
return results.map((result: TEntity) => ({
node: result,
cursor: result[this.cursorColumn]
}));
}
protected applyWhereConditionToQuery(
query: SelectQueryBuilder<TEntity>,
condition: [string, Object?]
) {
if (query.expressionMap.wheres && query.expressionMap.wheres.length) {
query = query.andWhere(...condition);
} else {
query = query.where(...condition);
}
return query;
}
protected stipLimitationsFromQuery(query: SelectQueryBuilder<TEntity>) {
query.expressionMap.groupBys = [];
query.expressionMap.offset = undefined;
query.expressionMap.limit = undefined;
query.expressionMap.skip = undefined;
query.expressionMap.take = undefined;
return query;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment