Last active
December 21, 2022 17:19
-
-
Save VojtaSim/6b03466f1964a6c81a3dbf1f8cec8d5c to your computer and use it in GitHub Desktop.
TypeORM + TypeGraphQL cursor pagination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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