import { SelectQueryBuilder } from "typeorm" ; interface IForwardPagingParams { first : number ; after : string ; } interface IBackwardPagingParams { last : number ; before : string ; } export type PagingParams = IForwardPagingParams & IBackwardPagingParams ; export enum SortOrder { DESC = "desc" , ASC = "asc" } export function invertSortOrder ( order ) { if ( order === SortOrder . DESC ) { return SortOrder . ASC ; } return SortOrder . DESC ; } export function parseCursorPagination ( args ) { const { first , after , last , before } = args ; if ( ! first && ! last ) { throw new Error ( "Missing paging parameters, specify first: or last:" ) ; } return { limit : first || last , order : last ? SortOrder . DESC : SortOrder . ASC , cursor : last ? before : after } ; } export function properlyOrdered ( records , pagingParams ) { return pagingParams . last ? records . reverse ( ) : records ; } export async function paginate ( queryBuilder , pagingParams , cursorCols , cursorParser ? ) { return new Pager ( queryBuilder , pagingParams , cursorCols , cursorParser ) . paginate ( ) ; } class Pager { private readonly cursorCols : string [ ] ; private readonly cursorParameters : string [ ] ; constructor ( private readonly queryBuilder , private readonly pagingParams , cursorCols , private readonly cursorParser ? ) { this . cursorCols = typeof cursorCols === "string" ? [ cursorCols ] : cursorCols ; this . cursorCols . sort ( ) ; this . cursorParameters = this . cursorCols . map ( col => col . replace ( /^\w+\./ , "" ) ) ; } public async paginate ( ) { const { limit , cursor , order } = parseCursorPagination ( this . pagingParams ) ; this . cursorCols . forEach ( col => { this . queryBuilder . addOrderBy ( col , order . toUpperCase ( ) as "ASC" | "DESC" ) ; } ) ; this . queryBuilder . take ( limit ) ; if ( cursor ) { this . applyCursor ( cursor ) ; } return properlyOrdered ( await this . queryBuilder . getMany ( ) , this . pagingParams ) ; } private applyCursor ( cursor ) { if ( this . pagingParams . after ) { this . queryBuilder . andWhere ( this . buildWhereExpression ( ">" ) ) ; } else if ( this . pagingParams . before ) { this . queryBuilder . andWhere ( this . buildWhereExpression ( "<" ) ) ; } if ( ! this . cursorParser ) { this . queryBuilder . setParameter ( this . cursorParameters [ 0 ] , cursor ) ; } else { const cursorVals = this . cursorParser ( cursor ) ; this . checkCursorParser ( cursorVals ) ; for ( const name of this . cursorParameters ) { this . queryBuilder . setParameter ( name , cursorVals [ name ] ) ; } } } private checkCursorParser ( cursorVals ) { const keys = Object . keys ( cursorVals ) . sort ( ) ; if ( JSON . stringify ( keys ) !== JSON . stringify ( this . cursorParameters ) ) { throw new Error ( `cursorParser returned unsuitable values, got [${ keys }], need [${ this . cursorParameters }]` ) ; } } private buildWhereExpression ( op ) { const columnNames = this . cursorCols . join ( ", " ) ; const placeholders = this . cursorParameters . map ( cp => `:${ cp }` ) . join ( ", " ) ; return `(${ columnNames }) ${ op } (${ placeholders })` ; } }