Last active
August 30, 2024 05:11
-
-
Save tugascript/7d0027a83bccfd991e94ddaed699a83e to your computer and use it in GitHub Desktop.
A Generic Paginated Type for GraphQL in NestJS code first approach
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
I haven gotten that problem @simplenotezy can you send a link to the repo to try to replicate? Maybe remove the buffer constructor, this.buff
in hindsight was me thinking with a Rust brain
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@tugascript I've used a similar setup in a other nestjs project, but when using this is a new one, I am getting:
Unable to resolve signature of property decorator when called as an expression. Argument of type 'undefined' is not assignable to parameter of type 'Object'.ts(1240)
On the @field decorators. Any idea why?