graphql-resolvers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GraphQL Resolvers

GraphQL 解析器

Apply resolver implementation patterns to create efficient, maintainable GraphQL servers. This skill covers resolver function signatures, execution chains, context management, DataLoader patterns, async handling, authentication, and testing strategies.
应用解析器实现模式来创建高效、可维护的GraphQL服务器。本技能涵盖了解析器函数签名、执行链、上下文管理、DataLoader模式、异步处理、认证以及测试策略。

Resolver Function Signature

解析器函数签名

Every resolver function receives four arguments: parent, args, context, and info. Understanding these arguments is fundamental to writing effective resolvers.
typescript
type ResolverFn = (
  parent: any,
  args: any,
  context: any,
  info: GraphQLResolveInfo
) => any;

const resolvers = {
  Query: {
    // parent: root value (usually undefined for Query)
    // args: arguments passed to the query
    // context: shared context object
    // info: execution information
    user: async (parent, args, context, info) => {
      const { id } = args;
      const { dataSources, user } = context;

      // Use context to access data sources and auth info
      return dataSources.userAPI.getUserById(id);
    },

    posts: async (parent, args, context, info) => {
      const { limit, offset } = args;

      // Access requested fields from info
      const fields = info.fieldNodes[0].selectionSet.selections
        .map(s => s.name.value);

      return context.dataSources.postAPI.getPosts({
        limit,
        offset,
        fields
      });
    }
  }
};
每个解析器函数都会接收四个参数:parent、args、context和info。理解这些参数是编写高效解析器的基础。
typescript
type ResolverFn = (
  parent: any,
  args: any,
  context: any,
  info: GraphQLResolveInfo
) => any;

const resolvers = {
  Query: {
    // parent: 根值(Query通常为undefined)
    // args: 传递给查询的参数
    // context: 共享上下文对象
    // info: 执行信息
    user: async (parent, args, context, info) => {
      const { id } = args;
      const { dataSources, user } = context;

      // 使用上下文访问数据源和认证信息
      return dataSources.userAPI.getUserById(id);
    },

    posts: async (parent, args, context, info) => {
      const { limit, offset } = args;

      // 从info中获取请求的字段
      const fields = info.fieldNodes[0].selectionSet.selections
        .map(s => s.name.value);

      return context.dataSources.postAPI.getPosts({
        limit,
        offset,
        fields
      });
    }
  }
};

Field Resolvers

字段解析器

Field resolvers define how to resolve individual fields on a type. The parent argument contains the resolved parent object.
typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    }
  },

  User: {
    // Field resolver for computed field
    fullName: (parent) => {
      return `${parent.firstName} ${parent.lastName}`;
    },

    // Field resolver for related data
    posts: async (parent, args, { dataSources }) => {
      // parent.id available from parent User object
      return dataSources.postAPI.getPostsByAuthor(parent.id);
    },

    // Field resolver with arguments
    friends: async (parent, { limit }, { dataSources }) => {
      return dataSources.userAPI.getFriends(parent.id, limit);
    },

    // Async computed field
    postCount: async (parent, _, { dataSources }) => {
      return dataSources.postAPI.countByAuthor(parent.id);
    }
  },

  Post: {
    author: async (parent, _, { dataSources }) => {
      // parent.authorId from parent Post object
      return dataSources.userAPI.getUserById(parent.authorId);
    },

    comments: async (parent, _, { dataSources }) => {
      return dataSources.commentAPI.getByPostId(parent.id);
    }
  }
};
字段解析器定义了如何解析类型上的单个字段。parent参数包含已解析的父对象。
typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    }
  },

  User: {
    // 计算字段的解析器
    fullName: (parent) => {
      return `${parent.firstName} ${parent.lastName}`;
    },

    // 关联数据的字段解析器
    posts: async (parent, args, { dataSources }) => {
      // 从父User对象中获取parent.id
      return dataSources.postAPI.getPostsByAuthor(parent.id);
    },

    // 带参数的字段解析器
    friends: async (parent, { limit }, { dataSources }) => {
      return dataSources.userAPI.getFriends(parent.id, limit);
    },

    // 异步计算字段
    postCount: async (parent, _, { dataSources }) => {
      return dataSources.postAPI.countByAuthor(parent.id);
    }
  },

  Post: {
    author: async (parent, _, { dataSources }) => {
      // 从父Post对象中获取parent.authorId
      return dataSources.userAPI.getUserById(parent.authorId);
    },

    comments: async (parent, _, { dataSources }) => {
      return dataSources.commentAPI.getByPostId(parent.id);
    }
  }
};

Context Object Patterns

上下文对象模式

The context object is shared across all resolvers in a single request. Use it for authentication, data sources, and request-scoped data.
typescript
interface Context {
  user: User | null;
  dataSources: DataSources;
  db: Database;
  req: Request;
  loaders: Loaders;
}

// Context creation function
const createContext = async ({ req }): Promise<Context> => {
  // Extract and verify authentication token
  const token = req.headers.authorization?.replace('Bearer ', '');
  const user = token ? await verifyToken(token) : null;

  // Initialize data sources
  const dataSources = {
    userAPI: new UserAPI(),
    postAPI: new PostAPI(),
    commentAPI: new CommentAPI()
  };

  // Initialize DataLoaders
  const loaders = {
    userLoader: new DataLoader(ids => batchGetUsers(ids)),
    postLoader: new DataLoader(ids => batchGetPosts(ids))
  };

  return {
    user,
    dataSources,
    db: database,
    req,
    loaders
  };
};

// Using context in resolvers
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new Error('Not authenticated');
      }
      return user;
    },

    post: async (_, { id }, { loaders }) => {
      return loaders.postLoader.load(id);
    }
  }
};
上下文对象在单个请求的所有解析器之间共享。可将其用于认证、数据源和请求范围的数据存储。
typescript
interface Context {
  user: User | null;
  dataSources: DataSources;
  db: Database;
  req: Request;
  loaders: Loaders;
}

// 上下文创建函数
const createContext = async ({ req }): Promise<Context> => {
  // 提取并验证认证令牌
  const token = req.headers.authorization?.replace('Bearer ', '');
  const user = token ? await verifyToken(token) : null;

  // 初始化数据源
  const dataSources = {
    userAPI: new UserAPI(),
    postAPI: new PostAPI(),
    commentAPI: new CommentAPI()
  };

  // 初始化DataLoaders
  const loaders = {
    userLoader: new DataLoader(ids => batchGetUsers(ids)),
    postLoader: new DataLoader(ids => batchGetPosts(ids))
  };

  return {
    user,
    dataSources,
    db: database,
    req,
    loaders
  };
};

// 在解析器中使用上下文
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new Error('Not authenticated');
      }
      return user;
    },

    post: async (_, { id }, { loaders }) => {
      return loaders.postLoader.load(id);
    }
  }
};

Resolver Chains and Execution

解析器链与执行

Resolvers execute in a chain where parent resolvers complete before child resolvers begin. Understanding execution order is crucial for optimization.
typescript
const resolvers = {
  Query: {
    // Step 1: Root resolver executes
    user: async (_, { id }, { db }) => {
      console.log('1. Fetching user');
      return db.users.findById(id);
    }
  },

  User: {
    // Step 2: Field resolvers execute with parent data
    posts: async (parent, _, { db }) => {
      console.log('2. Fetching posts for user', parent.id);
      return db.posts.findByAuthor(parent.id);
    },

    profile: async (parent, _, { db }) => {
      console.log('2. Fetching profile for user', parent.id);
      return db.profiles.findByUserId(parent.id);
    }
  },

  Post: {
    // Step 3: Nested field resolvers execute
    comments: async (parent, _, { db }) => {
      console.log('3. Fetching comments for post', parent.id);
      return db.comments.findByPostId(parent.id);
    }
  }
};

// Query execution order:
// query {
//   user(id: "1") {        # 1. User resolver
//     posts {              # 2. Posts resolver
//       comments {         # 3. Comments resolver
//         text
//       }
//     }
//     profile {            # 2. Profile resolver (parallel)
//       bio
//     }
//   }
// }
解析器会按链执行,父解析器完成后子解析器才会开始。理解执行顺序对优化至关重要。
typescript
const resolvers = {
  Query: {
    // 步骤1:根解析器执行
    user: async (_, { id }, { db }) => {
      console.log('1. Fetching user');
      return db.users.findById(id);
    }
  },

  User: {
    // 步骤2:使用父数据执行字段解析器
    posts: async (parent, _, { db }) => {
      console.log('2. Fetching posts for user', parent.id);
      return db.posts.findByAuthor(parent.id);
    },

    profile: async (parent, _, { db }) => {
      console.log('2. Fetching profile for user', parent.id);
      return db.profiles.findByUserId(parent.id);
    }
  },

  Post: {
    // 步骤3:执行嵌套字段解析器
    comments: async (parent, _, { db }) => {
      console.log('3. Fetching comments for post', parent.id);
      return db.comments.findByPostId(parent.id);
    }
  }
};

// 查询执行顺序:
// query {
//   user(id: "1") {        # 1. 用户解析器
//     posts {              # 2. 帖子解析器
//       comments {         # 3. 评论解析器
//         text
//       }
//     }
//     profile {            # 2. 资料解析器(并行执行)
//       bio
//     }
//   }
// }

DataLoader Pattern for Batching

用于批处理的DataLoader模式

DataLoader solves the N+1 problem by batching multiple individual loads into a single batch request and caching results.
typescript
import DataLoader from 'dataloader';

// Batch function receives array of keys
// Must return array of results in same order
const batchGetUsers = async (userIds: string[]) => {
  console.log('Batch loading users:', userIds);

  // Single database query for all IDs
  const users = await db.users.findByIds(userIds);

  // Create map for O(1) lookup
  const userMap = new Map(users.map(u => [u.id, u]));

  // Return users in same order as input IDs
  return userIds.map(id => userMap.get(id) || null);
};

// Create loader in context
const userLoader = new DataLoader(batchGetUsers, {
  // Optional configuration
  cache: true,            // Cache results (default: true)
  maxBatchSize: 100,      // Maximum batch size
  batchScheduleFn: cb => setTimeout(cb, 10) // Custom scheduling
});

const resolvers = {
  Post: {
    author: async (parent, _, { loaders }) => {
      // These calls are automatically batched
      return loaders.userLoader.load(parent.authorId);
    }
  },

  Comment: {
    author: async (parent, _, { loaders }) => {
      // Added to same batch as Post.author
      return loaders.userLoader.load(parent.authorId);
    }
  }
};

// Example: Without DataLoader (N+1 problem)
// Query for 10 posts = 1 query for posts + 10 queries for authors
//
// With DataLoader:
// Query for 10 posts = 1 query for posts + 1 batched query for all
// authors
DataLoader通过将多个单独的加载操作合并为一个批量请求并缓存结果,解决了N+1查询问题。
typescript
import DataLoader from 'dataloader';

// 批量函数接收键数组
// 必须返回与输入顺序一致的结果数组
const batchGetUsers = async (userIds: string[]) => {
  console.log('Batch loading users:', userIds);

  // 为所有ID执行单次数据库查询
  const users = await db.users.findByIds(userIds);

  // 创建映射以实现O(1)查找
  const userMap = new Map(users.map(u => [u.id, u]));

  // 按输入ID的顺序返回用户
  return userIds.map(id => userMap.get(id) || null);
};

// 在上下文中创建加载器
const userLoader = new DataLoader(batchGetUsers, {
  // 可选配置
  cache: true,            // 缓存结果(默认值:true)
  maxBatchSize: 100,      // 最大批量大小
  batchScheduleFn: cb => setTimeout(cb, 10) // 自定义调度
});

const resolvers = {
  Post: {
    author: async (parent, _, { loaders }) => {
      // 这些调用会被自动批处理
      return loaders.userLoader.load(parent.authorId);
    }
  },

  Comment: {
    author: async (parent, _, { loaders }) => {
      // 加入与Post.author相同的批处理
      return loaders.userLoader.load(parent.authorId);
    }
  }
};

// 示例:不使用DataLoader(存在N+1问题)
// 查询10个帖子 = 1次帖子查询 + 10次作者查询
//
// 使用DataLoader:
// 查询10个帖子 = 1次帖子查询 + 1次所有作者的批量查询

Advanced DataLoader Patterns

高级DataLoader模式

typescript
// Composite key loader
interface CompositeKey {
  userId: string;
  type: string;
}

const batchGetUserData = async (keys: CompositeKey[]) => {
  // Group by type for efficient querying
  const byType = keys.reduce((acc, key) => {
    acc[key.type] = acc[key.type] || [];
    acc[key.type].push(key.userId);
    return acc;
  }, {});

  // Fetch data by type
  const results = await Promise.all(
    Object.entries(byType).map(([type, userIds]) =>
      fetchDataByType(type, userIds)
    )
  );

  // Map back to original key order
  return keys.map(key =>
    results.find(r => r.userId === key.userId && r.type === key.type)
  );
};

const dataLoader = new DataLoader(
  batchGetUserData,
  {
    cacheKeyFn: (key: CompositeKey) => `${key.userId}:${key.type}`
  }
);

// Prime the cache
await dataLoader.prime({ userId: '1', type: 'profile' }, userData);

// Clear specific key
dataLoader.clear({ userId: '1', type: 'profile' });

// Clear all cache
dataLoader.clearAll();
typescript
// 复合键加载器
interface CompositeKey {
  userId: string;
  type: string;
}

const batchGetUserData = async (keys: CompositeKey[]) => {
  // 按类型分组以实现高效查询
  const byType = keys.reduce((acc, key) => {
    acc[key.type] = acc[key.type] || [];
    acc[key.type].push(key.userId);
    return acc;
  }, {});

  // 按类型获取数据
  const results = await Promise.all(
    Object.entries(byType).map(([type, userIds]) =>
      fetchDataByType(type, userIds)
    )
  );

  // 映射回原始键的顺序
  return keys.map(key =>
    results.find(r => r.userId === key.userId && r.type === key.type)
  );
};

const dataLoader = new DataLoader(
  batchGetUserData,
  {
    cacheKeyFn: (key: CompositeKey) => `${key.userId}:${key.type}`
  }
);

// 预填充缓存
await dataLoader.prime({ userId: '1', type: 'profile' }, userData);

// 清除特定键
dataLoader.clear({ userId: '1', type: 'profile' });

// 清除所有缓存
dataLoader.clearAll();

Async Error Handling

异步错误处理

Proper error handling in resolvers ensures meaningful errors reach the client while protecting sensitive information.
typescript
import { GraphQLError } from 'graphql';
import { ApolloServerErrorCode } from '@apollo/server/errors';

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      try {
        const user = await dataSources.userAPI.getUserById(id);

        if (!user) {
          throw new GraphQLError('User not found', {
            extensions: {
              code: 'USER_NOT_FOUND',
              http: { status: 404 }
            }
          });
        }

        return user;
      } catch (error) {
        // Log full error for debugging
        console.error('Error fetching user:', error);

        // Throw safe error to client
        if (error instanceof GraphQLError) {
          throw error;
        }

        throw new GraphQLError('Failed to fetch user', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR'
          }
        });
      }
    }
  },

  Mutation: {
    createPost: async (_, { input }, { user, dataSources }) => {
      // Validation errors
      if (!input.title || input.title.length < 3) {
        throw new GraphQLError('Title must be at least 3 characters', {
          extensions: {
            code: 'BAD_USER_INPUT',
            argumentName: 'title'
          }
        });
      }

      // Authentication errors
      if (!user) {
        throw new GraphQLError('Must be authenticated', {
          extensions: {
            code: ApolloServerErrorCode.UNAUTHENTICATED
          }
        });
      }

      try {
        return await dataSources.postAPI.create(input);
      } catch (error) {
        throw new GraphQLError('Failed to create post', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR'
          },
          originalError: error
        });
      }
    }
  }
};
解析器中的正确错误处理可确保有意义的错误传递给客户端,同时保护敏感信息。
typescript
import { GraphQLError } from 'graphql';
import { ApolloServerErrorCode } from '@apollo/server/errors';

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      try {
        const user = await dataSources.userAPI.getUserById(id);

        if (!user) {
          throw new GraphQLError('User not found', {
            extensions: {
              code: 'USER_NOT_FOUND',
              http: { status: 404 }
            }
          });
        }

        return user;
      } catch (error) {
        // 记录完整错误用于调试
        console.error('Error fetching user:', error);

        // 向客户端抛出安全错误
        if (error instanceof GraphQLError) {
          throw error;
        }

        throw new GraphQLError('Failed to fetch user', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR'
          }
        });
      }
    }
  },

  Mutation: {
    createPost: async (_, { input }, { user, dataSources }) => {
      // 验证错误
      if (!input.title || input.title.length < 3) {
        throw new GraphQLError('Title must be at least 3 characters', {
          extensions: {
            code: 'BAD_USER_INPUT',
            argumentName: 'title'
          }
        });
      }

      // 认证错误
      if (!user) {
        throw new GraphQLError('Must be authenticated', {
          extensions: {
            code: ApolloServerErrorCode.UNAUTHENTICATED
          }
        });
      }

      try {
        return await dataSources.postAPI.create(input);
      } catch (error) {
        throw new GraphQLError('Failed to create post', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR'
          },
          originalError: error
        });
      }
    }
  }
};

Authentication and Authorization

认证与授权

Implement authentication and authorization patterns in resolvers and context.
typescript
// Authentication middleware
const requireAuth = (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' }
      });
    }
    return resolver(parent, args, context, info);
  };
};

// Authorization middleware
const requireRole = (role: string) => (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' }
      });
    }

    if (!context.user.roles.includes(role)) {
      throw new GraphQLError('Insufficient permissions', {
        extensions: { code: 'FORBIDDEN' }
      });
    }

    return resolver(parent, args, context, info);
  };
};

const resolvers = {
  Query: {
    me: requireAuth((_, __, { user }) => user),

    adminPanel: requireRole('ADMIN')(
      async (_, __, { dataSources }) => {
        return dataSources.adminAPI.getDashboard();
      }
    ),

    // Resource-based authorization
    post: async (_, { id }, { user, dataSources }) => {
      const post = await dataSources.postAPI.getById(id);

      if (!post) {
        throw new GraphQLError('Post not found');
      }

      // Check if user can view this post
      if (post.status === 'DRAFT' && post.authorId !== user?.id) {
        throw new GraphQLError('Cannot view draft posts', {
          extensions: { code: 'FORBIDDEN' }
        });
      }

      return post;
    }
  },

  Mutation: {
    updatePost: requireAuth(
      async (_, { id, input }, { user, dataSources }) => {
        const post = await dataSources.postAPI.getById(id);

        // Check ownership
        if (post.authorId !== user.id && !user.roles.includes('ADMIN')) {
          throw new GraphQLError('Not authorized to update this post', {
            extensions: { code: 'FORBIDDEN' }
          });
        }

        return dataSources.postAPI.update(id, input);
      }
    )
  }
};
在解析器和上下文中实现认证与授权模式。
typescript
// 认证中间件
const requireAuth = (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' }
      });
    }
    return resolver(parent, args, context, info);
  };
};

// 授权中间件
const requireRole = (role: string) => (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' }
      });
    }

    if (!context.user.roles.includes(role)) {
      throw new GraphQLError('Insufficient permissions', {
        extensions: { code: 'FORBIDDEN' }
      });
    }

    return resolver(parent, args, context, info);
  };
};

const resolvers = {
  Query: {
    me: requireAuth((_, __, { user }) => user),

    adminPanel: requireRole('ADMIN')(
      async (_, __, { dataSources }) => {
        return dataSources.adminAPI.getDashboard();
      }
    ),

    // 基于资源的授权
    post: async (_, { id }, { user, dataSources }) => {
      const post = await dataSources.postAPI.getById(id);

      if (!post) {
        throw new GraphQLError('Post not found');
      }

      // 检查用户是否可以查看该帖子
      if (post.status === 'DRAFT' && post.authorId !== user?.id) {
        throw new GraphQLError('Cannot view draft posts', {
          extensions: { code: 'FORBIDDEN' }
        });
      }

      return post;
    }
  },

  Mutation: {
    updatePost: requireAuth(
      async (_, { id, input }, { user, dataSources }) => {
        const post = await dataSources.postAPI.getById(id);

        // 检查所有权
        if (post.authorId !== user.id && !user.roles.includes('ADMIN')) {
          throw new GraphQLError('Not authorized to update this post', {
            extensions: { code: 'FORBIDDEN' }
          });
        }

        return dataSources.postAPI.update(id, input);
      }
    )
  }
};

Caching Strategies

缓存策略

Implement caching at the resolver level for improved performance.
typescript
import { createHash } from 'crypto';

// In-memory cache
const cache = new Map<string, { data: any; expiry: number }>();

const getCacheKey = (prefix: string, args: any): string => {
  const hash = createHash('md5')
    .update(JSON.stringify(args))
    .digest('hex');
  return `${prefix}:${hash}`;
};

const cacheResolver = (
  resolver,
  { ttl = 300, prefix = 'cache' } = {}
) => {
  return async (parent, args, context, info) => {
    const key = getCacheKey(prefix, args);
    const cached = cache.get(key);

    if (cached && cached.expiry > Date.now()) {
      console.log('Cache hit:', key);
      return cached.data;
    }

    const result = await resolver(parent, args, context, info);

    cache.set(key, {
      data: result,
      expiry: Date.now() + (ttl * 1000)
    });

    return result;
  };
};

const resolvers = {
  Query: {
    // Cache for 5 minutes
    popularPosts: cacheResolver(
      async (_, { limit }, { dataSources }) => {
        return dataSources.postAPI.getPopular(limit);
      },
      { ttl: 300, prefix: 'popular-posts' }
    ),

    // Redis caching
    user: async (_, { id }, { redis, dataSources }) => {
      const cacheKey = `user:${id}`;

      // Try cache first
      const cached = await redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }

      // Fetch and cache
      const user = await dataSources.userAPI.getUserById(id);
      await redis.setex(cacheKey, 3600, JSON.stringify(user));

      return user;
    }
  }
};
在解析器层面实现缓存以提升性能。
typescript
import { createHash } from 'crypto';

// 内存缓存
const cache = new Map<string, { data: any; expiry: number }>();

const getCacheKey = (prefix: string, args: any): string => {
  const hash = createHash('md5')
    .update(JSON.stringify(args))
    .digest('hex');
  return `${prefix}:${hash}`;
};

const cacheResolver = (
  resolver,
  { ttl = 300, prefix = 'cache' } = {}
) => {
  return async (parent, args, context, info) => {
    const key = getCacheKey(prefix, args);
    const cached = cache.get(key);

    if (cached && cached.expiry > Date.now()) {
      console.log('Cache hit:', key);
      return cached.data;
    }

    const result = await resolver(parent, args, context, info);

    cache.set(key, {
      data: result,
      expiry: Date.now() + (ttl * 1000)
    });

    return result;
  };
};

const resolvers = {
  Query: {
    // 缓存5分钟
    popularPosts: cacheResolver(
      async (_, { limit }, { dataSources }) => {
        return dataSources.postAPI.getPopular(limit);
      },
      { ttl: 300, prefix: 'popular-posts' }
    ),

    // Redis缓存
    user: async (_, { id }, { redis, dataSources }) => {
      const cacheKey = `user:${id}`;

      // 先尝试从缓存获取
      const cached = await redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }

      // 获取数据并缓存
      const user = await dataSources.userAPI.getUserById(id);
      await redis.setex(cacheKey, 3600, JSON.stringify(user));

      return user;
    }
  }
};

Resolver Middleware and Plugins

解析器中间件与插件

Create reusable middleware patterns for cross-cutting concerns.
typescript
// Logging middleware
const logResolver = (resolver) => {
  return async (parent, args, context, info) => {
    const start = Date.now();
    const fieldName = info.fieldName;

    try {
      const result = await resolver(parent, args, context, info);
      const duration = Date.now() - start;
      console.log(`${fieldName} resolved in ${duration}ms`);
      return result;
    } catch (error) {
      console.error(`${fieldName} failed:`, error);
      throw error;
    }
  };
};

// Timing middleware
const timeResolver = (resolver) => {
  return async (parent, args, context, info) => {
    const start = performance.now();
    const result = await resolver(parent, args, context, info);
    const duration = performance.now() - start;

    // Add timing to extensions
    info.operation.extensions = info.operation.extensions || {};
    info.operation.extensions.timing =
      info.operation.extensions.timing || {};
    info.operation.extensions.timing[info.fieldName] = duration;

    return result;
  };
};

// Compose middleware
const compose = (...middlewares) => (resolver) => {
  return middlewares.reduceRight(
    (acc, middleware) => middleware(acc),
    resolver
  );
};

const resolvers = {
  Query: {
    user: compose(
      logResolver,
      timeResolver,
      requireAuth
    )(async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    })
  }
};
为横切关注点创建可复用的中间件模式。
typescript
// 日志中间件
const logResolver = (resolver) => {
  return async (parent, args, context, info) => {
    const start = Date.now();
    const fieldName = info.fieldName;

    try {
      const result = await resolver(parent, args, context, info);
      const duration = Date.now() - start;
      console.log(`${fieldName} resolved in ${duration}ms`);
      return result;
    } catch (error) {
      console.error(`${fieldName} failed:`, error);
      throw error;
    }
  };
};

// 计时中间件
const timeResolver = (resolver) => {
  return async (parent, args, context, info) => {
    const start = performance.now();
    const result = await resolver(parent, args, context, info);
    const duration = performance.now() - start;

    // 将计时信息添加到扩展字段
    info.operation.extensions = info.operation.extensions || {};
    info.operation.extensions.timing =
      info.operation.extensions.timing || {};
    info.operation.extensions.timing[info.fieldName] = duration;

    return result;
  };
};

// 组合中间件
const compose = (...middlewares) => (resolver) => {
  return middlewares.reduceRight(
    (acc, middleware) => middleware(acc),
    resolver
  );
};

const resolvers = {
  Query: {
    user: compose(
      logResolver,
      timeResolver,
      requireAuth
    )(async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    })
  }
};

Testing Resolvers

测试解析器

Write comprehensive tests for resolvers using mocked context and data sources.
typescript
import { describe, it, expect, vi } from 'vitest';

describe('User Resolvers', () => {
  it('should fetch user by id', async () => {
    const mockUser = { id: '1', username: 'test' };

    const mockContext = {
      dataSources: {
        userAPI: {
          getUserById: vi.fn().mockResolvedValue(mockUser)
        }
      }
    };

    const result = await resolvers.Query.user(
      null,
      { id: '1' },
      mockContext,
      {} as any
    );

    expect(result).toEqual(mockUser);
    expect(mockContext.dataSources.userAPI.getUserById)
      .toHaveBeenCalledWith('1');
  });

  it('should throw error when user not found', async () => {
    const mockContext = {
      dataSources: {
        userAPI: {
          getUserById: vi.fn().mockResolvedValue(null)
        }
      }
    };

    await expect(
      resolvers.Query.user(null, { id: '999' }, mockContext, {} as any)
    ).rejects.toThrow('User not found');
  });

  it('should require authentication', async () => {
    const mockContext = {
      user: null,
      dataSources: {}
    };

    await expect(
      resolvers.Query.me(null, {}, mockContext, {} as any)
    ).rejects.toThrow('Not authenticated');
  });

  it('should use DataLoader for batching', async () => {
    const mockUsers = [
      { id: '1', username: 'user1' },
      { id: '2', username: 'user2' }
    ];

    const batchFn = vi.fn().mockResolvedValue(mockUsers);
    const loader = new DataLoader(batchFn);

    const mockContext = {
      loaders: { userLoader: loader }
    };

    // Make multiple calls
    const [user1, user2] = await Promise.all([
      resolvers.Post.author(
        { authorId: '1' },
        {},
        mockContext,
        {} as any
      ),
      resolvers.Post.author(
        { authorId: '2' },
        {},
        mockContext,
        {} as any
      )
    ]);

    expect(user1).toEqual(mockUsers[0]);
    expect(user2).toEqual(mockUsers[1]);
    expect(batchFn).toHaveBeenCalledTimes(1);
    expect(batchFn).toHaveBeenCalledWith(['1', '2']);
  });
});
使用模拟的上下文和数据源为解析器编写全面的测试。
typescript
import { describe, it, expect, vi } from 'vitest';

describe('User Resolvers', () => {
  it('should fetch user by id', async () => {
    const mockUser = { id: '1', username: 'test' };

    const mockContext = {
      dataSources: {
        userAPI: {
          getUserById: vi.fn().mockResolvedValue(mockUser)
        }
      }
    };

    const result = await resolvers.Query.user(
      null,
      { id: '1' },
      mockContext,
      {} as any
    );

    expect(result).toEqual(mockUser);
    expect(mockContext.dataSources.userAPI.getUserById)
      .toHaveBeenCalledWith('1');
  });

  it('should throw error when user not found', async () => {
    const mockContext = {
      dataSources: {
        userAPI: {
          getUserById: vi.fn().mockResolvedValue(null)
        }
      }
    };

    await expect(
      resolvers.Query.user(null, { id: '999' }, mockContext, {} as any)
    ).rejects.toThrow('User not found');
  });

  it('should require authentication', async () => {
    const mockContext = {
      user: null,
      dataSources: {}
    };

    await expect(
      resolvers.Query.me(null, {}, mockContext, {} as any)
    ).rejects.toThrow('Not authenticated');
  });

  it('should use DataLoader for batching', async () => {
    const mockUsers = [
      { id: '1', username: 'user1' },
      { id: '2', username: 'user2' }
    ];

    const batchFn = vi.fn().mockResolvedValue(mockUsers);
    const loader = new DataLoader(batchFn);

    const mockContext = {
      loaders: { userLoader: loader }
    };

    // 发起多个调用
    const [user1, user2] = await Promise.all([
      resolvers.Post.author(
        { authorId: '1' },
        {},
        mockContext,
        {} as any
      ),
      resolvers.Post.author(
        { authorId: '2' },
        {},
        mockContext,
        {} as any
      )
    ]);

    expect(user1).toEqual(mockUsers[0]);
    expect(user2).toEqual(mockUsers[1]);
    expect(batchFn).toHaveBeenCalledTimes(1);
    expect(batchFn).toHaveBeenCalledWith(['1', '2']);
  });
});

Best Practices

最佳实践

  1. Keep resolvers thin: Delegate business logic to service layer, use resolvers only for data fetching and transformation
  2. Use DataLoader: Implement DataLoader for any resolver that fetches related data to avoid N+1 queries
  3. Leverage context: Store shared resources (database, auth, data sources) in context for all resolvers
  4. Handle errors gracefully: Catch errors and throw meaningful GraphQLError instances with appropriate codes
  5. Implement proper auth: Check authentication and authorization in resolvers or middleware consistently
  6. Cache strategically: Cache expensive operations at resolver level using in-memory or distributed cache
  7. Use typed resolvers: Define TypeScript types for resolver functions to catch errors at compile time
  8. Test thoroughly: Write unit tests for resolvers with mocked dependencies and edge cases
  9. Avoid blocking operations: Use async/await and parallel execution where possible to prevent blocking
  10. Monitor performance: Log resolver execution time and identify slow resolvers for optimization
  1. 保持解析器精简:将业务逻辑委托给服务层,仅使用解析器进行数据获取和转换
  2. 使用DataLoader:为任何获取关联数据的解析器实现DataLoader,以避免N+1查询
  3. 利用上下文:在上下文中存储所有解析器共享的资源(数据库、认证、数据源)
  4. 优雅处理错误:捕获错误并抛出带有适当代码的有意义的GraphQLError实例
  5. 实现正确的认证机制:在解析器或中间件中持续检查认证和授权
  6. 策略性地使用缓存:使用内存缓存或分布式缓存对解析器层面的昂贵操作进行缓存
  7. 使用类型化解析器:为解析器函数定义TypeScript类型,在编译时捕获错误
  8. 全面测试:为解析器编写单元测试,模拟依赖项和边缘情况
  9. 避免阻塞操作:尽可能使用async/await和并行执行,防止阻塞事件循环
  10. 监控性能:记录解析器执行时间,识别需要优化的慢速解析器

Common Pitfalls

常见陷阱

  1. N+1 queries: Fetching related data in loops without batching, causing excessive database queries
  2. Blocking operations: Using synchronous operations in resolvers that block the event loop
  3. Memory leaks: Storing data in closures or module scope that grows unbounded
  4. Inconsistent error handling: Throwing raw errors without proper GraphQLError wrapping and codes
  5. Over-fetching in resolvers: Fetching entire objects when only specific fields are needed
  6. Context mutation: Modifying context object during resolver execution, causing side effects
  7. Missing authentication checks: Forgetting to verify auth in sensitive resolvers
  8. Improper DataLoader usage: Creating new DataLoader instances per resolver instead of per request
  9. Circular resolver chains: Creating resolver dependencies that cause infinite loops
  10. Not using info parameter: Ignoring the info parameter that contains requested fields for optimization
  1. N+1查询:在循环中获取关联数据而不进行批处理,导致过多的数据库查询
  2. 阻塞操作:在解析器中使用同步操作阻塞事件循环
  3. 内存泄漏:在闭包或模块作用域中存储无限制增长的数据
  4. 不一致的错误处理:抛出原始错误而不进行适当的GraphQLError包装和编码
  5. 解析器过度获取:仅需要特定字段时却获取整个对象
  6. 上下文突变:在解析器执行期间修改上下文对象,导致副作用
  7. 缺失认证检查:在敏感解析器中忘记验证认证
  8. DataLoader使用不当:为每个解析器创建新的DataLoader实例,而非每个请求创建一个
  9. 循环解析器链:创建导致无限循环的解析器依赖
  10. 未使用info参数:忽略包含请求字段的info参数以进行优化

When to Use This Skill

何时使用本技能

Use GraphQL resolver skills when:
  • Implementing a new GraphQL server
  • Optimizing existing resolver performance
  • Debugging N+1 query problems
  • Adding authentication and authorization
  • Implementing data batching and caching
  • Writing resolver unit tests
  • Refactoring resolvers for better maintainability
  • Adding logging and monitoring to resolvers
  • Implementing custom middleware or plugins
  • Migrating from REST API to GraphQL
在以下场景中使用GraphQL解析器技能:
  • 实现新的GraphQL服务器
  • 优化现有解析器的性能
  • 调试N+1查询问题
  • 添加认证和授权
  • 实现数据批处理和缓存
  • 编写解析器单元测试
  • 重构解析器以提升可维护性
  • 为解析器添加日志和监控
  • 实现自定义中间件或插件
  • 从REST API迁移到GraphQL

Resources

参考资源