Skip to content

Instantly share code, notes, and snippets.

@tugascript
Last active May 3, 2023 09:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tugascript/7d0027a83bccfd991e94ddaed699a83e to your computer and use it in GitHub Desktop.
Save tugascript/7d0027a83bccfd991e94ddaed699a83e to your computer and use it in GitHub Desktop.
A Generic Paginated Type for GraphQL in NestJS code first approach
export interface IEdge<T> {
cursor: string;
node: T;
}
interface IPageInfo {
endCursor: string;
hasNextPage: boolean;
}
export interface IPaginated<T> {
totalCount: number;
edges: IEdge<T>[];
pageInfo: IPageInfo;
}
import { Type } from '@nestjs/common';
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType('PageInfo')
abstract class PageInfoType {
@Field(() => String)
public endCursor: string;
@Field(() => Boolean)
public hasNextPage: boolean;
}
export function Paginated<T>(classRef: Type<T>): any {
@ObjectType(`${classRef.name}Edge`)
abstract class EdgeType {
@Field(() => String)
cursor: string;
@Field(() => classRef)
node: T;
}
@ObjectType({ isAbstract: true })
abstract class PaginatedType {
@Field(() => Int)
public totalCount: number;
@Field(() => [EdgeType])
public edges: EdgeType[];
@Field(() => PageInfoType)
public pageInfo: PageInfoType;
}
return PaginatedType;
}
import { ArgsType, Field, Int } from "@nestjs/graphql";
import { IsBase64, IsInt, IsString, Max, Min } from "class-validator";
@ArgsType()
export class PaginationDto {
@Field(() => String, { nullable: true })
@IsString()
@IsBase64()
public after?: string;
@Field(() => Int)
@IsInt()
@Min(1)
@Max(50)
public first = 10;
}
import {
BadRequestException,
Injectable,
InternalServerErrorException,
} from "@nestjs/common";
@Injectable()
export class PaginationService {
//-------------------- Cursor Pagination --------------------
private readonly buff = Buffer;
/**
* Paginate
*
* Takes an entity array and returns the paginated type of that entity array
* It uses cursor pagination as recomended in https://graphql.org/learn/pagination/
*/
public paginate<T>(
instances: T[],
totalCount: number,
cursor: keyof T,
first: number,
innerCursor?: string
): IPaginated<T> {
const pages: IPaginated<T> = {
totalCount,
edges: [],
pageInfo: {
endCursor: "",
hasNextPage: false,
},
};
const len = instances.length;
if (len > 0) {
for (let i = 0; i < len; i++) {
pages.edges.push(this.createEdge(instances[i], cursor, innerCursor));
}
pages.pageInfo.endCursor = pages.edges[pages.edges.length - 1].cursor;
pages.pageInfo.hasNextPage = totalCount > first;
}
return pages;
}
private createEdge<T>(
instance: T,
cursor: keyof T,
innerCursor?: string
): IEdge<T> {
try {
return {
node: instance,
cursor: this.encodeCursor(
innerCursor ? instance[cursor][innerCursor] : instance[cursor]
),
};
} catch (_) {
throw new InternalServerErrorException("The given cursor is invalid");
}
}
private encodeCursor(val: Date | string | number): string {
let str: string;
if (val instanceof Date) {
str = val.getTime().toString();
} else if (typeof val === "number" || typeof val === "bigint") {
str = val.toString();
} else {
str = val;
}
return this.buff.from(str, "utf-8").toString("base64");
}
/**
* Decode Cursor
*
* Takes a base64 cursor and returns the string or number value
*/
public decodeCursor(cursor: string, isNum = false): string | number {
const str = this.buff.from(cursor, "base64").toString("utf-8");
if (isNum) {
const num = parseInt(str, 10);
if (isNaN(num))
throw new BadRequestException(
"Cursor does not reference a valid number"
);
return num;
}
return str;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment