Relay-style pagination utilities for implementing cursor-based pagination in GraphQL mock resolvers. Provides standardized pagination patterns with realistic mock data for connection fields.
Creates Relay-style pagination resolver for connection fields with cursor-based navigation.
/**
* Create a Relay-style pagination resolver for connection fields
* @param store - Mock store instance for data consistency
* @param options - Configuration options for pagination behavior
* @returns Field resolver implementing Relay pagination pattern
*/
const relayStylePaginationMock: <
TContext,
TArgs extends RelayPaginationParams = RelayPaginationParams
>(
store: IMockStore,
options?: RelayStylePaginationMockOptions<TContext, TArgs>
) => IFieldResolver<Ref, TContext, TArgs, any>;Usage Examples:
import {
addMocksToSchema,
createMockStore,
relayStylePaginationMock,
MockList
} from "@graphql-tools/mock";
const schema = buildSchema(`
type User {
id: ID!
name: String!
posts(first: Int, after: String, last: Int, before: String): PostConnection!
}
type Post {
id: ID!
title: String!
content: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
user(id: ID!): User
}
`);
const store = createMockStore({
schema,
mocks: {
User: () => ({
id: () => `user_${Math.random().toString(36).substr(2, 9)}`,
name: () => ['Alice', 'Bob', 'Charlie'][Math.floor(Math.random() * 3)]
}),
Post: () => ({
id: () => `post_${Math.random().toString(36).substr(2, 9)}`,
title: () => 'Sample Post Title',
content: () => 'Sample post content...'
})
}
});
const mockedSchema = addMocksToSchema({
schema,
store,
resolvers: {
User: {
posts: relayStylePaginationMock(store, {
// Generate 10-50 total posts for each user
allNodesFn: (parent, args, context, info) => {
const totalPosts = Math.floor(Math.random() * 40) + 10;
return Array.from({ length: totalPosts }, (_, index) => ({
$ref: { typeName: 'Post', key: `${parent.$ref.key}_post_${index}` }
}));
},
// Custom cursor generation
cursorFn: (nodeRef) => Buffer.from(nodeRef.$ref.key).toString('base64')
})
}
}
});Standard Relay pagination parameter types.
/**
* Standard Relay pagination parameters
*/
type RelayPaginationParams = {
/** Number of items to fetch from the beginning */
first?: number;
/** Cursor to start fetching after */
after?: string;
/** Number of items to fetch from the end */
last?: number;
/** Cursor to start fetching before */
before?: string;
};
/**
* Relay PageInfo structure for pagination metadata
*/
type RelayPageInfo = {
/** Whether there are more items when paginating forward */
hasPreviousPage: boolean;
/** Whether there are more items when paginating backward */
hasNextPage: boolean;
/** Cursor of the first item in current page */
startCursor: string;
/** Cursor of the last item in current page */
endCursor: string;
};Configuration options for customizing pagination behavior.
/**
* Configuration options for Relay-style pagination mock
*/
type RelayStylePaginationMockOptions<TContext, TArgs extends RelayPaginationParams> = {
/** Function to apply additional filtering/sorting to nodes */
applyOnNodes?: (nodeRefs: Ref[], args: TArgs) => Ref[];
/** Function to get all available nodes for pagination */
allNodesFn?: AllNodesFn<TContext, TArgs>;
/** Custom cursor generation function */
cursorFn?: (nodeRef: Ref) => string;
};
/**
* Function type for getting all nodes for pagination
*/
type AllNodesFn<TContext, TArgs extends RelayPaginationParams> = (
parent: Ref,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => Ref[];// Simple pagination with default behavior
const mockedSchema = addMocksToSchema({
schema,
store,
resolvers: {
User: {
// Basic posts pagination
posts: relayStylePaginationMock(store),
// Followers pagination with custom node generation
followers: relayStylePaginationMock(store, {
allNodesFn: (parent) => {
const followerCount = Math.floor(Math.random() * 100) + 10;
return Array.from({ length: followerCount }, (_, index) => ({
$ref: { typeName: 'User', key: `follower_${parent.$ref.key}_${index}` }
}));
}
})
}
}
});const mockedSchema = addMocksToSchema({
schema,
store,
resolvers: {
User: {
posts: relayStylePaginationMock(store, {
allNodesFn: (parent) => {
// Generate posts with timestamps
const postCount = Math.floor(Math.random() * 50) + 5;
return Array.from({ length: postCount }, (_, index) => ({
$ref: { typeName: 'Post', key: `${parent.$ref.key}_post_${index}` }
}));
},
applyOnNodes: (nodeRefs, args) => {
// Apply filtering and sorting
let filteredNodes = nodeRefs;
// Sort by creation date (newest first)
filteredNodes = filteredNodes.sort((a, b) => {
const aTime = parseInt(a.$ref.key.split('_').pop() || '0');
const bTime = parseInt(b.$ref.key.split('_').pop() || '0');
return bTime - aTime;
});
// Apply any additional filtering based on args
if (args.status) {
filteredNodes = filteredNodes.filter(node => {
// Custom filtering logic based on args
return true; // Simplified
});
}
return filteredNodes;
}
})
}
}
});const mockedSchema = addMocksToSchema({
schema,
store,
resolvers: {
User: {
posts: relayStylePaginationMock(store, {
allNodesFn: (parent) => {
return Array.from({ length: 25 }, (_, index) => ({
$ref: { typeName: 'Post', key: `${parent.$ref.key}_post_${index}` }
}));
},
// Custom cursor encoding with timestamp
cursorFn: (nodeRef) => {
const postIndex = nodeRef.$ref.key.split('_').pop();
const timestamp = Date.now() - (parseInt(postIndex || '0') * 86400000); // Days ago
return Buffer.from(`${nodeRef.$ref.key}:${timestamp}`).toString('base64');
}
})
}
}
});const mockedSchema = addMocksToSchema({
schema,
store,
resolvers: {
User: {
// Posts connection
posts: relayStylePaginationMock(store, {
allNodesFn: (parent) => Array.from({ length: 30 }, (_, i) => ({
$ref: { typeName: 'Post', key: `${parent.$ref.key}_post_${i}` }
}))
}),
// Comments connection
comments: relayStylePaginationMock(store, {
allNodesFn: (parent) => Array.from({ length: 75 }, (_, i) => ({
$ref: { typeName: 'Comment', key: `${parent.$ref.key}_comment_${i}` }
}))
}),
// Following connection
following: relayStylePaginationMock(store, {
allNodesFn: (parent) => Array.from({ length: 15 }, (_, i) => ({
$ref: { typeName: 'User', key: `following_${parent.$ref.key}_${i}` }
}))
})
},
Post: {
// Post comments
comments: relayStylePaginationMock(store, {
allNodesFn: (parent) => {
const commentCount = Math.floor(Math.random() * 20);
return Array.from({ length: commentCount }, (_, i) => ({
$ref: { typeName: 'Comment', key: `${parent.$ref.key}_comment_${i}` }
}));
}
})
}
}
});import { execute } from 'graphql';
describe('Pagination tests', () => {
test('should handle first/after pagination', async () => {
const query = `
query GetUserPosts($first: Int!, $after: String) {
user(id: "1") {
posts(first: $first, after: $after) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
`;
const result = await execute({
schema: mockedSchema,
document: parse(query),
variableValues: { first: 5 }
});
expect(result.data?.user?.posts?.edges).toHaveLength(5);
expect(result.data?.user?.posts?.pageInfo?.hasNextPage).toBeDefined();
expect(result.data?.user?.posts?.pageInfo?.startCursor).toBeDefined();
expect(result.data?.user?.posts?.pageInfo?.endCursor).toBeDefined();
});
test('should handle last/before pagination', async () => {
const query = `
query GetUserPosts($last: Int!, $before: String) {
user(id: "1") {
posts(last: $last, before: $before) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
`;
const result = await execute({
schema: mockedSchema,
document: parse(query),
variableValues: { last: 3 }
});
expect(result.data?.user?.posts?.edges).toHaveLength(3);
expect(result.data?.user?.posts?.pageInfo?.hasPreviousPage).toBeDefined();
});
});// Pre-populate store with pagination data
const store = createMockStore({ schema });
// Set up user with known posts
store.set('User', 'user_1', {
name: 'Alice',
email: 'alice@example.com'
});
// Create specific posts for testing
Array.from({ length: 20 }, (_, index) => {
store.set('Post', `user_1_post_${index}`, {
title: `Post ${index + 1}`,
content: `Content for post ${index + 1}`,
author: { $ref: { typeName: 'User', key: 'user_1' } }
});
});
// Use pagination with pre-populated data
const mockedSchema = addMocksToSchema({
schema,
store,
resolvers: {
User: {
posts: relayStylePaginationMock(store, {
allNodesFn: (parent) => {
// Return references to the pre-populated posts
return Array.from({ length: 20 }, (_, i) => ({
$ref: { typeName: 'Post', key: `${parent.$ref.key}_post_${i}` }
}));
}
})
}
}
});