Afonso Barracha
Posted on June 29, 2022
Introduction
There are two common cursor pagination methods for GraphQL, the "Pagination and Edges" pagination as seen here and the "Relay Style" pagination described here.
Overview
To be able to paginate objects, we need to create:
- Both interfaces and generic (abstract objects) for pagination;
- The base for the pagination logic;
- Enums for the unique cursor, and SQL order by;
- A generic implementation for Mikro-ORM query builder.
I like to organize all these steps in the same global module which I normally call "common".
Architecture
Common Module
The common module itself will be very simple, with just its service as a dependency:
import { Global, Module } from '@nestjs/common';
import { CommonService } from './common.service';
@Global()
@Module({
providers: [CommonService],
exports: [CommonService],
})
export class CommonModule {}
Interfaces
Firstly inside the common module folder, create a interfaces folder were you will add a file called "paginated.interface.ts", which will contain five interfaces:
- Edge Interface;
- Basic Page Info Interface;
- Relay Page Info Interface;
- Basic Paginated Interface;
- Relay Paginated Interface.
Edge Interface
It represents the edge of both cursor pagination methods:
export interface IEdge<T> {
cursor: string;
node: T;
}
Page Info Interface
Since the basic is a one way and the relay a two way pagination the "Relay Style" extends the "Pagination and Edges" page info.
export interface IBasicPageInfo {
endCursor: string;
hasNextPage: boolean;
}
export interface IRelayPageInfo extends IBasicPageInfo {
startCursor: string;
hasPreviousPage: boolean;
}
Paginated Interface
The total count in the basic paginated is the current distinct count of the cursor parameter. While we have two count for relay, previousCount, the count of the previous page, and currentCount, the same as total count.
export interface IBasicPaginated<T> {
totalCount: number;
edges: IEdge<T>[];
pageInfo: IBasicPageInfo;
}
export interface IRelayPaginated<T> {
previousCount: number;
currentCount: number;
edges: IEdge<T>[];
pageInfo: IRelayPageInfo;
}
Putting it all together, your "paginated.interface.ts" should look like this:
export interface IEdge<T> {
cursor: string;
node: T;
}
export interface IBasicPageInfo {
endCursor: string;
hasNextPage: boolean;
}
export interface IRelayPageInfo extends IBasicPageInfo {
startCursor: string;
hasPreviousPage: boolean;
}
export interface IBasicPaginated<T> {
totalCount: number;
edges: IEdge<T>[];
pageInfo: IBasicPageInfo;
}
export interface IRelayPaginated<T> {
previousCount: number;
currentCount: number;
edges: IEdge<T>[];
pageInfo: IRelayPageInfo;
}
Generics
After setting up the interfaces we need generics to be able to create Paginated Objects, on your common module folder create a directory called "gql-types", where all common GraphQL Object Types will be stored.
Edge Generic
In a file called "edge.type.ts" create the following generic:
import { Type } from '@nestjs/common';
import { Field, ObjectType } from '@nestjs/graphql';
import { IEdge } from '../interfaces/paginated.interface';
export function Edge<T>(classRef: Type<T>): Type<IEdge<T>> {
@ObjectType({ isAbstract: true })
abstract class EdgeType implements IEdge<T> {
@Field(() => String)
public cursor: string;
@Field(() => classRef)
public node: T;
}
return EdgeType as Type<IEdge<T>>;
}
Basic Paginated Generic
In a file called "basic-paginated.type.ts" create the following generic:
import { Type } from '@nestjs/common';
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Edge } from './edge.type';
import { IBasicPageInfo, IBasicPaginated } from '../interfaces/paginated.interface';
@ObjectType('BasicPageInfo')
abstract class PageInfoType implements IBasicPageInfo {
@Field(() => String)
public endCursor: string;
@Field(() => Boolean)
public hasNextPage: boolean;
}
export function BasicPaginated<T>(classRef: Type<T>): Type<IBasicPaginated<T>> {
@ObjectType(`${classRef.name}BasicEdge`)
abstract class EdgeType extends Edge(classRef) {}
@ObjectType({ isAbstract: true })
abstract class PaginatedType implements IBasicPaginated<T> {
@Field(() => Int)
public totalCount: number;
@Field(() => [EdgeType])
public edges: EdgeType[];
@Field(() => PageInfoType)
public pageInfo: PageInfoType;
}
return PaginatedType as Type<IBasicPaginated<T>>;
}
Relay Paginated Generic
In a file called "relay-paginated.type.ts" create the following generic:
import { Type } from '@nestjs/common';
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Edge } from './edge.type';
import { IRelayPageInfo, IRelayPaginated } from '../interfaces/paginated.interface';
@ObjectType('RelayPageInfo')
abstract class PageInfoType implements IRelayPageInfo {
@Field(() => String)
public startCursor: string;
@Field(() => String)
public endCursor: string;
@Field(() => Boolean)
public hasNextPage: boolean;
@Field(() => Boolean)
public hasPreviousPage: boolean;
}
export function RelayPaginated<T>(classRef: Type<T>): Type<IRelayPaginated<T>> {
@ObjectType(`${classRef.name}RelayEdge`)
abstract class EdgeType extends Edge(classRef) {}
@ObjectType({ isAbstract: true })
abstract class RelayPaginatedType implements IRelayPaginated<T> {
@Field(() => Int)
public previousCount: number;
@Field(() => Int)
public currentCount: number;
@Field(() => [EdgeType])
public edges: EdgeType[];
@Field(() => PageInfoType)
public pageInfo: PageInfoType;
}
return PaginatedType as Type<IRelayPaginated<T>>;
}
Enums
There are are two enums that are necessary for filtering cursor paginated objects:
- The Query Cursor Enum that represents the type of cursor, normally a alphabetical or chronological unique cursor;
- The Query Order Enum that represents the order, which can be either ascending or descending.
Start by creating the "enum" directory on the common module folder.
Query Cursor Enum
The base interface represents the base entity that all your main entities will extend from. In this particular example the ID will be an auto-incremented integer that will represent the chronological cursor, while the slug is a unique varchar index that will represent the alphabetical cursor.
import { registerEnumType } from '@nestjs/graphql';
import { IBase } from '../interfaces/base.interface';
import { IUser } from '../../users/interfaces/user.interface';
export enum QueryCursorEnum {
DATE = 'DATE',
ALPHA = 'ALPHA',
}
registerEnumType(QueryCursorEnum, {
name: 'QueryCursor',
});
export const getQueryCursor = (cursor: QueryCursorEnum): keyof IBase =>
cursor === QueryCursorEnum.ALPHA ? 'id' : 'slug';
Query Order Enum
Is a smaller version of the Mikro-ORM Order Enum. I normally save the helper functions inside the enums, but you are free to move them into their own file.
import { registerEnumType } from '@nestjs/graphql';
export type tOrderEnum = '$gt' | '$lt';
export type tOppositeOrder = '$gte' | '$lte';
export enum QueryOrderEnum {
ASC = 'ASC',
DESC = 'DESC',
}
export const getQueryOrder = (order: QueryOrderEnum): tOrderEnum =>
order === QueryOrderEnum.ASC ? '$gt' : '$lt';
export const getOppositeOrder = (order: QueryOrderEnum): tOppositeOrder =>
order === QueryOrderEnum.ASC ? '$lte' : '$gte';
registerEnumType(QueryOrderEnum, {
name: 'QueryOrder',
});
The get opposite order is necessary to get the previous count in the "Relay Style" pagination.
Common Service
In the common service we will have all the logic necessary for paginating objects. This logic will be divided in various methods:
- Encoding and Decoding the Cursor to base 64;
- Edge creation;
- Raw implementation of the basic and relay cursor pagination;
- Query Builder implementation of the basic and relay cursor pagination.
Encoding and Decoding the Cursor
For these methods we can use the NodeJS buffer object.
Since the enconding function is private I will make it static:
import { Injectable } from '@nestjs/common';
@Injectable()
export class CommonService {
/**
* Encode Cursor
*
* Takes a date, string or integer and returns the base 64
* representation of it
*/
private static 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 Buffer.from(str, 'utf-8').toString('base64');
}
// ...
}
Even though in the encoding method we didn't need to specify the type of cursor, in the decoding method we will have to:
@Injectable()
export class CommonService {
// ...
/**
* Decode Cursor
*
* Takes a base64 cursor and returns the string or number value
*/
public decodeCursor(cursor: string, isNum = false): string | number {
const str = Buffer.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;
}
// ...
}
Edge Creation
The inner cursor is for entities paginated by a relation. As the encoder since it's a private method I'll make it static:
// ...
import { IEdge } from './interfaces/paginated.interface';
@Injectable()
export class CommonService {
// ...
/**
* Create Edge
*
* Takes an instance, the cursor key and a innerCursor,
* and generates a GraphQL edge
*/
private static createEdge<T>(
instance: T,
cursor: keyof T,
innerCursor?: string,
): IEdge<T> {
try {
return {
node: instance,
cursor: CommonService.encodeCursor(
innerCursor ? instance[cursor][innerCursor] : instance[cursor],
),
};
} catch (_) {
throw new InternalServerErrorException('The given cursor is invalid');
}
}
// ...
}
Raw Implementation
The basic and relay versions are not that different, and they take about the same parameters:
- The instances;
- The count values;
- The cursor;
- The amount of fetched instances;
- And optional inner cursor, for instances paginated by relations.
The basic version:
// ...
import { IEdge, IBasicPaginated } from './interfaces/paginated.interface';
@Injectable()
export class CommonService {
// ...
/**
* Basic Paginate
*
* Takes an entity array and returns the paginated type of that entity array
* It uses cursor pagination as recommended in https://graphql.org/learn/pagination/
*/
public basicPaginate<T>(
instances: T[],
totalCount: number,
cursor: keyof T,
first: number,
innerCursor?: string,
): IBasicPaginated<T> {
const pages: IBasicPaginated<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[len - 1].cursor;
pages.pageInfo.hasNextPage = totalCount > first;
}
return pages;
}
// ...
}
The relay version:
// ...
import { IEdge, IRelayPaginated } from './interfaces/paginated.interface';
@Injectable()
export class CommonService {
// ...
/**
* Relay Paginate
*
* Takes an entity array and returns the paginated type of that entity array
* It uses cursor pagination as recommended in https://relay.dev/graphql/connections.htm
*/
public relayPaginate<T>(
instances: T[],
currentCount: number,
previousCount: number,
cursor: keyof T,
first: number,
innerCursor?: string,
): IRelayPaginated<T> {
const pages: IRelayPaginated<T> = {
currentCount,
previousCount,
edges: [],
pageInfo: {
endCursor: '',
startCursor: '',
hasPreviousPage: false,
hasNextPage: false,
},
};
const len = instances.length;
if (len > 0) {
for (let i = 0; i < len; i++) {
pages.edges.push(
CommonService.createEdge(instances[i], cursor, innerCursor),
);
}
pages.pageInfo.startCursor = pages.edges[0].cursor;
pages.pageInfo.endCursor = pages.edges[len - 1].cursor;
pages.pageInfo.hasNextPage = currentCount > first;
pages.pageInfo.hasPreviousPage = previousCount > 0;
}
return pages;
}
// ...
}
QueryBuilder Implementation
Before implementing the methods themselves, we need some helper methods:
- Get Order By method to get the order input for the query builder;
- Get Filters method to get the where input for the query builder;
- Throw Internal Error method, a promise wrapper to throw internal errors.
Since both the first two are private methods with no common service dependencies I'll make them static.
Get Order By
//...
import { Dictionary, FilterQuery } from '@mikro-orm/core';
import { EntityRepository, QueryBuilder } from '@mikro-orm/postgresql';
import { QueryOrderEnum } from './enums/query-order.enum';
@Injectable()
export class CommonService {
// ...
/**
* Get Order By
*
* Makes the order by query for MikroORM orderBy method.
*/
private static getOrderBy<T>(
cursor: keyof T,
order: QueryOrderEnum,
innerCursor?: string,
): Record<string, QueryOrderEnum | Record<string, QueryOrderEnum>> {
return innerCursor
? {
[cursor]: {
[innerCursor]: order,
},
}
: {
[cursor]: order,
};
}
// ...
}
Get Filters
//...
import { Dictionary, FilterQuery } from '@mikro-orm/core';
import { EntityRepository, QueryBuilder } from '@mikro-orm/postgresql';
import {
QueryOrderEnum,
tOppositeOrder,
tOrderEnum,
} from './enums/query-order.enum';
@Injectable()
export class CommonService {
// ...
/**
* Get Filters
*
* Gets the where clause filter logic for the query builder pagination
*/
private static getFilters<T>(
cursor: keyof T,
decoded: string | number,
order: tOrderEnum | tOppositeOrder,
innerCursor?: string,
): FilterQuery<Dictionary<T>> {
return innerCursor
? {
[cursor]: {
[innerCursor]: {
[order]: decoded,
},
},
}
: {
[cursor]: {
[order]: decoded,
},
};
}
// ...
}
Throw Internal Error
import { Injectable, InternalServerErrorException } from '@nestjs/common';
@Injectable()
export class CommonService {
// ...
/**
* Throw Internal Error
*
* Function to abstract throwing internal server exception
*/
public async throwInternalError<T>(promise: Promise<T>): Promise<T> {
try {
return await promise;
} catch (error) {
throw new InternalServerErrorException(error);
}
}
// ...
}
In terms of parameters both version of the pagination methods will have the same ones.
- Alias: the alias of the query builder;
- Cursor: the unique cursor;
- First: the amount of instances to be fetched;
- Order: the query order enum;
- QB: the query builder;
- After: the optional parameter for where the query "should start" after;
- After Is Number: since the cursor can be a numeric value;
- Inner Cursor: for relations.
Basic Implementation
//...
import { Dictionary, FilterQuery } from '@mikro-orm/core';
import { EntityRepository, QueryBuilder } from '@mikro-orm/postgresql';
import {
getQueryOrder,
QueryOrderEnum,
tOppositeOrder,
tOrderEnum,
} from './enums/query-order.enum';
import { IEdge, IBasicPaginated } from './interfaces/paginated.interface';
@Injectable()
export class CommonService {
// ...
/**
* Basic Query Builder Pagination
*
* Takes a query builder and returns the entities paginated
*/
public async basicQueryBuilderPagination<T extends Object>(
alias: string,
cursor: keyof T,
first: number,
order: QueryOrderEnum,
qb: QueryBuilder<T>,
after?: string,
afterIsNum = false,
innerCursor?: string,
): Promise<IBasicPaginated<T>> {
if (after) {
const decoded = this.decodeCursor(after, afterIsNum);
const qbOrder = getQueryOrder(order);
qb.andWhere(
CommonService.getFilters(cursor, decoded, qbOrder, innerCursor),
);
}
const cqb = qb.clone()
const [count, entities]: [number, T[]] =
await this.throwInternalError(
Promise.all([
cqb.count(`${alias}.${String(cursor)}`, true),
qb
.select(`${alias}.*`)
.orderBy(this.getOrderBy(cursor, order, innerCursor))
.limit(first)
.getResult(),
]),
);
return this.basicPaginate(
entities,
count,
cursor,
first,
innerCursor,
);
}
// ...
}
Relay Implementation
//...
import { Dictionary, FilterQuery } from '@mikro-orm/core';
import { EntityRepository, QueryBuilder } from '@mikro-orm/postgresql';
import {
getOppositeOrder,
getQueryOrder,
QueryOrderEnum,
tOppositeOrder,
tOrderEnum,
} from './enums/query-order.enum';
import { IEdge, IBasicPaginated, IRelayPaginated } from './interfaces/paginated.interface';
@Injectable()
export class CommonService {
// ...
/**
* Relay Query Builder Pagination
*
* Takes a query builder and returns the entities paginated
*/
public async relayQueryBuilderPagination<T extends Object>(
alias: string,
cursor: keyof T,
first: number,
order: QueryOrderEnum,
qb: QueryBuilder<T>,
after?: string,
afterIsNum = false,
innerCursor?: string,
): Promise<IRelayPaginated<T>> {
const strCursor = String(cursor);
const aliasCursor = `${alias}.${strCursor}`;
let prevCount = 0;
if (after) {
const decoded = this.decodeCursor(after, afterIsNum);
const oppositeOd = getOppositeOrder(order);
const tempQb = qb.clone();
tempQb.andWhere(
CommonService.getFilters(cursor, decoded, oppositeOd, innerCursor),
);
prevCount = await tempQb.count(aliasCursor, true);
const normalOd = getQueryOrder(order);
qb.andWhere(
CommonService.getFilters(cursor, decoded, normalOd, innerCursor),
);
}
const cqb = qb.clone();
const [count, entities]: [number, T[]] = await this.throwInternalError(
Promise.all([
cqb.count(aliasCursor, true),
qb
.select(`${alias}.*`)
.orderBy(CommonService.getOrderBy(cursor, order, innerCursor))
.limit(first)
.getResult(),
]),
);
return this.relayPaginate(
entities,
count,
prevCount,
cursor,
first,
innerCursor,
);
}
// ...
}
Puting it All Together
Finally your common service should look something like this:
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Dictionary, FilterQuery } from '@mikro-orm/core';
import { EntityRepository, QueryBuilder } from '@mikro-orm/postgresql';
import {
getOppositeOrder,
getQueryOrder,
QueryOrderEnum,
tOppositeOrder,
tOrderEnum,
} from './enums/query-order.enum';
import { IEdge, IBasicPaginated, IRelayPaginated } from './interfaces/paginated.interface';
@Injectable()
export class CommonService {
/**
* Encode Cursor
*
* Takes a date, string or integer and returns the base 64
* representation of it
*/
private static 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 Buffer.from(str, 'utf-8').toString('base64');
}
/**
* Create Edge
*
* Takes an instance, the cursor key and a innerCursor,
* and generates a GraphQL edge
*/
private static createEdge<T>(
instance: T,
cursor: keyof T,
innerCursor?: string,
): IEdge<T> {
try {
return {
node: instance,
cursor: CommonService.encodeCursor(
innerCursor ? instance[cursor][innerCursor] : instance[cursor],
),
};
} catch (_) {
throw new InternalServerErrorException('The given cursor is invalid');
}
}
/**
* Get Order By
*
* Makes the order by query for MikroORM orderBy method.
*/
private static getOrderBy<T>(
cursor: keyof T,
order: QueryOrderEnum,
innerCursor?: string,
): Record<string, QueryOrderEnum | Record<string, QueryOrderEnum>> {
return innerCursor
? {
[cursor]: {
[innerCursor]: order,
},
}
: {
[cursor]: order,
};
}
/**
* Get Filters
*
* Gets the where clause filter logic for the query builder pagination
*/
private static getFilters<T>(
cursor: keyof T,
decoded: string | number,
order: tOrderEnum | tOppositeOrder,
innerCursor?: string,
): FilterQuery<Dictionary<T>> {
return innerCursor
? {
[cursor]: {
[innerCursor]: {
[order]: decoded,
},
},
}
: {
[cursor]: {
[order]: decoded,
},
};
}
/**
* Throw Internal Error
*
* Function to abstract throwing internal server exception
*/
public async throwInternalError<T>(promise: Promise<T>): Promise<T> {
try {
return await promise;
} catch (error) {
throw new InternalServerErrorException(error);
}
}
/**
* Decode Cursor
*
* Takes a base64 cursor and returns the string or number value
*/
public decodeCursor(cursor: string, isNum = false): string | number {
const str = Buffer.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;
}
/**
* Basic Paginate
*
* Takes an entity array and returns the paginated type of that entity array
* It uses cursor pagination as recommended in https://graphql.org/learn/pagination/
*/
public basicPaginate<T>(
instances: T[],
totalCount: number,
cursor: keyof T,
first: number,
innerCursor?: string,
): IBasicPaginated<T> {
const pages: IBasicPaginated<T> = {
totalCount,
edges: [],
pageInfo: {
endCursor: '',
hasNextPage: false,
},
};
const len = instances.length;
if (len > 0) {
for (let i = 0; i < len; i++) {
pages.edges.push(
CommonService.createEdge(instances[i], cursor, innerCursor),
);
}
pages.pageInfo.endCursor = pages.edges[len - 1].cursor;
pages.pageInfo.hasNextPage = totalCount > first;
}
return pages;
}
/**
* Relay Paginate
*
* Takes an entity array and returns the paginated type of that entity array
* It uses cursor pagination as recommended in https://relay.dev/graphql/connections.htm
*/
public relayPaginate<T>(
instances: T[],
currentCount: number,
previousCount: number,
cursor: keyof T,
first: number,
innerCursor?: string,
): IRelayPaginated<T> {
const pages: IRelayPaginated<T> = {
currentCount,
previousCount,
edges: [],
pageInfo: {
endCursor: '',
startCursor: '',
hasPreviousPage: false,
hasNextPage: false,
},
};
const len = instances.length;
if (len > 0) {
for (let i = 0; i < len; i++) {
pages.edges.push(
CommonService.createEdge(instances[i], cursor, innerCursor),
);
}
pages.pageInfo.startCursor = pages.edges[0].cursor;
pages.pageInfo.endCursor = pages.edges[len - 1].cursor;
pages.pageInfo.hasNextPage = currentCount > first;
pages.pageInfo.hasPreviousPage = previousCount > 0;
}
return pages;
}
/**
* Basic Query Builder Pagination
*
* Takes a query builder and returns the entities paginated
*/
public async basicQueryBuilderPagination<T extends Object>(
alias: string,
cursor: keyof T,
first: number,
order: QueryOrderEnum,
qb: QueryBuilder<T>,
after?: string,
afterIsNum = false,
innerCursor?: string,
): Promise<IBasicPaginated<T>> {
if (after) {
const decoded = this.decodeCursor(after, afterIsNum);
const qbOrder = getQueryOrder(order);
qb.andWhere(
CommonService.getFilters(cursor, decoded, qbOrder, innerCursor),
);
}
const cqb = qb.clone()
const [count, entities]: [number, T[]] =
await this.throwInternalError(
Promise.all([
cqb.count(`${alias}.${String(cursor)}`, true),
qb
.select(`${alias}.*`)
.orderBy(this.getOrderBy(cursor, order, innerCursor))
.limit(first)
.getResult(),
]),
);
return this.basicPaginate(
entities,
count,
cursor,
first,
innerCursor,
);
}
/**
* Relay Query Builder Pagination
*
* Takes a query builder and returns the entities paginated
*/
public async relayQueryBuilderPagination<T extends Object>(
alias: string,
cursor: keyof T,
first: number,
order: QueryOrderEnum,
qb: QueryBuilder<T>,
after?: string,
afterIsNum = false,
innerCursor?: string,
): Promise<IRelayPaginated<T>> {
const strCursor = String(cursor);
const aliasCursor = `${alias}.${strCursor}`;
let prevCount = 0;
if (after) {
const decoded = this.decodeCursor(after, afterIsNum);
const oppositeOd = getOppositeOrder(order);
const tempQb = qb.clone();
tempQb.andWhere(
CommonService.getFilters(cursor, decoded, oppositeOd, innerCursor),
);
prevCount = await tempQb.count(aliasCursor, true);
const normalOd = getQueryOrder(order);
qb.andWhere(
CommonService.getFilters(cursor, decoded, normalOd, innerCursor),
);
}
const cqb = qb.clone();
const [count, entities]: [number, T[]] = await this.throwInternalError(
Promise.all([
cqb.count(aliasCursor, true),
qb
.select(`${alias}.*`)
.orderBy(CommonService.getOrderBy(cursor, order, innerCursor))
.limit(first)
.getResult(),
]),
);
return this.relayPaginate(
entities,
count,
prevCount,
cursor,
first,
innerCursor,
);
}
}
Conclusion
With this implementation you'll be able to create paginated objects of your main object types and filter them on your resolvers.
Posted on June 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.