Skip to content

Instantly share code, notes, and snippets.

@tumainimosha
Last active September 18, 2024 08:18
Show Gist options
  • Save tumainimosha/6652deb0aea172f7f2c4b2077c72d16c to your computer and use it in GitHub Desktop.
Save tumainimosha/6652deb0aea172f7f2c4b2077c72d16c to your computer and use it in GitHub Desktop.
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

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

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

@aoxiang78
Copy link

Reverse order requires the use of LessThan
DESC
.orWhere({ id : LessThan('356731416110829568')}).take(10)

@kezoo
Copy link

kezoo commented Aug 29, 2023

Have a look on this pager module which has taken care of both order cases.

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