参考链接
目录结构:
index.js
const express = require('express')
const { ApolloServer, gql } = require('apollo-server-express')
const schema = require('./schema')
const dataSources = require('./data-sources')
const app = express()
const server = new ApolloServer({
schema,
dataSources,
// 所有的 GraphQL 查询都会经过这里
context ({ req }) {
const token = req.headers['authorization']
return {
token
}
},
// schemaDirectives: {
// // 自定义指令
// }
})
// 将 Apollo-server 和 express 集合到一起
server.applyMiddleware({ app })
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
schema.js
const { makeExecutableSchema } = require('apollo-server-express')
const typeDefs = require('./type-defs')
const UpperCaseDirective = require('./schema-directives/upper')
const AuthCaseDirective = require('./schema-directives/auth')
const userResolvers = require('./resolvers/user')
const articleResolvers = require('./resolvers/article')
const schema = makeExecutableSchema({
typeDefs,
resolvers: [userResolvers, articleResolvers],
schemaDirectives: {
upper: UpperCaseDirective,
auth: AuthCaseDirective
}
})
module.exports = schema
type-defs/index.js
const { gql } = require('apollo-server-express')
const typeDefs = gql`
directive @upper on FIELD_DEFINITION
directive @auth on FIELD_DEFINITION
type User {
email: String!
# username: String! @deprecated(reason: "请使用 newUsername")
username: String!
bio: String
image: String
token: String
following: Boolean
}
type UserPayload {
user: User
}
type ArticlesPayload {
articles: [Article!]
articlesCount: Int!
}
type Query {
# foo: String @upper
foo: String @auth @upper
currentUser: User @auth
articles(offset: Int = 0, limit: Int = 2): ArticlesPayload
}
input LoginInput {
email: String!
password: String!
}
input CreateUserInput {
username: String!
email: String!
password: String!
}
input UpdateUserInput {
email: String
username: String
password: String
image: String
bio: String
}
input CreateArticleInput {
title: String!
description: String!
body: String!
tagList: [String!]
}
type Article {
_id: String!
title: String!
description: String!
body: String!
tagList: [String!]
createdAt: String!
updatedAt: String!
favorited: Boolean
favoritesCount: Int
author: User
}
type CreateArticlePayload {
article: Article
}
type Mutation {
# User
login(user: LoginInput): UserPayload
createUser(user: CreateUserInput): UserPayload
updateUser(user: UpdateUserInput): UserPayload @auth
# Article
createArticle(article: CreateArticleInput): CreateArticlePayload @auth
}
`
module.exports = typeDefs
schema-directives/upper.js
const { SchemaDirectiveVisitor } = require('apollo-server-express')
const { defaultFieldResolver } = require('graphql')
class UpperCaseDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
// 把字段本身的 resolve 函数备份一下
const { resolve = defaultFieldResolver } = field
// 重写字段的 resolve 函数
field.resolve = async function (parent, args, context, info) {
// 调用原本的 resolve 函数
const result = await resolve(parent, args, context, info)
if (typeof result === 'string') {
return result.toUpperCase()
}
return result
}
}
}
module.exports = UpperCaseDirective
schema-directives/auth.js
const { SchemaDirectiveVisitor, AuthenticationError } = require('apollo-server-express')
const { defaultFieldResolver } = require('graphql')
const { jwtSecret } = require('../config/config.default')
const jwt = require('../util/jwt')
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
// 把字段本身的 resolve 函数备份一下
const { resolve = defaultFieldResolver } = field
// 重写字段的 resolve 函数
field.resolve = async function (parent, args, context, info) {
const { token, dataSources } = context
if (!token) {
throw new AuthenticationError('未授权')
}
try {
const decodedData = await jwt.verify(token, jwtSecret)
const user = await dataSources.users.findById(decodedData.userId)
// 把当前登录用户挂载到 context 上下文对象中,给后续的 resolve 使用
context.user = user
} catch (err) {
throw new AuthenticationError('未授权')
}
const result = await resolve(parent, args, context, info)
return result
}
}
}
module.exports = AuthDirective
resolvers/user.js
const { UserInputError } = require('apollo-server-express')
const jwt = require('../util/jwt')
const { jwtSecret } = require('../config/config.default')
const md5 = require('../util/md5')
const resolvers = {
// 所有的 Query 都走这里
Query: {
foo (parent, args, context, info) {
console.log('foo resolve => ', context.user)
return 'hello'
},
currentUser (parent, args, context, info) {
// 校验当前登录状态
// 具体的操作
return context.user
}
},
Mutation: {
async createUser (parent, { user }, { dataSources }) {
// 判断用户是否存在
const users = dataSources.users
const user1 = await users.findByEmail(user.email)
if (user1) {
throw new UserInputError('邮箱已存在')
}
const user2 = await users.findByUsername(user.username)
if (user2) {
throw new UserInputError('用户已存在')
}
// 判断邮箱是否存在
// 保存用户
// 生成 token 发送给客户端
const userData = await users.saveUser(user)
const token = await jwt.sign({
userId: userData._id
}, jwtSecret, {
expiresIn: 60 * 60 * 24
})
return {
user: {
...userData.toObject(),
token
}
}
},
async login (parent, { user }, { dataSources }) {
const userData = await dataSources.users.findByEmail(user.email)
if (!userData) {
throw new UserInputError('邮箱不存在')
}
if (md5(user.password) !== userData.password) {
throw new UserInputError('密码错误')
}
const token = await jwt.sign({
userId: userData._id
}, jwtSecret, {
expiresIn: 60 * 60 * 24
})
return {
user: {
...userData.toObject(),
token
}
}
// 密码是否正确
// 生成用户 token
// 发送成功响应
},
async updateUser (parent, { user: userInput }, { user, dataSources }) {
if (userInput.password) {
userInput.password = md5(userInput.password)
}
const ret = await dataSources.users.updateUser(user._id, userInput)
return {
user: ret
}
}
}
}
module.exports = resolvers
resolvers/article.js
module.exports = {
Query: {
async articles (parent, { offset, limit }, { dataSources }) {
return {}
// const [ articles, articlesCount ] = await Promise.all([
// dataSources.articles.getArticles({
// offset,
// limit
// }),
// dataSources.articles.getCount()
// ])
// return {
// articles,
// articlesCount
// }
}
},
Mutation: {
async createArticle (parent, { article }, { dataSources, user }) {
article.author = user._id
const ret = await dataSources.articles.createArticle
// 根据 用户 ID 获取用户信息填充到 article.author 中
(article)
return {
article: ret
}
}
},
Article: {
async author (parent, args, { dataSources }) {
const user = await dataSources.users.findById(parent.author)
return user
}
},
ArticlesPayload: {
async articles (parent, { offset, limit }, { dataSources }) {
const articles = await dataSources.articles.getArticles({
offset,
limit
})
return articles
},
async articlesCount (parent, args, { dataSources }) {
const count = await dataSources.articles.getCount()
return count
}
}
}
util/jwt.js
const jwt = require('jsonwebtoken')
const { promisify } = require('util')
exports.sign = promisify(jwt.sign)
exports.verify = promisify(jwt.verify)
exports.decode = promisify(jwt.decode)
util/md5.js
const crypto = require('crypto')
module.exports = str => {
return crypto.createHash('md5')
.update('lagou' + str)
.digest('hex')
}
config/config.default.js
/**
* 默认配置
*/
module.exports = {
dbUri: 'mongodb://localhost:27017/realworld',
jwtSecret: '13a054c8-39ea-11eb-adc1-0242ac120002'
}
data-sources/index.js
const dbModel = require('../models')
const Users = require('./user')
const Articles = require('./article')
module.exports = () => {
return {
users: new Users(dbModel.User),
articles: new Articles(dbModel.Article)
}
}
data-sources/user.js
const { MongoDataSource } = require('apollo-datasource-mongodb')
class Users extends MongoDataSource {
findByEmail (email) {
return this.model.findOne({
email
})
}
findByUsername (username) {
return this.model.findOne({
username
})
}
saveUser (args) {
const user = new this.model(args)
return user.save()
}
findById (userId) {
return this.findOneById(userId)
}
updateUser (userId, data) {
return this.model.findOneAndUpdate(
{ _id: userId }, // 条件
data,
{
new: true // 默认返回更新之前的数据,配置为 true 返回更新之后的数据
}
)
}
}
module.exports = Users
data-sources/article.js
const { MongoDataSource } = require('apollo-datasource-mongodb')
class Articles extends MongoDataSource {
createArticle (data) {
const article = new this.model(data)
// article.populate('author').execPopulate()
return article.save()
}
getArticles (options) {
return this.model.find().skip(options.offset).limit(options.limit)
}
getCount () {
return this.model.countDocuments()
}
}
module.exports = Articles
models/index.js
const mongoose = require('mongoose')
const { dbUri } = require('../config/config.default')
// 连接 MongoDB 数据库
mongoose.connect(dbUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false
})
const db = mongoose.connection
// 当连接失败的时候
db.on('error', err => {
console.log('MongoDB 数据库连接失败', err)
})
// 当连接成功的时候
db.once('open', function () {
console.log('MongoDB 数据库连接成功')
})
// 组织导出模型类
module.exports = {
User: mongoose.model('User', require('./user')),
Article: mongoose.model('Article', require('./article'))
}
models/user.js
const mongoose = require('mongoose')
const baseModel = require('./base-model')
const md5 = require('../util/md5')
const userSchema = new mongoose.Schema({
...baseModel,
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true,
set: value => md5(value),
// select: false
},
bio: {
type: String,
default: null
},
image: {
type: String,
default: null
}
})
module.exports = userSchema
models/article.js
const mongoose = require('mongoose')
const baseModel = require('./base-model')
const Schema = mongoose.Schema
const articleSchema = new mongoose.Schema({
...baseModel,
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
body: {
type: String,
required: true
},
tagList: {
type: [String],
default: null
},
favoritesCount: {
type: Number,
default: 0
},
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
}
})
module.exports = articleSchema
models/base-model.js
module.exports = {
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}