- 发布于
GraphQL API开发实战:Schema设计与查询优化技巧
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
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开发的核心要点:
- Schema设计:类型定义、接口设计、输入验证
- Resolver实现:查询优化、错误处理、权限控制
- 性能优化:DataLoader、查询复杂度、缓存策略
- 安全防护:认证授权、速率限制、查询深度控制
- 最佳实践:代码组织、错误处理、监控日志
GraphQL为API开发提供了强大的灵活性,通过合理的设计和优化,可以构建出高性能、可扩展的API服务。