Skip to content

Instantly share code, notes, and snippets.

@VojtaSim
Last active December 21, 2022 17:19

Revisions

  1. VojtaSim revised this gist Apr 26, 2019. 1 changed file with 28 additions and 54 deletions.
    82 changes: 28 additions & 54 deletions cursorPagination.ts
    Original file line number Diff line number Diff line change
    @@ -18,6 +18,7 @@ export class CursorPaginationArgs {
    export class CursorPagination<TEntity> {

    protected resultsQuery: SelectQueryBuilder<TEntity>;
    protected countQuery: SelectQueryBuilder<TEntity>;

    protected args: CursorPaginationArgs;

    @@ -44,13 +45,8 @@ export class CursorPagination<TEntity> {
    selectiveCondition = [`${this.tableName}.${this.cursorColumn} < :cursor`, { cursor: args.before }];
    }

    if (query.expressionMap.wheres && query.expressionMap.wheres.length) {
    query = query.andWhere(...selectiveCondition);
    } else {
    query = query.where(...selectiveCondition);
    }

    this.resultsQuery = query
    this.countQuery = query.clone();
    this.resultsQuery = this.applyWhereConditionToQuery(query, selectiveCondition)
    .orderBy(`${this.tableName}.${this.cursorColumn}`, 'ASC')
    .limit(args.limit);
    }
    @@ -79,21 +75,24 @@ export class CursorPagination<TEntity> {
    }

    protected async getCount(startCursor: number, endCursor: number) {
    const beforeCountQuery = this.stipLimitationsFromQuery(this.resultsQuery)
    .select(`COUNT(DISTINCT(${this.tableName}.${this.cursorColumn})) as \"count\"`);
    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 beforeCountQuery
    .where(`${this.tableName}.${this.cursorColumn} < :cursor`, { cursor: startCursor })
    .getRawOne();
    const beforeCountResult = await (this.applyWhereConditionToQuery(
    beforeCountQuery,
    [`${this.tableName}.${this.cursorColumn} < :cursor`, { cursor: startCursor }]
    ).getRawOne());

    const afterCountResult = await afterCountQuery
    .where(`${this.tableName}.${this.cursorColumn} > :cursor`, { cursor: endCursor })
    .getRawOne();
    const afterCountResult = await (this.applyWhereConditionToQuery(
    afterCountQuery,
    [`${this.tableName}.${this.cursorColumn} > :cursor`, { cursor: endCursor }]
    ).getRawOne());

    return {
    totalCount: await this.resultsQuery.getCount(),
    totalCount: await totalCountQuery.getCount(),
    moreAfter: afterCountResult['count'],
    moreBefore: beforeCountResult['count']
    };
    @@ -106,9 +105,20 @@ export class CursorPagination<TEntity> {
    }));
    }

    protected stipLimitationsFromQuery(query: SelectQueryBuilder<TEntity>) {
    query = query.clone();
    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;
    @@ -117,40 +127,4 @@ export class CursorPagination<TEntity> {

    return query;
    }
    }

    export function CursorPaginated<TItem>(TItemClass: ClassType<TItem>) {

    @ObjectType(`${TItemClass.name}Edge`)
    class Edge {
    @Field(type => [TItemClass])
    node!: TItem;

    @Field({ description: 'Cursor for single node.' })
    cursor: TCursor;
    }

    @ObjectType(`${TItemClass.name}List`)
    class List {

    @Field(type => [Edge])
    edges!: Edge[];

    @Field(type => Cursor)
    startCursor?: TCursor;

    @Field(type => Cursor)
    endCursor?: TCursor;

    @Field(type => Int)
    totalCount: number;

    @Field()
    moreBefore!: number;

    @Field()
    moreAfter!: number;
    }

    return List;
    }
  2. VojtaSim created this gist Apr 26, 2019.
    156 changes: 156 additions & 0 deletions cursorPagination.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,156 @@
    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 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 }];
    }

    if (query.expressionMap.wheres && query.expressionMap.wheres.length) {
    query = query.andWhere(...selectiveCondition);
    } else {
    query = query.where(...selectiveCondition);
    }

    this.resultsQuery = query
    .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 beforeCountQuery = this.stipLimitationsFromQuery(this.resultsQuery)
    .select(`COUNT(DISTINCT(${this.tableName}.${this.cursorColumn})) as \"count\"`);

    const afterCountQuery = beforeCountQuery.clone();

    const beforeCountResult = await beforeCountQuery
    .where(`${this.tableName}.${this.cursorColumn} < :cursor`, { cursor: startCursor })
    .getRawOne();

    const afterCountResult = await afterCountQuery
    .where(`${this.tableName}.${this.cursorColumn} > :cursor`, { cursor: endCursor })
    .getRawOne();

    return {
    totalCount: await this.resultsQuery.getCount(),
    moreAfter: afterCountResult['count'],
    moreBefore: beforeCountResult['count']
    };
    }

    protected createEdges(results: TEntity[]) {
    return results.map((result: TEntity) => ({
    node: result,
    cursor: result[this.cursorColumn]
    }));
    }

    protected stipLimitationsFromQuery(query: SelectQueryBuilder<TEntity>) {
    query = query.clone();

    query.expressionMap.groupBys = [];
    query.expressionMap.offset = undefined;
    query.expressionMap.limit = undefined;
    query.expressionMap.skip = undefined;
    query.expressionMap.take = undefined;

    return query;
    }
    }

    export function CursorPaginated<TItem>(TItemClass: ClassType<TItem>) {

    @ObjectType(`${TItemClass.name}Edge`)
    class Edge {
    @Field(type => [TItemClass])
    node!: TItem;

    @Field({ description: 'Cursor for single node.' })
    cursor: TCursor;
    }

    @ObjectType(`${TItemClass.name}List`)
    class List {

    @Field(type => [Edge])
    edges!: Edge[];

    @Field(type => Cursor)
    startCursor?: TCursor;

    @Field(type => Cursor)
    endCursor?: TCursor;

    @Field(type => Int)
    totalCount: number;

    @Field()
    moreBefore!: number;

    @Field()
    moreAfter!: number;
    }

    return List;
    }