阶段三:GraphQL
理论学完了,是时候综合运用!本课实现一个完整的项目管理API——从Schema设计到Resolver实现、DataLoader优化、认证授权、错误处理和性能监控,把前面学到的GraphQL知识串联成生产级应用。
// 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'));
# 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 .
// 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); // 自动批处理!
});
在Mutation的context中获取当前用户,只有项目成员才能创建任务,只有任务创建者或项目Owner才能删除任务。
安装dataloader包,为User和Project创建DataLoader实例,用查询对比优化前后的数据库调用次数。