From bfa45aa2835c6a4e811d455a0f8d5077433d2e01 Mon Sep 17 00:00:00 2001 From: Timur Ramazanov Date: Fri, 24 May 2019 13:46:04 +0300 Subject: [PATCH] Use price in cursor for offers --- src/orm/entities/offer.ts | 7 ++- src/schema/resolvers/account.ts | 4 +- src/schema/resolvers/offer.ts | 4 +- src/util/paging.ts | 77 +++++++++++++++++++++++++++++---- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/orm/entities/offer.ts b/src/orm/entities/offer.ts index 6d1d7731..1351f5ad 100644 --- a/src/orm/entities/offer.ts +++ b/src/orm/entities/offer.ts @@ -35,7 +35,12 @@ export class Offer { @Column({ name: "lastmodified" }) lastModified: number; + public static parsePagingToken(token: string) { + const [id, price] = token.split("-"); + return { id, price }; + } + public get paging_token() { - return this.id; + return `${this.id}-${this.price}`; } } diff --git a/src/schema/resolvers/account.ts b/src/schema/resolvers/account.ts index 0cee31c6..c5d2205a 100644 --- a/src/schema/resolvers/account.ts +++ b/src/schema/resolvers/account.ts @@ -116,7 +116,9 @@ export default { qb.andWhere("offers.buying = :buying", { buying: AssetTransformer.to(buying) }); } - return makeConnection(await paginate(qb, paging, "offers.id")); + const offers = await paginate(qb, paging, "offers.id", Offer.parsePagingToken); + + return makeConnection(offers); }, inflationDestination: resolvers.account }, diff --git a/src/schema/resolvers/offer.ts b/src/schema/resolvers/offer.ts index 775816c4..abb4cf97 100644 --- a/src/schema/resolvers/offer.ts +++ b/src/schema/resolvers/offer.ts @@ -86,7 +86,9 @@ export default { .setParameter("selling", AssetTransformer.to(selling)) .setParameter("buying", AssetTransformer.to(buying)); - return makeConnection(await paginate(qb, paging, "offers.id")); + const offers = await paginate(qb, paging, ["offers.price", "offers.id"], Offer.parsePagingToken); + + return makeConnection(offers); }, tick: async (root: any, args: any, ctx: IApolloContext, info: any) => { const repo = getCustomRepository(OfferRepository); diff --git a/src/util/paging.ts b/src/util/paging.ts index 6a351c86..3576a96f 100644 --- a/src/util/paging.ts +++ b/src/util/paging.ts @@ -46,17 +46,76 @@ export function properlyOrdered(records: any[], pagingParams: PagingParams): any export async function paginate( queryBuilder: SelectQueryBuilder, pagingParams: PagingParams, - cursorCol: string -): Promise { - const { limit, order } = parseCursorPagination(pagingParams); + cursorCols: string | string[], + cursorParser?: (token: string) => { [name: string]: string } +) { + return new Pager(queryBuilder, pagingParams, cursorCols, cursorParser).paginate(); +} + +class Pager { + private cursorCols: string[]; + private cursorParameters: string[]; - queryBuilder.orderBy(cursorCol, order.toUpperCase() as "ASC" | "DESC").take(limit); + constructor( + private queryBuilder: SelectQueryBuilder, + private pagingParams: PagingParams, + cursorCols: string | string[], + private cursorParser?: (token: string) => { [name: string]: string } + ) { + this.cursorCols = typeof cursorCols === "string" ? [cursorCols] : cursorCols.sort(); - if (pagingParams.after) { - queryBuilder.andWhere(`${cursorCol} > :cursor`, { cursor: pagingParams.after }); - } else if (pagingParams.before) { - queryBuilder.andWhere(`${cursorCol} < :cursor`, { cursor: pagingParams.before }); + // Strip tablename prefix + this.cursorParameters = this.cursorCols.map(col => col.replace(/^\w+\./, "")); } - return properlyOrdered(await queryBuilder.getMany(), pagingParams); + public async paginate(): Promise { + 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: string) { + 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: { [name: string]: string }): void { + 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(", "); + // (col1, col2) > (:col1, :col2) + return `(${columnNames}) ${op} (${placeholders})`; + } }