Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
NestJS Graphql Cursor Based pagination
import { ObjectType, Field } from "@nestjs/graphql";
@ObjectType()
export class PageInfo {
@Field({ nullable: true })
startCursor: string;
@Field({ nullable: true })
endCursor: string;
@Field()
hasPreviousPage: boolean;
@Field()
hasNextPage: boolean;
}
import { Logger } from '@nestjs/common';
import { PageInfo } from './page-info';
import { PaginationArgs } from './pagination.args';
import { SelectQueryBuilder, MoreThan, LessThan } from 'typeorm';
/**
* Based on https://gist.github.com/VojtaSim/6b03466f1964a6c81a3dbf1f8cec8d5c
*/
export async function paginate<T>(
query: SelectQueryBuilder<T>,
paginationArgs: PaginationArgs,
cursorColumn = 'id',
defaultLimit = 25,
): Promise<any> {
const logger = new Logger('Pagination');
// pagination ordering
query.orderBy({ [cursorColumn]: 'DESC' })
const totalCountQuery = query.clone();
// FORWARD pagination
if (paginationArgs.first) {
if (paginationArgs.after) {
const offsetId = Number(Buffer.from(paginationArgs.after, 'base64').toString('ascii'));
logger.verbose(`Paginate AfterID: ${offsetId}`);
query.where({ [cursorColumn]: MoreThan(offsetId) });
}
const limit = paginationArgs.first ?? defaultLimit;
query.take(limit)
}
// REVERSE pagination
else if (paginationArgs.last && paginationArgs.before) {
const offsetId = Number(Buffer.from(paginationArgs.before, 'base64').toString('ascii'));
logger.verbose(`Paginate BeforeID: ${offsetId}`);
const limit = paginationArgs.last ?? defaultLimit;
query
.where({ [cursorColumn]: LessThan(offsetId) })
.take(limit);
}
const result = await query.getMany();
const startCursorId: number = result.length > 0 ? result[0][cursorColumn] : null;
const endCursorId: number = result.length > 0 ? result.slice(-1)[0][cursorColumn] : null;
const beforeQuery = totalCountQuery.clone();
const afterQuery = beforeQuery.clone();
let countBefore = 0;
let countAfter = 0;
if (beforeQuery.expressionMap.wheres && beforeQuery.expressionMap.wheres.length) {
countBefore = await beforeQuery
.andWhere(`${cursorColumn} < :cursor`, { cursor: startCursorId })
.getCount();
countAfter = await afterQuery
.andWhere(`${cursorColumn} > :cursor`, { cursor: endCursorId })
.getCount();
} else {
countBefore = await beforeQuery
.where(`${cursorColumn} < :cursor`, { cursor: startCursorId })
.getCount();
countAfter = await afterQuery
.where(`${cursorColumn} > :cursor`, { cursor: endCursorId })
.getCount();
}
logger.debug(`CountBefore:${countBefore}`);
logger.debug(`CountAfter:${countAfter}`);
const edges = result.map((value) => {
return {
node: value,
cursor: Buffer.from(`${value[cursorColumn]}`).toString('base64'),
};
});
const pageInfo = new PageInfo();
pageInfo.startCursor = edges.length > 0 ? edges[0].cursor : null;
pageInfo.endCursor = edges.length > 0 ? edges.slice(-1)[0].cursor : null;
pageInfo.hasNextPage = countAfter > 0;
pageInfo.hasPreviousPage = countBefore > 0;
// pageInfo.countBefore = countBefore;
// pageInfo.countNext = countAfter;
// pageInfo.countCurrent = edges.length;
// pageInfo.countTotal = countAfter + countBefore + edges.length;
return { edges, pageInfo };
}
/**
* Example of paginated graphql model
*/
import { Post } from "../models/post.model";
import { ObjectType } from '@nestjs/graphql';
import { Paginated } from "src/shared/pagination/types/paginated";
@ObjectType()
export class PaginatedPost extends Paginated(Post) { }
import { Field, ObjectType } from '@nestjs/graphql';
import { Type } from '@nestjs/common';
import { PageInfo } from './page-info';
/**
* Based on https://docs.nestjs.com/graphql/resolvers#generics
*
* @param classRef
*/
export function Paginated<T>(classRef: Type<T>): any {
@ObjectType(`${classRef.name}Edge`, { isAbstract: true })
abstract class EdgeType {
@Field(() => String)
cursor: string;
@Field(() => classRef)
node: T;
}
@ObjectType({ isAbstract: true })
abstract class PaginatedType {
@Field(() => [EdgeType], { nullable: true })
edges: EdgeType[];
@Field(() => PageInfo, { nullable: true })
pageInfo: PageInfo;
}
return PaginatedType;
}
import { ArgsType, Int, Field } from '@nestjs/graphql';
@ArgsType()
export class PaginationArgs {
@Field(() => Int, { nullable: true })
first: number;
@Field(() => String, { nullable: true })
after: string;
@Field(() => Int, { nullable: true })
last: number;
@Field(() => String, { nullable: true })
before: string;
}
import { Post } from "../models/post.model";
import { PostService } from '../providers/post.service';
@Resolver(() => Post)
export class PostResolver {
constructor(private readonly postService: PostService) { }
@Query(() => PaginatedPost)
getPosts(
@Args() pagination: PaginationArgs,
@Args() filter: PostFilter,
): Promise<PaginatedPost> {
return this.postService.getPaginatedPosts(pagination, filter);
}
}
import { paginate } from './paginate';
@Injectable()
export class PostService {
private readonly logger = new Logger('PostService');
constructor(
@InjectRepository(PostRepository)
private postRepository: PostRepository,
) { }
async getPaginatedPosts(paginationArgs: PaginationArgs, filter: PostFilter): Promise<PaginatedPost> {
const query = await this.postRepository
.createQueryBuilder()
.select();
// todo... you can apply filters here to the query as where clauses
return paginate(query, paginationArgs);
}
}
@OsoianMarcel
Copy link

OsoianMarcel commented Jun 30, 2022

Thanks for the example but this code works fine only when the sorting is ASC, otherwise hasNextPage and hasPreviousPage displays wrong results.

@jefferyajamin
Copy link

jefferyajamin commented Jul 6, 2022

Is true, when filter by DESC the displays doesn't work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment