Paginated Connection is a utility library for handling pagination in your applications. It simplifies the process of managing paginated data, making it easy to integrate into your projects. It has built for GraphQL, and it's fully compliant with GraphQL Cursor Connections Specification
Made with β€οΈ atΒ Β , join us in making a difference!
Pagination is essential for managing large datasets in a user-friendly manner. Paginated Connection provides a straightforward way to implement pagination logic in your applications, supporting both simple and complex use cases.
- Easy to set up and use
- Highly customizable
- Fully complaint with GraphQL Cursor Connections Specification
- Supports various pagination strategies
- Works seamlessly with different data sources
To install Paginated Connection:
npm install @treedom/paginated-connection
Here is a basic example to get you started with Paginated Connection:
import { paginatedConnection, PaginationInput } from '@treedom/paginated-connection'
// Define a simple node type
type Node = {
id: string;
};
// Define encode and decode functions
// Function to get cursor object from node
const getCursor = node => ({ after: node.id });
// encodeCursor should return a string
const encodeCursor = ({ node, getCursor }) => Buffer.from(JSON.stringify(getCursor())).toString('base64');
// decodeCursor should return an object
const decodeCursor = cursor => JSON.parse(Buffer.from(cursor, 'base64url').toString())
// Sample data loader
const dataLoader = async ({ cursor, first, encodeCursor, getEdge }) => {
// Fetch data based on cursor and first
const edges = fetchDataFromDataSource(cursor, first);
return {
edges: edges.map(node => getEdge(node, getCursor)),
hasNextPage: checkIfHasNextPage(),
};
};
// Sample count loader
const countLoader = async ({ cursor }) => {
return countDataFromDataSource(cursor);
};
const paginationInput: PaginationInput = { after: 'cursor123', first: 10 };
const paginationSafeLimit = 50;
const result = await paginatedConnection<Node>({
pagination: paginationInput,
paginationSafeLimit,
dataLoader,
encodeCursor,
decodeCursor,
countLoader,
});
console.log(result);
Using Paginated Connection with MySQL:
import { mysqlPaginatedConnection } from '@treedom/paginated-connection';
// Define a simple node type
type Node = {
id: string;
};
// Function to get cursor object from node
const getCursor = node => ({ after: node.id });
// Define MySQL specific data loader
const mysqlDataLoader = async ({ cursor, first, encodeCursor }) => {
// Fetch data from MySQL database
const edges = fetchDataFromMySQL(cursor, first);
return {
edges: edges.map(node => getEdge(node, getCursor)),
};
};
// Define MySQL specific count loader
const mysqlCountLoader = async ({ cursor }) => {
return countDataInMySQL(cursor);
};
const paginationInput = { after: 'cursor123', first: 10 };
const paginationSafeLimit = 50;
const result = await mysqlPaginatedConnection<Node>({
pagination: paginationInput,
paginationSafeLimit,
dataLoader: mysqlDataLoader,
countLoader: mysqlCountLoader,
});
console.log(result);
In the MySQL implementation, the +1
handling of data for the calculation of the hasNextPage
value is implicitly managed by the function execution. This means you don't need to handle it yourself. The hasNextPage
value is automatically calculated, so you should not return it in your data loader.
Using Paginated Connection with MongoDB:
import { mongoDbPaginatedConnection } from '@treedom/paginated-connection';
// Define a simple node type
type Node = {
id: string;
};
// Function to get cursor object from node
const getCursor = node => ({ after: node.id });
// Define MongoDB specific data loader
const mongoDbDataLoader = async ({ cursor, first, encodeCursor }) => {
// Fetch data from MongoDB
const edges = fetchDataFromMongoDB(cursor, first);
return {
edges: edges.map(node => getEdge(node, getCursor))
};
};
// Define MongoDB specific count loader
const mongoDbCountLoader = async ({ cursor }) => {
return countDataInMongoDB(cursor);
};
const paginationInput = { after: 'cursor123', first: 10 };
const paginationSafeLimit = 50;
const result = await mongoDbPaginatedConnection<Node>({
pagination: paginationInput,
paginationSafeLimit,
dataLoader: mongoDbDataLoader,
countLoader: mongoDbCountLoader,
});
console.log(result);
In the MongoDB implementation, the +1
handling of data for the calculation of the hasNextPage
value is implicitly managed by the function execution. This means you don't need to handle it yourself. The hasNextPage
value is automatically calculated, so you should not return it in your data loader.
Every paginatedConnection function returns an object of PaginatedConnectionReturnType
:
export type PaginatedConnectionReturnType<TNode> = Promise<{
totalCount: () => Promise<number>
pageInfo: {
endCursor: string
hasNextPage: boolean
}
edges: Array<{
node: TNode
cursor: string
}>
}>
where TNode
is the type of the node
loaded by dataLoader
function.
When executing dataloader
function, it provides getEdge
function, which is a shortcut to return an Edge
object. Object returned by getEdge
will contain both node
and cursor
values.
This function is very useful to avoid write boilerplate code to compose the Edge
object, specially for cursor. Under the hood, it executes the encodeCursor
function, providing cursor inside of return object Edge
.
const dataLoader = async ({ cursor, first, encodeCursor }) => {
const nodes = fetchDataFromDataSource(cursor, first);
return {
edges: nodes.map(node => getEdge(node, getCursor)),
hasNextPage: checkIfHasNextPage(),
};
};
Function getEdge
gets in input:
node
object, which should hasTNode
type;getCursor
function, which should returns an object of typeTCursor
.
When executing dataloader
function, it provides getEdges
function, which is a shortcut to return an Edges
array. Every item returned by getEdges will contain both node
and cursor
values.
This function is very useful when you have an array of loaded items, which every item is already typed as TNode
and ready to be used as a node inside Edge
.
const dataLoader = async ({ cursor, first, encodeCursor }) => {
const nodes = fetchDataFromDataSource(cursor, first); // nodes is an array of TNode object
return {
edges: getEdges(nodes, getCursor),
hasNextPage: checkIfHasNextPage(),
};
};
Function getEdges
gets in input:
nodes
array, which should hasArray<TNode>
type;getCursor
function, which should returns an object of typeTCursor
.
Under the hood, it executes the encodeCursor
function, in order to provide the cursor inside of Edge
.
If you need more customization of data, Edges
could be manually composed, returning an array of Edge
.
const dataLoader = async ({ cursor, first, encodeCursor }) => {
const edges = fetchDataFromDataSource(cursor, first);
return {
edges: edges.map(node => ({ node, cursor: encodeCursor({ node, getCursor }) })),
hasNextPage: checkIfHasNextPage(),
};
};
By default, the cursor type only includes an after
field, which is a string. This is simple and suitable for basic pagination scenarios.
{ after: string };
The default cursor is used when no specific cursor type is provided to paginatedConnection
(or mysqlPaginatedConnection
, mongoDbPaginatedConnection
, ecc...):
type Node = {
id: string;
};
const paginationInput = { after: 'cursor123', first: 10 };
// Return value should be an object containing `after` field only
const getCursor = (node): { after: string } => ({
after: node.id,
});
// Here we're not passing any custom cursor type to paginatedConnection, so it'll use the default type
const result = await paginatedConnection<Node>({
...
dataLoader,
...
});
For more complex scenarios, you can customize the cursor type to include additional fields, such as sorting information. The value of all cursor fields must be one of the following:
string
number
boolean
type CustomCursor = { after: string; sortField: string; sortOrder: 'asc' | 'desc', ranking: number, includeMetadata: boolean };
When using a custom cursor type, you need to type the paginatedConnection
(or mysqlPaginatedConnection
, mongoDbPaginatedConnection
, ecc...), providing cursor custom type:
import { paginatedConnection } from '@treedom/paginated-connection';
type Node = {
id: string;
sortField: string;
};
// Custom cursor type
type CustomCursor = { after: string; sortField: string; sortOrder: 'asc' | 'desc' };
// Return value should be an object of type `CustomCursor`
const getCursor = (node): CustomCursor => ({
after: node.id,
sortField: node.sortField,
sortOrder: 'asc',
});
// Sample data loader
const dataLoader = async ({ cursor, first, encodeCursor }) => {
const edges = fetchDataFromDataSource(cursor, first);
return {
edges: edges.map(node => getEdge(node, getCursor)),
hasNextPage: checkIfHasNextPage(),
};
};
// Provide CustomCursor type
const result = await paginatedConnection<Node, CustomCursor>({
...
dataLoader,
...
});
console.log(result);
paginatedConnection<TNode, TCursor>(props: PaginatedConnectionProps<TNode, TCursor>)
Handles pagination to offset-style ordering, returning Connection-style GraphQL result.
- props (
PaginatedConnectionProps
):pagination
(PaginationInput
): Pagination parameters.paginationSafeLimit
(number
): Safe limit for pagination.dataLoader
((props: DataloaderProps<TNode, TCursor>) => Promise<{ edges: { node: TNode; cursor: string }[]; -- hasNextPage: boolean }>
): Data loader function.encodeCursor
(EncodeCursor<TNode, TCursor>
): Function to encode cursor, it should return astring.
decodeCursor
((cursor: string) => TCursor
): Function to decode cursor.countLoader
((props: CountLoaderProps<TCursor>) => Promise<number>
): Count loader function.
mysqlPaginatedConnection<TNode, TCursor>(props: MysqlPaginatedConnectionProps<TNode, TCursor>)
Handles pagination for MySQL databases, extending the basic paginatedConnection
.
- props (
MysqlPaginatedConnectionProps
):dataLoader
((props: DataloaderProps<TNode, TCursor>) => Promise<{ edges: { node: TNode; cursor: string }[]; }>
): MySQL data loader.countLoader
((props: CountLoaderProps<TCursor>) => Promise<number>
): MySQL count loader.pagination
(PaginationInput
): Pagination parameters.paginationSafeLimit
(number
): Safe limit for pagination.
In the MySQL implementation, the +1
handling of data for the calculation of the hasNextPage
value is implicitly managed by the function execution. This means you don't need to handle it yourself. The hasNextPage
value is automatically calculated, so you should not return this value in your data loader.
mongoDbPaginatedConnection<TNode, TCursor>(props: MongoDbPaginatedConnectionProps<TNode, TCursor>)
Handles pagination for MongoDB databases, extending the basic paginatedConnection.
- props (
MongoDbPaginatedConnectionProps
):dataLoader
((props: DataloaderProps<TNode, TCursor>) => Promise<{ edges: { node: TNode; cursor: string }[]; }>
): MongoDB data loader.countLoader
((props: CountLoaderProps<TCursor>) => Promise<number>
): MongoDB count loader.pagination
(PaginationInput
): Pagination parameters.paginationSafeLimit
(number
): Safe limit for pagination.
In the MongoDB implementation, the +1
handling of data for the calculation of the hasNextPage
value is implicitly managed by the function execution. This means you don't need to handle it yourself. The hasNextPage
value is automatically calculated, so you should not return this value in your data loader.
We invite all developers who use Treedom's open-source code to support our mission of sustainability by planting a tree with us. By contributing to reforestation efforts, you help create a healthier planet and give back to the environment. Visit our Treedom Open Source Forest to plant your tree today and join our community of eco-conscious developers.
Additionally, you can integrate the Treedom GitHub badge into your repository to showcase the number of trees in your Treedom forest and encourage others to plant new ones. Check out our integration guide to get started.
Together, we can make a lasting impact! ππ
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.
This project is licensed under the MIT License.