import { Brackets , ObjectType , OrderByCondition , SelectQueryBuilder , WhereExpression , } from 'typeorm' ; import { atob , btoa , encodeByType , decodeByType , pascalToUnderscore , } from './utils' ; export type Order = 'ASC' | 'DESC' ; export type EscapeFn = ( name : string ) => string ; interface CursorParam { [ key ] : any ; } export interface Cursor { beforeCursor : string | null ; afterCursor : string | null ; } export interface PagingResult < Entity > { data : Entity [ ] ; cursor : Cursor ; } export default class Paginator < Entity > { private afterCursor : string | null = null ; private beforeCursor : string | null = null ; private nextAfterCursor : string | null = null ; private nextBeforeCursor : string | null = null ; private alias : string = pascalToUnderscore ( this . entity . name ) ; private limit = 100 ; private order : Order = 'DESC' ; public constructor ( private entity , private paginationKeys ) { } public setAlias ( alias ) { this . alias = alias ; } public setAfterCursor ( cursor ) { this . afterCursor = cursor ; } public setBeforeCursor ( cursor ) { this . beforeCursor = cursor ; } public setLimit ( limit ) { this . limit = limit ; } public setOrder ( order ) { this . order = order ; } public async paginate ( builder ) { const entities = await this . appendPagingQuery ( builder ) . getMany ( ) ; const hasMore = entities . length > this . limit ; if ( hasMore ) { entities . splice ( entities . length - 1 , 1 ) ; } if ( entities . length === 0 ) { return this . toPagingResult ( entities ) ; } if ( ! this . hasAfterCursor ( ) && this . hasBeforeCursor ( ) ) { entities . reverse ( ) ; } if ( this . hasBeforeCursor ( ) || hasMore ) { this . nextAfterCursor = this . encode ( entities [ entities . length - 1 ] ) ; } if ( this . hasAfterCursor ( ) || ( hasMore && this . hasBeforeCursor ( ) ) ) { this . nextBeforeCursor = this . encode ( entities [ 0 ] ) ; } return this . toPagingResult ( entities ) ; } private getCursor ( ) { return { afterCursor : this . nextAfterCursor , beforeCursor : this . nextBeforeCursor , } ; } private appendPagingQuery ( builder ) { const cursors = { } ; if ( this . hasAfterCursor ( ) ) { Object . assign ( cursors , this . decode ( this . afterCursor as string ) ) ; } else if ( this . hasBeforeCursor ( ) ) { Object . assign ( cursors , this . decode ( this . beforeCursor as string ) ) ; } if ( Object . keys ( cursors ) . length > 0 ) { builder . andWhere ( new Brackets ( ( where ) => this . buildCursorQuery ( where , cursors ) ) ) ; } builder . take ( this . limit + 1 ) ; builder . orderBy ( this . buildOrder ( ) ) ; return builder ; } private buildCursorQuery ( where , cursors ) { const operator = this . getOperator ( ) ; const params = { } ; let query = '' ; this . paginationKeys . forEach ( ( key ) => { params [ key ] = cursors [ key ] ; where . orWhere ( `${ query }${ this . alias }.${ key } ${ operator } :${ key }` , params ) ; query = `${ query }${ this . alias }.${ key } = :${ key } AND ` ; } ) ; } private getOperator ( ) { if ( this . hasAfterCursor ( ) ) { return this . order === 'ASC' ? '>' : '<' ; } if ( this . hasBeforeCursor ( ) ) { return this . order === 'ASC' ? '<' : '>' ; } return '=' ; } private buildOrder ( ) { let { order } = this ; if ( ! this . hasAfterCursor ( ) && this . hasBeforeCursor ( ) ) { order = this . flipOrder ( order ) ; } const orderByCondition = { } ; this . paginationKeys . forEach ( ( key ) => { orderByCondition [ `${ this . alias }.${ key }` ] = order ; } ) ; return orderByCondition ; } private hasAfterCursor ( ) { return this . afterCursor !== null ; } private hasBeforeCursor ( ) { return this . beforeCursor !== null ; } private encode ( entity ) { const payload = this . paginationKeys . map ( ( key ) => { const type = this . getEntityPropertyType ( key ) ; const value = encodeByType ( type , entity [ key ] ) ; return `${ key }:${ value }` ; } ) . join ( ',' ) ; return btoa ( payload ) ; } private decode ( cursor ) { const cursors = { } ; const columns = atob ( cursor ) . split ( ',' ) ; columns . forEach ( ( column ) => { const [ key , raw ] = column . split ( ':' ) ; const type = this . getEntityPropertyType ( key ) ; const value = decodeByType ( type , raw ) ; cursors [ key ] = value ; } ) ; return cursors ; } private getEntityPropertyType ( key ) { return Reflect . getMetadata ( 'design:type' , this . entity . prototype , key ) . name . toLowerCase ( ) ; } private flipOrder ( order ) { return order === 'ASC' ? 'DESC' : 'ASC' ; } private toPagingResult < Entity > ( entities ) { return { data : entities , cursor : this . getCursor ( ) , } ; } }