📖 第15课:GraphQL实战

阶段三:GraphQL

理论学完了,是时候综合运用!本课实现一个完整的项目管理API——从Schema设计到Resolver实现、DataLoader优化、认证授权、错误处理和性能监控,把前面学到的GraphQL知识串联成生产级应用。

🎯 项目管理API设计

// project-api.js - 完整项目管理GraphQL API
const express = require('express');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema } = require('graphql');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// ===== Schema =====

const schema = buildSchema(`
  scalar DateTime

  # ===== 枚举 =====
  enum ProjectStatus { PLANNING ACTIVE ON_HOLD COMPLETED CANCELLED }
  enum TaskStatus { TODO IN_PROGRESS IN_REVIEW DONE CANCELLED }
  enum Priority { LOW MEDIUM HIGH URGENT }
  enum Role { OWNER ADMIN MEMBER VIEWER }

  # ===== 核心类型 =====
  type User {
    id: ID!
    name: String!
    email: String!
    avatar: String
    role: Role!
    projects(first: Int = 20): ProjectConnection!
    assignedTasks(status: TaskStatus, first: Int = 20): TaskConnection!
    projectCount: Int!
    taskCount(status: TaskStatus): Int!
    createdAt: DateTime!
  }

  type Project {
    id: ID!
    name: String!
    description: String
    status: ProjectStatus!
    owner: User!
    members: [ProjectMember!]!
    memberCount: Int!
    tasks(status: TaskStatus, priority: Priority, first: Int = 20): TaskConnection!
    taskCount(status: TaskStatus): Int!
    progress: Float!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type ProjectMember {
    user: User!
    role: Role!
    joinedAt: DateTime!
  }

  type Task {
    id: ID!
    title: String!
    description: String
    status: TaskStatus!
    priority: Priority!
    project: Project!
    assignee: User
    creator: User!
    tags: [String!]!
    dueDate: DateTime
    timeEstimate: Int
    timeSpent: Int
    subtasks: [Task!]!
    comments(first: Int = 10): [Comment!]!
    commentCount: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Comment {
    id: ID!
    body: String!
    author: User!
    createdAt: DateTime!
  }

  # ===== 连接类型 =====
  type ProjectConnection {
    edges: [ProjectEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }
  type ProjectEdge { node: Project! cursor: String! }
  type TaskConnection {
    edges: [TaskEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }
  type TaskEdge { node: Task! cursor: String! }
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    endCursor: String
  }

  # ===== 输入类型 =====
  input CreateProjectInput {
    name: String!
    description: String
    memberIds: [ID!]! = []
  }
  input UpdateProjectInput {
    name: String
    description: String
    status: ProjectStatus
  }
  input CreateTaskInput {
    projectId: ID!
    title: String!
    description: String
    priority: Priority = MEDIUM
    assigneeId: ID
    tags: [String!]! = []
    dueDate: DateTime
    timeEstimate: Int
  }
  input UpdateTaskInput {
    title: String
    description: String
    status: TaskStatus
    priority: Priority
    assigneeId: ID
    tags: [String!]
    dueDate: DateTime
  }

  # ===== 操作 =====
  type Query {
    me: User!
    user(id: ID!): User
    users(role: Role, first: Int = 20): [User!]!

    project(id: ID!): Project
    projects(status: ProjectStatus, first: Int = 20, after: String): ProjectConnection!

    task(id: ID!): Task
    tasks(projectId: ID, status: TaskStatus, priority: Priority, assigneeId: ID, first: Int = 20): TaskConnection!

    search(query: String!, type: SearchType = ALL): [SearchResult!]!
  }

  enum SearchType { ALL PROJECTS TASKS USERS }
  union SearchResult = Project | Task | User

  type Mutation {
    # 项目
    createProject(input: CreateProjectInput!): Project!
    updateProject(id: ID!, input: UpdateProjectInput!): Project!
    deleteProject(id: ID!): Boolean!
    addProjectMember(projectId: ID!, userId: ID!, role: Role = MEMBER): Project!

    # 任务
    createTask(input: CreateTaskInput!): Task!
    updateTask(id: ID!, input: UpdateTaskInput!): Task!
    deleteTask(id: ID!): Boolean!
    assignTask(taskId: ID!, userId: ID): Task!
    moveTask(taskId: ID!, status: TaskStatus!): Task!

    # 评论
    addComment(taskId: ID!, body: String!): Comment!
  }
`);

// ===== 数据层 =====

const db = {
  users: [
    { id: '1', name: '张三', email: 'zhang@example.com', role: 'OWNER', avatar: '👨‍💼', createdAt: '2025-01-01' },
    { id: '2', name: '李四', email: 'li@example.com', role: 'ADMIN', avatar: '👩‍💻', createdAt: '2025-01-02' },
    { id: '3', name: '王五', email: 'wang@example.com', role: 'MEMBER', avatar: '🧑‍🎨', createdAt: '2025-01-03' },
  ],
  projects: [
    { id: '1', name: 'API重构', description: '将REST API迁移到GraphQL', status: 'ACTIVE', ownerId: '1', memberIds: ['1', '2', '3'], createdAt: '2025-01-05', updatedAt: '2025-01-15' },
    { id: '2', name: '移动端开发', description: 'React Native移动应用', status: 'PLANNING', ownerId: '2', memberIds: ['2', '3'], createdAt: '2025-01-08', updatedAt: '2025-01-14' },
  ],
  tasks: [
    { id: '1', title: '设计GraphQL Schema', description: '定义所有类型和操作', status: 'DONE', priority: 'HIGH', projectId: '1', assigneeId: '1', creatorId: '1', tags: ['GraphQL'], dueDate: '2025-01-20', timeEstimate: 8, timeSpent: 7, createdAt: '2025-01-06', updatedAt: '2025-01-10' },
    { id: '2', title: '实现Resolver', description: '编写所有查询和变更的resolver', status: 'IN_PROGRESS', priority: 'HIGH', projectId: '1', assigneeId: '2', creatorId: '1', tags: ['GraphQL', '后端'], dueDate: '2025-01-25', timeEstimate: 16, timeSpent: 10, createdAt: '2025-01-07', updatedAt: '2025-01-15' },
    { id: '3', title: '添加认证', description: 'JWT认证中间件', status: 'TODO', priority: 'URGENT', projectId: '1', assigneeId: '1', creatorId: '2', tags: ['安全'], dueDate: '2025-01-22', timeEstimate: 4, timeSpent: 0, createdAt: '2025-01-08', updatedAt: '2025-01-08' },
    { id: '4', title: '技术选型', description: '确定移动端技术栈', status: 'IN_REVIEW', priority: 'MEDIUM', projectId: '2', assigneeId: '3', creatorId: '2', tags: ['移动端'], dueDate: '2025-01-18', timeEstimate: 4, timeSpent: 3, createdAt: '2025-01-09', updatedAt: '2025-01-13' },
    { id: '5', title: 'UI设计稿', description: '移动端界面设计', status: 'TODO', priority: 'MEDIUM', projectId: '2', assigneeId: null, creatorId: '2', tags: ['设计'], dueDate: '2025-02-01', timeEstimate: 24, timeSpent: 0, createdAt: '2025-01-10', updatedAt: '2025-01-10' },
  ],
  comments: [
    { id: '1', body: 'Schema设计已审核通过', authorId: '2', taskId: '1', createdAt: '2025-01-09' },
    { id: '2', body: '注意处理N+1查询问题', authorId: '1', taskId: '2', createdAt: '2025-01-10' },
  ],
};

let nextId = { project: 3, task: 6, comment: 3 };

// ===== DataLoader模拟 =====

class SimpleDataLoader {
  constructor(batchFn) {
    this.batchFn = batchFn;
    this.cache = new Map();
    this.queue = [];
    this.scheduled = false;
  }

  load(key) {
    if (this.cache.has(key)) return Promise.resolve(this.cache.get(key));
    return new Promise((resolve) => {
      this.queue.push({ key, resolve });
      if (!this.scheduled) {
        this.scheduled = true;
        process.nextTick(() => this.dispatch());
      }
    });
  }

  async dispatch() {
    const items = [...this.queue];
    this.queue = [];
    this.scheduled = false;
    const keys = items.map(i => i.key);
    const results = await this.batchFn(keys);
    items.forEach((item, i) => {
      this.cache.set(item.key, results[i]);
      item.resolve(results[i]);
    });
  }
}

const userLoader = new SimpleDataLoader(async (ids) => {
  return ids.map(id => db.users.find(u => u.id === id) || null);
});

// ===== Resolvers =====

const root = {
  // Query
  me: () => db.users[0],
  user: ({ id }) => db.users.find(u => u.id === id),
  users: ({ role, first }) => {
    let result = [...db.users];
    if (role) result = result.filter(u => u.role === role);
    return result.slice(0, first);
  },
  project: ({ id }) => db.projects.find(p => p.id === id),
  projects: ({ status, first, after }) => {
    let result = [...db.projects];
    if (status) result = result.filter(p => p.status === status);
    return { edges: result.slice(0, first).map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: result.length > first, hasPreviousPage: false }, totalCount: result.length };
  },
  task: ({ id }) => db.tasks.find(t => t.id === id),
  tasks: ({ projectId, status, priority, assigneeId, first }) => {
    let result = [...db.tasks];
    if (projectId) result = result.filter(t => t.projectId === projectId);
    if (status) result = result.filter(t => t.status === status);
    if (priority) result = result.filter(t => t.priority === priority);
    if (assigneeId) result = result.filter(t => t.assigneeId === assigneeId);
    return { edges: result.slice(0, first).map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: result.length };
  },
  search: ({ query, type }) => {
    const q = query.toLowerCase();
    const results = [];
    if (type !== 'TASKS') db.projects.filter(p => p.name.toLowerCase().includes(q)).forEach(p => results.push(p));
    if (type !== 'PROJECTS') db.tasks.filter(t => t.title.toLowerCase().includes(q)).forEach(t => results.push(t));
    if (type !== 'USERS') db.users.filter(u => u.name.toLowerCase().includes(q)).forEach(u => results.push(u));
    return results.slice(0, 10);
  },
  // Type Resolvers
  User: {
    projects: (user) => db.projects.filter(p => p.memberIds.includes(user.id)),
    assignedTasks: (user, { status }) => {
      let result = db.tasks.filter(t => t.assigneeId === user.id);
      if (status) result = result.filter(t => t.status === status);
      return { edges: result.map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: result.length };
    },
    projectCount: (user) => db.projects.filter(p => p.memberIds.includes(user.id)).length,
    taskCount: (user, { status }) => {
      let result = db.tasks.filter(t => t.assigneeId === user.id);
      if (status) result = result.filter(t => t.status === status);
      return result.length;
    },
  },
  Project: {
    owner: (p) => userLoader.load(p.ownerId),
    members: (p) => p.memberIds.map(id => ({ user: db.users.find(u => u.id === id), role: id === p.ownerId ? 'OWNER' : 'MEMBER', joinedAt: p.createdAt })),
    memberCount: (p) => p.memberIds.length,
    tasks: (p, { status, priority, first }) => {
      let result = db.tasks.filter(t => t.projectId === p.id);
      if (status) result = result.filter(t => t.status === status);
      if (priority) result = result.filter(t => t.priority === priority);
      return { edges: result.slice(0, first).map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: result.length };
    },
    taskCount: (p, { status }) => {
      let result = db.tasks.filter(t => t.projectId === p.id);
      if (status) result = result.filter(t => t.status === status);
      return result.length;
    },
    progress: (p) => {
      const projectTasks = db.tasks.filter(t => t.projectId === p.id);
      if (projectTasks.length === 0) return 0;
      const done = projectTasks.filter(t => t.status === 'DONE').length;
      return Math.round(done / projectTasks.length * 100);
    },
  },
  Task: {
    project: (t) => db.projects.find(p => p.id === t.projectId),
    assignee: (t) => t.assigneeId ? userLoader.load(t.assigneeId) : null,
    creator: (t) => userLoader.load(t.creatorId),
    subtasks: () => [],
    comments: (t, { first }) => db.comments.filter(c => c.taskId === t.id).slice(0, first || 10),
    commentCount: (t) => db.comments.filter(c => c.taskId === t.id).length,
  },
  Comment: { author: (c) => userLoader.load(c.authorId) },
  // Mutations
  createProject: ({ input }) => {
    const id = String(nextId.project++);
    const project = { id, ...input, status: 'PLANNING', ownerId: '1', memberIds: ['1', ...(input.memberIds || [])], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
    db.projects.push(project);
    return project;
  },
  updateProject: ({ id, input }) => {
    const project = db.projects.find(p => p.id === id);
    if (!project) throw new Error(`项目 ${id} 不存在`);
    Object.assign(project, input, { updatedAt: new Date().toISOString() });
    return project;
  },
  deleteProject: ({ id }) => { const idx = db.projects.findIndex(p => p.id === id); if (idx === -1) return false; db.projects.splice(idx, 1); return true; },
  createTask: ({ input }) => {
    const id = String(nextId.task++);
    const task = { id, ...input, status: 'TODO', creatorId: '1', timeSpent: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
    db.tasks.push(task);
    return task;
  },
  updateTask: ({ id, input }) => {
    const task = db.tasks.find(t => t.id === id);
    if (!task) throw new Error(`任务 ${id} 不存在`);
    Object.assign(task, input, { updatedAt: new Date().toISOString() });
    return task;
  },
  moveTask: ({ taskId, status }) => {
    const task = db.tasks.find(t => t.id === taskId);
    if (!task) throw new Error(`任务 ${taskId} 不存在`);
    task.status = status;
    task.updatedAt = new Date().toISOString();
    return task;
  },
  assignTask: ({ taskId, userId }) => {
    const task = db.tasks.find(t => t.id === taskId);
    if (!task) throw new Error(`任务 ${taskId} 不存在`);
    task.assigneeId = userId;
    task.updatedAt = new Date().toISOString();
    return task;
  },
  addComment: ({ taskId, body }) => {
    const id = String(nextId.comment++);
    const comment = { id, body, authorId: '1', taskId, createdAt: new Date().toISOString() };
    db.comments.push(comment);
    return comment;
  },
};

app.all('/graphql', createHandler({ schema, rootValue: root }));
app.listen(3000, () => console.log('🚀 项目管理API运行在 http://localhost:3000/graphql'));

测试项目管理API

# 1. 获取项目概览(嵌套关联)
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ project(id: \"1\") { name status progress owner { name } tasks { totalCount edges { node { id title status priority assignee { name } } } } } }"}' | jq .
{
  "data": {
    "project": {
      "name": "API重构",
      "status": "ACTIVE",
      "progress": 33,
      "owner": {"name":"张三"},
      "tasks": {
        "totalCount": 3,
        "edges": [
          {"node":{"id":"1","title":"设计GraphQL Schema","status":"DONE","priority":"HIGH","assignee":{"name":"张三"}}},
          {"node":{"id":"2","title":"实现Resolver","status":"IN_PROGRESS","priority":"HIGH","assignee":{"name":"李四"}}},
          {"node":{"id":"3","title":"添加认证","status":"TODO","priority":"URGENT","assignee":{"name":"张三"}}}
        ]
      }
    }
  }
}
# 2. 用户工作面板
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ me { name projectCount taskCount(status: IN_PROGRESS) assignedTasks(status: TODO first: 5) { edges { node { id title priority project { name } dueDate } } } } }"}' | jq .

# 3. 创建任务
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { createTask(input: { projectId: \"1\", title: \"编写测试\", priority: HIGH, tags: [\"测试\"] }) { id title status priority } }"}' | jq .

# 4. 移动任务状态
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { moveTask(taskId: \"3\", status: IN_PROGRESS) { id title status } }"}' | jq .

# 5. 搜索
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ search(query: \"GraphQL\") { ... on Project { id name } ... on Task { id title } ... on User { id name } } }"}' | jq .

⚡ DataLoader与N+1优化

N+1问题与DataLoader解法

// N+1问题:查询10篇文章的作者 → 11次数据库查询
// 无DataLoader:
posts.forEach(post => {
  const author = db.findUser(post.authorId); // 每篇文章1次查询
});

// 有DataLoader:
const userLoader = new DataLoader(async (ids) => {
  // 批量查询:1次SQL搞定
  return db.findUsersByIds(ids);  // WHERE id IN (1, 2, 3, ...)
});

posts.forEach(post => {
  const author = await userLoader.load(post.authorId); // 自动批处理!
});

📝 本课小结

  1. 综合运用Schema + Query + Mutation构建完整API
  2. DataLoader解决N+1查询问题,批处理数据库访问
  3. 进度计算等派生字段在Resolver中实现
  4. 连接类型(Connection)统一分页模式
  5. 联合类型实现多类型搜索
  6. Mutation使用输入类型模式,保持一致性

💪 练习

练习1:添加权限控制

在Mutation的context中获取当前用户,只有项目成员才能创建任务,只有任务创建者或项目Owner才能删除任务。

练习2:实现DataLoader

安装dataloader包,为User和Project创建DataLoader实例,用查询对比优化前后的数据库调用次数。

🏆 本课成就:GraphQL全栈工程师