CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-graphql-tools--mock

GraphQL schema mocking utilities with stateful stores and realistic test data generation

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

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

docs

index.md

list-mocking.md

mock-server.md

mock-store.md

pagination.md

schema-mocking.md

tile.json