or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.mdlist-mocking.mdmock-server.mdmock-store.mdpagination.mdschema-mocking.md
tile.json

pagination.mddocs/

Pagination

Relay-style pagination utilities for implementing cursor-based pagination in GraphQL mock resolvers. Provides standardized pagination patterns with realistic mock data for connection fields.

Capabilities

Relay Style Pagination Mock

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')
      })
    }
  }
});

Pagination Parameter Types

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;
};

Pagination Configuration Options

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[];

Usage Patterns

Basic Pagination Setup

// 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}` }
          }));
        }
      })
    }
  }
});

Custom Node Filtering and Sorting

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;
        }
      })
    }
  }
});

Custom Cursor Generation

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');
        }
      })
    }
  }
});

Multiple Connection Fields

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}` }
          }));
        }
      })
    }
  }
});

Testing Pagination

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();
  });
});

Integration with MockStore

// 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}` }
          }));
        }
      })
    }
  }
});