GraphQL 综合案例

12 篇文章 1 订阅

参考链接

目录结构:
在这里插入图片描述
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
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值