发布于

GraphQL API开发实战:Schema设计与查询优化技巧

作者

GraphQL API开发实战:Schema设计与查询优化技巧

GraphQL为API开发提供了更灵活、高效的数据查询方式。本文将分享GraphQL API开发的实战经验和最佳实践。

GraphQL基础概念

Schema设计原则

# schema.graphql - GraphQL Schema定义

# 标量类型扩展
scalar DateTime
scalar JSON
scalar Upload

# 枚举类型
enum UserRole {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

enum SortDirection {
  ASC
  DESC
}

# 接口定义
interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

interface Timestamped {
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 用户类型
type User implements Node & Timestamped {
  id: ID!
  email: String!
  username: String!
  firstName: String
  lastName: String
  avatar: String
  role: UserRole!
  isActive: Boolean!
  lastLoginAt: DateTime
  profile: UserProfile
  posts: [Post!]!
  orders: [Order!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 用户资料
type UserProfile {
  bio: String
  website: String
  location: String
  birthDate: DateTime
  preferences: JSON
}

# 文章类型
type Post implements Node & Timestamped {
  id: ID!
  title: String!
  content: String!
  excerpt: String
  slug: String!
  status: PostStatus!
  publishedAt: DateTime
  author: User!
  categories: [Category!]!
  tags: [Tag!]!
  comments: [Comment!]!
  likes: [Like!]!
  viewCount: Int!
  createdAt: DateTime!
  updatedAt: DateTime!
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

# 分类类型
type Category implements Node & Timestamped {
  id: ID!
  name: String!
  slug: String!
  description: String
  parent: Category
  children: [Category!]!
  posts: [Post!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 标签类型
type Tag implements Node & Timestamped {
  id: ID!
  name: String!
  slug: String!
  color: String
  posts: [Post!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 评论类型
type Comment implements Node & Timestamped {
  id: ID!
  content: String!
  author: User!
  post: Post!
  parent: Comment
  replies: [Comment!]!
  likes: [Like!]!
  isApproved: Boolean!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 点赞类型
type Like implements Node & Timestamped {
  id: ID!
  user: User!
  post: Post
  comment: Comment
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 订单类型
type Order implements Node & Timestamped {
  id: ID!
  orderNumber: String!
  user: User!
  items: [OrderItem!]!
  status: OrderStatus!
  totalAmount: Float!
  shippingAddress: Address!
  billingAddress: Address
  paymentMethod: PaymentMethod!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 订单项
type OrderItem {
  id: ID!
  product: Product!
  quantity: Int!
  price: Float!
  total: Float!
}

# 产品类型
type Product implements Node & Timestamped {
  id: ID!
  name: String!
  description: String!
  price: Float!
  sku: String!
  stock: Int!
  images: [String!]!
  categories: [Category!]!
  variants: [ProductVariant!]!
  reviews: [Review!]!
  averageRating: Float
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 产品变体
type ProductVariant {
  id: ID!
  name: String!
  price: Float!
  sku: String!
  stock: Int!
  attributes: JSON!
}

# 地址类型
type Address {
  id: ID!
  street: String!
  city: String!
  state: String!
  zipCode: String!
  country: String!
}

# 支付方式
type PaymentMethod {
  id: ID!
  type: String!
  last4: String
  expiryMonth: Int
  expiryYear: Int
}

# 评价类型
type Review implements Node & Timestamped {
  id: ID!
  rating: Int!
  title: String
  content: String!
  user: User!
  product: Product!
  isVerified: Boolean!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# 分页信息
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

# 连接类型
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}

# 输入类型
input UserInput {
  email: String!
  username: String!
  firstName: String
  lastName: String
  password: String!
}

input UserUpdateInput {
  username: String
  firstName: String
  lastName: String
  avatar: Upload
}

input PostInput {
  title: String!
  content: String!
  excerpt: String
  slug: String
  categoryIds: [ID!]!
  tagIds: [ID!]
  status: PostStatus = DRAFT
}

input PostUpdateInput {
  title: String
  content: String
  excerpt: String
  slug: String
  categoryIds: [ID!]
  tagIds: [ID!]
  status: PostStatus
}

input PostFilter {
  authorId: ID
  categoryId: ID
  tagId: ID
  status: PostStatus
  search: String
  dateRange: DateRangeInput
}

input DateRangeInput {
  start: DateTime
  end: DateTime
}

input SortInput {
  field: String!
  direction: SortDirection = ASC
}

# 查询根类型
type Query {
  # 用户查询
  user(id: ID!): User
  users(
    first: Int
    after: String
    filter: UserFilter
    sort: SortInput
  ): UserConnection!
  me: User
  
  # 文章查询
  post(id: ID, slug: String): Post
  posts(
    first: Int
    after: String
    filter: PostFilter
    sort: SortInput
  ): PostConnection!
  
  # 分类查询
  category(id: ID, slug: String): Category
  categories: [Category!]!
  
  # 标签查询
  tag(id: ID, slug: String): Tag
  tags: [Tag!]!
  
  # 产品查询
  product(id: ID!): Product
  products(
    first: Int
    after: String
    filter: ProductFilter
    sort: SortInput
  ): ProductConnection!
  
  # 订单查询
  order(id: ID!): Order
  orders(
    first: Int
    after: String
    filter: OrderFilter
    sort: SortInput
  ): OrderConnection!
  
  # 搜索
  search(query: String!, type: SearchType): SearchResult!
}

# 变更根类型
type Mutation {
  # 用户操作
  createUser(input: UserInput!): User!
  updateUser(id: ID!, input: UserUpdateInput!): User!
  deleteUser(id: ID!): Boolean!
  
  # 认证操作
  login(email: String!, password: String!): AuthPayload!
  logout: Boolean!
  refreshToken: AuthPayload!
  
  # 文章操作
  createPost(input: PostInput!): Post!
  updatePost(id: ID!, input: PostUpdateInput!): Post!
  deletePost(id: ID!): Boolean!
  publishPost(id: ID!): Post!
  
  # 评论操作
  createComment(postId: ID!, content: String!, parentId: ID): Comment!
  updateComment(id: ID!, content: String!): Comment!
  deleteComment(id: ID!): Boolean!
  
  # 点赞操作
  likePost(postId: ID!): Like!
  unlikePost(postId: ID!): Boolean!
  likeComment(commentId: ID!): Like!
  unlikeComment(commentId: ID!): Boolean!
  
  # 订单操作
  createOrder(input: OrderInput!): Order!
  updateOrderStatus(id: ID!, status: OrderStatus!): Order!
  cancelOrder(id: ID!): Order!
}

# 订阅根类型
type Subscription {
  # 文章订阅
  postAdded: Post!
  postUpdated(id: ID!): Post!
  postDeleted: ID!
  
  # 评论订阅
  commentAdded(postId: ID!): Comment!
  commentUpdated(id: ID!): Comment!
  commentDeleted: ID!
  
  # 用户订阅
  userOnline: User!
  userOffline: ID!
  
  # 订单订阅
  orderStatusChanged(userId: ID!): Order!
}

# 认证载荷
type AuthPayload {
  token: String!
  refreshToken: String!
  user: User!
  expiresIn: Int!
}

# 搜索相关
enum SearchType {
  ALL
  POSTS
  USERS
  PRODUCTS
}

union SearchResult = Post | User | Product

# 错误类型
type Error {
  message: String!
  code: String!
  path: [String!]
}

# API响应包装
type Response {
  success: Boolean!
  message: String
  errors: [Error!]
}

Apollo Server实现

// server/index.ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { GraphQLScalarType } from 'graphql'
import { DateTimeResolver, JSONResolver } from 'graphql-scalars'
import { readFileSync } from 'fs'
import { join } from 'path'
import { resolvers } from './resolvers'
import { createContext } from './context'
import { formatError } from './utils/errorHandler'
import { authDirective } from './directives/auth'
import { rateLimitDirective } from './directives/rateLimit'
import { cacheDirective } from './directives/cache'

// 读取Schema文件
const typeDefs = readFileSync(join(__dirname, 'schema.graphql'), 'utf8')

// 自定义标量类型
const customScalars = {
  DateTime: DateTimeResolver,
  JSON: JSONResolver,
  Upload: new GraphQLScalarType({
    name: 'Upload',
    description: 'File upload scalar type',
    parseValue: (value) => value,
    serialize: (value) => value,
    parseLiteral: (ast) => ast
  })
}

// 创建Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers: {
    ...customScalars,
    ...resolvers
  },
  formatError,
  plugins: [
    // 查询复杂度分析
    {
      requestDidStart() {
        return {
          didResolveOperation(requestContext) {
            const { request, document } = requestContext
            // 分析查询复杂度
            console.log('Query complexity analysis:', {
              query: request.query,
              variables: request.variables
            })
          }
        }
      }
    },
    
    // 性能监控
    {
      requestDidStart() {
        return {
          willSendResponse(requestContext) {
            const { response } = requestContext
            console.log('Response metrics:', {
              duration: Date.now() - requestContext.request.http?.startTime,
              errors: response.errors?.length || 0
            })
          }
        }
      }
    }
  ],
  
  // 指令
  schemaDirectives: {
    auth: authDirective,
    rateLimit: rateLimitDirective,
    cache: cacheDirective
  }
})

// 启动服务器
async function startServer() {
  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
    context: createContext
  })
  
  console.log(`🚀 GraphQL Server ready at ${url}`)
}

startServer().catch(error => {
  console.error('Failed to start server:', error)
  process.exit(1)
})

// context.ts - 上下文创建
import { Request } from 'express'
import jwt from 'jsonwebtoken'
import { User } from './models/User'
import { DataLoader } from './dataloaders'

export interface Context {
  user?: User
  dataloaders: DataLoader
  req: Request
  isAuthenticated: boolean
}

export async function createContext({ req }: { req: Request }): Promise<Context> {
  let user: User | undefined
  let isAuthenticated = false
  
  // 从请求头获取token
  const token = req.headers.authorization?.replace('Bearer ', '')
  
  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any
      user = await User.findById(decoded.userId)
      isAuthenticated = !!user
    } catch (error) {
      console.warn('Invalid token:', error.message)
    }
  }
  
  return {
    user,
    isAuthenticated,
    req,
    dataloaders: new DataLoader()
  }
}

// dataloaders/index.ts - DataLoader实现
import DataLoader from 'dataloader'
import { User } from '../models/User'
import { Post } from '../models/Post'
import { Category } from '../models/Category'
import { Comment } from '../models/Comment'

export class DataLoaders {
  // 用户DataLoader
  userLoader = new DataLoader<string, User>(
    async (userIds) => {
      const users = await User.find({ _id: { $in: userIds } })
      const userMap = new Map(users.map(user => [user._id.toString(), user]))
      return userIds.map(id => userMap.get(id) || null)
    },
    {
      cache: true,
      maxBatchSize: 100
    }
  )
  
  // 文章DataLoader
  postLoader = new DataLoader<string, Post>(
    async (postIds) => {
      const posts = await Post.find({ _id: { $in: postIds } })
      const postMap = new Map(posts.map(post => [post._id.toString(), post]))
      return postIds.map(id => postMap.get(id) || null)
    }
  )
  
  // 按作者ID批量加载文章
  postsByAuthorLoader = new DataLoader<string, Post[]>(
    async (authorIds) => {
      const posts = await Post.find({ authorId: { $in: authorIds } })
      const postsByAuthor = new Map<string, Post[]>()
      
      posts.forEach(post => {
        const authorId = post.authorId.toString()
        if (!postsByAuthor.has(authorId)) {
          postsByAuthor.set(authorId, [])
        }
        postsByAuthor.get(authorId)!.push(post)
      })
      
      return authorIds.map(id => postsByAuthor.get(id) || [])
    }
  )
  
  // 按文章ID批量加载评论
  commentsByPostLoader = new DataLoader<string, Comment[]>(
    async (postIds) => {
      const comments = await Comment.find({ postId: { $in: postIds } })
      const commentsByPost = new Map<string, Comment[]>()
      
      comments.forEach(comment => {
        const postId = comment.postId.toString()
        if (!commentsByPost.has(postId)) {
          commentsByPost.set(postId, [])
        }
        commentsByPost.get(postId)!.push(comment)
      })
      
      return postIds.map(id => commentsByPost.get(id) || [])
    }
  )
  
  // 分类DataLoader
  categoryLoader = new DataLoader<string, Category>(
    async (categoryIds) => {
      const categories = await Category.find({ _id: { $in: categoryIds } })
      const categoryMap = new Map(categories.map(cat => [cat._id.toString(), cat]))
      return categoryIds.map(id => categoryMap.get(id) || null)
    }
  )
  
  // 清除缓存方法
  clearUser(userId: string) {
    this.userLoader.clear(userId)
  }
  
  clearPost(postId: string) {
    this.postLoader.clear(postId)
    // 清除相关的批量加载缓存
    this.postsByAuthorLoader.clearAll()
    this.commentsByPostLoader.clear(postId)
  }
  
  clearAll() {
    this.userLoader.clearAll()
    this.postLoader.clearAll()
    this.postsByAuthorLoader.clearAll()
    this.commentsByPostLoader.clearAll()
    this.categoryLoader.clearAll()
  }
}

Resolver实现

查询Resolver

// resolvers/query.ts
import { QueryResolvers } from '../generated/graphql'
import { Context } from '../context'
import { ForbiddenError, UserInputError } from 'apollo-server-express'
import { validatePagination, buildFilter, buildSort } from '../utils/helpers'

export const Query: QueryResolvers<Context> = {
  // 用户查询
  async user(_, { id }, { dataloaders }) {
    return dataloaders.userLoader.load(id)
  },
  
  async users(_, { first = 10, after, filter, sort }, { user, isAuthenticated }) {
    // 权限检查
    if (!isAuthenticated || user?.role !== 'ADMIN') {
      throw new ForbiddenError('Access denied')
    }
    
    // 验证分页参数
    validatePagination({ first, after })
    
    // 构建查询条件
    const queryFilter = buildFilter(filter)
    const querySort = buildSort(sort)
    
    // 游标分页
    if (after) {
      queryFilter._id = { $gt: after }
    }
    
    const users = await User.find(queryFilter)
      .sort(querySort)
      .limit(first + 1)
    
    const hasNextPage = users.length > first
    if (hasNextPage) {
      users.pop()
    }
    
    const edges = users.map(user => ({
      node: user,
      cursor: user._id.toString()
    }))
    
    return {
      edges,
      pageInfo: {
        hasNextPage,
        hasPreviousPage: !!after,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor
      },
      totalCount: await User.countDocuments(queryFilter)
    }
  },
  
  async me(_, __, { user, isAuthenticated }) {
    if (!isAuthenticated) {
      throw new ForbiddenError('Authentication required')
    }
    return user
  },
  
  // 文章查询
  async post(_, { id, slug }, { dataloaders }) {
    if (id) {
      return dataloaders.postLoader.load(id)
    }
    
    if (slug) {
      return Post.findOne({ slug })
    }
    
    throw new UserInputError('Either id or slug must be provided')
  },
  
  async posts(_, { first = 10, after, filter, sort }) {
    validatePagination({ first, after })
    
    const queryFilter = buildFilter(filter)
    const querySort = buildSort(sort, { createdAt: -1 })
    
    // 处理搜索
    if (filter?.search) {
      queryFilter.$text = { $search: filter.search }
    }
    
    // 处理日期范围
    if (filter?.dateRange) {
      queryFilter.createdAt = {}
      if (filter.dateRange.start) {
        queryFilter.createdAt.$gte = filter.dateRange.start
      }
      if (filter.dateRange.end) {
        queryFilter.createdAt.$lte = filter.dateRange.end
      }
    }
    
    // 游标分页
    if (after) {
      queryFilter._id = { $gt: after }
    }
    
    const posts = await Post.find(queryFilter)
      .sort(querySort)
      .limit(first + 1)
    
    const hasNextPage = posts.length > first
    if (hasNextPage) {
      posts.pop()
    }
    
    const edges = posts.map(post => ({
      node: post,
      cursor: post._id.toString()
    }))
    
    return {
      edges,
      pageInfo: {
        hasNextPage,
        hasPreviousPage: !!after,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor
      },
      totalCount: await Post.countDocuments(queryFilter)
    }
  },
  
  // 分类查询
  async category(_, { id, slug }, { dataloaders }) {
    if (id) {
      return dataloaders.categoryLoader.load(id)
    }
    
    if (slug) {
      return Category.findOne({ slug })
    }
    
    throw new UserInputError('Either id or slug must be provided')
  },
  
  async categories() {
    return Category.find().sort({ name: 1 })
  },
  
  // 搜索
  async search(_, { query, type = 'ALL' }) {
    const results = []
    
    if (type === 'ALL' || type === 'POSTS') {
      const posts = await Post.find({
        $text: { $search: query },
        status: 'PUBLISHED'
      }).limit(10)
      results.push(...posts)
    }
    
    if (type === 'ALL' || type === 'USERS') {
      const users = await User.find({
        $or: [
          { username: { $regex: query, $options: 'i' } },
          { firstName: { $regex: query, $options: 'i' } },
          { lastName: { $regex: query, $options: 'i' } }
        ],
        isActive: true
      }).limit(10)
      results.push(...users)
    }
    
    if (type === 'ALL' || type === 'PRODUCTS') {
      const products = await Product.find({
        $text: { $search: query }
      }).limit(10)
      results.push(...products)
    }
    
    return results
  }
}

// resolvers/mutation.ts
import { MutationResolvers } from '../generated/graphql'
import { Context } from '../context'
import { AuthenticationError, ForbiddenError, UserInputError } from 'apollo-server-express'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { validateEmail, validatePassword, generateSlug } from '../utils/validation'

export const Mutation: MutationResolvers<Context> = {
  // 用户操作
  async createUser(_, { input }, { user, isAuthenticated, dataloaders }) {
    // 权限检查
    if (!isAuthenticated || user?.role !== 'ADMIN') {
      throw new ForbiddenError('Access denied')
    }
    
    // 输入验证
    if (!validateEmail(input.email)) {
      throw new UserInputError('Invalid email format')
    }
    
    if (!validatePassword(input.password)) {
      throw new UserInputError('Password must be at least 8 characters')
    }
    
    // 检查邮箱唯一性
    const existingUser = await User.findOne({ email: input.email })
    if (existingUser) {
      throw new UserInputError('Email already exists')
    }
    
    // 检查用户名唯一性
    const existingUsername = await User.findOne({ username: input.username })
    if (existingUsername) {
      throw new UserInputError('Username already exists')
    }
    
    // 密码加密
    const hashedPassword = await bcrypt.hash(input.password, 12)
    
    // 创建用户
    const newUser = new User({
      ...input,
      password: hashedPassword
    })
    
    await newUser.save()
    
    // 清除相关缓存
    dataloaders.clearAll()
    
    return newUser
  },
  
  async updateUser(_, { id, input }, { user, isAuthenticated, dataloaders }) {
    if (!isAuthenticated) {
      throw new AuthenticationError('Authentication required')
    }
    
    // 权限检查:只能更新自己的信息或管理员可以更新任何人
    if (user?._id.toString() !== id && user?.role !== 'ADMIN') {
      throw new ForbiddenError('Access denied')
    }
    
    const targetUser = await User.findById(id)
    if (!targetUser) {
      throw new UserInputError('User not found')
    }
    
    // 检查用户名唯一性
    if (input.username && input.username !== targetUser.username) {
      const existingUsername = await User.findOne({ username: input.username })
      if (existingUsername) {
        throw new UserInputError('Username already exists')
      }
    }
    
    // 更新用户
    Object.assign(targetUser, input)
    await targetUser.save()
    
    // 清除缓存
    dataloaders.clearUser(id)
    
    return targetUser
  },
  
  // 认证操作
  async login(_, { email, password }) {
    const user = await User.findOne({ email }).select('+password')
    if (!user) {
      throw new AuthenticationError('Invalid credentials')
    }
    
    const isValidPassword = await bcrypt.compare(password, user.password)
    if (!isValidPassword) {
      throw new AuthenticationError('Invalid credentials')
    }
    
    if (!user.isActive) {
      throw new AuthenticationError('Account is deactivated')
    }
    
    // 更新最后登录时间
    user.lastLoginAt = new Date()
    await user.save()
    
    // 生成JWT
    const token = jwt.sign(
      { userId: user._id, email: user.email },
      process.env.JWT_SECRET!,
      { expiresIn: '7d' }
    )
    
    const refreshToken = jwt.sign(
      { userId: user._id, type: 'refresh' },
      process.env.JWT_REFRESH_SECRET!,
      { expiresIn: '30d' }
    )
    
    return {
      token,
      refreshToken,
      user,
      expiresIn: 7 * 24 * 60 * 60 // 7天(秒)
    }
  },
  
  // 文章操作
  async createPost(_, { input }, { user, isAuthenticated, dataloaders }) {
    if (!isAuthenticated) {
      throw new AuthenticationError('Authentication required')
    }
    
    // 生成slug
    const slug = input.slug || generateSlug(input.title)
    
    // 检查slug唯一性
    const existingPost = await Post.findOne({ slug })
    if (existingPost) {
      throw new UserInputError('Slug already exists')
    }
    
    // 创建文章
    const post = new Post({
      ...input,
      slug,
      authorId: user!._id
    })
    
    await post.save()
    
    // 清除相关缓存
    dataloaders.clearAll()
    
    return post
  },
  
  async updatePost(_, { id, input }, { user, isAuthenticated, dataloaders }) {
    if (!isAuthenticated) {
      throw new AuthenticationError('Authentication required')
    }
    
    const post = await Post.findById(id)
    if (!post) {
      throw new UserInputError('Post not found')
    }
    
    // 权限检查:只有作者或管理员可以编辑
    if (post.authorId.toString() !== user!._id.toString() && user?.role !== 'ADMIN') {
      throw new ForbiddenError('Access denied')
    }
    
    // 检查slug唯一性
    if (input.slug && input.slug !== post.slug) {
      const existingPost = await Post.findOne({ slug: input.slug })
      if (existingPost) {
        throw new UserInputError('Slug already exists')
      }
    }
    
    // 更新文章
    Object.assign(post, input)
    await post.save()
    
    // 清除缓存
    dataloaders.clearPost(id)
    
    return post
  },
  
  async deletePost(_, { id }, { user, isAuthenticated, dataloaders }) {
    if (!isAuthenticated) {
      throw new AuthenticationError('Authentication required')
    }
    
    const post = await Post.findById(id)
    if (!post) {
      throw new UserInputError('Post not found')
    }
    
    // 权限检查
    if (post.authorId.toString() !== user!._id.toString() && user?.role !== 'ADMIN') {
      throw new ForbiddenError('Access denied')
    }
    
    await Post.findByIdAndDelete(id)
    
    // 清除缓存
    dataloaders.clearPost(id)
    
    return true
  }
}

查询优化技巧

N+1问题解决

// resolvers/types.ts - 类型Resolver
import { Resolvers } from '../generated/graphql'
import { Context } from '../context'

export const typeResolvers: Resolvers<Context> = {
  // 用户类型解析器
  User: {
    // 使用DataLoader解决N+1问题
    async posts(parent, _, { dataloaders }) {
      return dataloaders.postsByAuthorLoader.load(parent._id.toString())
    },
    
    async orders(parent, _, { dataloaders }) {
      return dataloaders.ordersByUserLoader.load(parent._id.toString())
    },
    
    // 计算字段
    async fullName(parent) {
      return `${parent.firstName || ''} ${parent.lastName || ''}`.trim()
    }
  },
  
  // 文章类型解析器
  Post: {
    async author(parent, _, { dataloaders }) {
      return dataloaders.userLoader.load(parent.authorId.toString())
    },
    
    async categories(parent, _, { dataloaders }) {
      if (!parent.categoryIds?.length) return []
      return dataloaders.categoryLoader.loadMany(
        parent.categoryIds.map(id => id.toString())
      )
    },
    
    async comments(parent, _, { dataloaders }) {
      return dataloaders.commentsByPostLoader.load(parent._id.toString())
    },
    
    async likes(parent, _, { dataloaders }) {
      return dataloaders.likesByPostLoader.load(parent._id.toString())
    },
    
    // 计算字段
    async likeCount(parent, _, { dataloaders }) {
      const likes = await dataloaders.likesByPostLoader.load(parent._id.toString())
      return likes.length
    },
    
    async commentCount(parent, _, { dataloaders }) {
      const comments = await dataloaders.commentsByPostLoader.load(parent._id.toString())
      return comments.length
    }
  },
  
  // 评论类型解析器
  Comment: {
    async author(parent, _, { dataloaders }) {
      return dataloaders.userLoader.load(parent.authorId.toString())
    },
    
    async post(parent, _, { dataloaders }) {
      return dataloaders.postLoader.load(parent.postId.toString())
    },
    
    async parent(parent, _, { dataloaders }) {
      if (!parent.parentId) return null
      return dataloaders.commentLoader.load(parent.parentId.toString())
    },
    
    async replies(parent, _, { dataloaders }) {
      return dataloaders.commentsByParentLoader.load(parent._id.toString())
    }
  },
  
  // 联合类型解析器
  SearchResult: {
    __resolveType(obj) {
      if (obj.title && obj.content) return 'Post'
      if (obj.email && obj.username) return 'User'
      if (obj.price && obj.sku) return 'Product'
      return null
    }
  }
}

// utils/queryComplexity.ts - 查询复杂度分析
import { createComplexityLimitRule } from 'graphql-query-complexity'
import { GraphQLError } from 'graphql'

export const complexityLimitRule = createComplexityLimitRule(1000, {
  maximumComplexity: 1000,
  variables: {},
  createError: (max: number, actual: number) => {
    return new GraphQLError(
      `Query is too complex: ${actual}. Maximum allowed complexity: ${max}`
    )
  },
  // 字段复杂度计算
  fieldExtensions: {
    complexity: (options: any) => {
      const { args, childComplexity } = options
      
      // 分页字段的复杂度计算
      if (args.first) {
        return args.first * childComplexity
      }
      
      return childComplexity
    }
  }
})

// utils/queryDepth.ts - 查询深度限制
import depthLimit from 'graphql-depth-limit'

export const depthLimitRule = depthLimit(10, {
  ignore: ['__schema', '__type']
})

// utils/rateLimiting.ts - 速率限制
import { shield, rule, and, or } from 'graphql-shield'
import { RateLimiterMemory } from 'rate-limiter-flexible'

const rateLimiter = new RateLimiterMemory({
  keyPrefix: 'graphql',
  points: 100, // 请求数
  duration: 60, // 时间窗口(秒)
})

const rateLimit = rule({ cache: 'contextual' })(
  async (parent, args, context, info) => {
    try {
      const key = context.user?.id || context.req.ip
      await rateLimiter.consume(key)
      return true
    } catch (rejRes) {
      throw new Error('Rate limit exceeded')
    }
  }
)

// 权限规则
const isAuthenticated = rule({ cache: 'contextual' })(
  async (parent, args, context) => {
    return context.isAuthenticated
  }
)

const isAdmin = rule({ cache: 'contextual' })(
  async (parent, args, context) => {
    return context.user?.role === 'ADMIN'
  }
)

const isOwner = rule({ cache: 'contextual' })(
  async (parent, args, context) => {
    // 检查资源所有权
    return true // 具体实现根据业务逻辑
  }
)

// 权限盾牌
export const permissions = shield({
  Query: {
    users: and(isAuthenticated, isAdmin, rateLimit),
    me: isAuthenticated,
    posts: rateLimit,
    post: rateLimit
  },
  Mutation: {
    createUser: and(isAuthenticated, isAdmin),
    updateUser: and(isAuthenticated, or(isOwner, isAdmin)),
    deleteUser: and(isAuthenticated, isAdmin),
    createPost: isAuthenticated,
    updatePost: and(isAuthenticated, isOwner),
    deletePost: and(isAuthenticated, isOwner)
  }
}, {
  allowExternalErrors: true,
  fallbackError: 'Access denied'
})

总结

GraphQL API开发的核心要点:

  1. Schema设计:类型定义、接口设计、输入验证
  2. Resolver实现:查询优化、错误处理、权限控制
  3. 性能优化:DataLoader、查询复杂度、缓存策略
  4. 安全防护:认证授权、速率限制、查询深度控制
  5. 最佳实践:代码组织、错误处理、监控日志

GraphQL为API开发提供了强大的灵活性,通过合理的设计和优化,可以构建出高性能、可扩展的API服务。