Skip to content

Instantly share code, notes, and snippets.

@killmenot
Forked from tumainimosha/page-info.ts
Created August 7, 2023 07:20
Show Gist options
  • Save killmenot/cd95c4ca8486edac9986f659d7dd2af5 to your computer and use it in GitHub Desktop.
Save killmenot/cd95c4ca8486edac9986f659d7dd2af5 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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment