본문 바로가기
GraphQL

Fullstack Part 8 #3 DB

by kicksky 2021. 3. 10.

fullstackopen.com/en/part8/database_and_user_administration 를 읽고 정리한 글.

 

🔸 MongoDB와 GraphQL 백엔드를 연결

🔸 유저 등록 및 유효성 검사

🔸 server 객체의 context로 유저 로그인 여부 확인

🔸 auth가 필요한 작업을 하는 방법 ( 친구 리스트에 새로운 값 추가 등 )

 

 

MongoDB

npm install mongoose mongoose-unique-validator

- 몽구스로 Schema 생성

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true,
    minlength: 5
  },
  phone: {
    type: String,
    minlength: 5
  },
  street: {
    type: String,
    required: true,
    minlength: 5
  },
  city: {
    type: String,
    required: true,
    minlength: 3
  },
})

module.exports = mongoose.model('Person', schema)

- GraphQL도 유효성 검사를 하지만 몽구스로 한번 더 확인

const { ApolloServer, UserInputError, gql } = require('apollo-server')
const mongoose = require('mongoose')
const Person = require('./models/person')

const MONGODB_URI = 'mongodb+srv://fullstack:halfstack@cluster0-ostce.mongodb.net/graphql?retryWrites=true'

console.log('connecting to', MONGODB_URI)

mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })
  .then(() => {
    console.log('connected to MongoDB')
  })
  .catch((error) => {
    console.log('error connection to MongoDB:', error.message)
  })

const typeDefs = gql`
  ...
`

const resolvers = {
  Query: {
    personCount: () => Person.collection.countDocuments(),
    allPersons: (root, args) => {
      // filters missing
      return Person.find({})
    },
    findPerson: (root, args) => Person.findOne({ name: args.name })
  },
  Person: {
    address: root => {
      return {
        street: root.street,
        city: root.city
      }
    }
  },
  Mutation: {
    addPerson: (root, args) => {
      const person = new Person({ ...args })
      return person.save()
    },
    editNumber: async (root, args) => {
      const person = await Person.findOne({ name: args.name })
      person.phone = args.phone
      return person.save()
    }
  }
}

- persons 배열을 Person 모델로 교체

- 몽고DB에 _id 필드가 있기 때문에 직접 id를 만들어 줄 필요는 없다.

- resulover 함수들이 Promise를 리턴한다. 아폴로 서버는 프로미스가 resolve한 값을 되돌려 보낸다. (www.apollographql.com/docs/apollo-server/data/resolvers/#return-values)

 

Query: {
  // ..
  allPersons: (root, args) => {
    if (!args.phone) {
      return Person.find({})
    }

    return Person.find({ phone: { $exists: args.phone === 'YES'  }})
  },
},

유효성 검사

- GraphQL과 mongoose-schema에서 모두 이루어진다.

- DB에 값을 save 메소드로 저장하는 경우에 에러를 try/catch 블록으로 잡아주어야 한다

Mutation: {
  addPerson: async (root, args) => {
      const person = new Person({ ...args })

      try {
        await person.save()
      } catch (error) {
        throw new UserInputError(error.message, {
          invalidArgs: args,
        })
      }
      return person
  },
    editNumber: async (root, args) => {
      const person = await Person.findOne({ name: args.name })
      person.phone = args.phone

      try {
        await person.save()
      } catch (error) {
        throw new UserInputError(error.message, {
          invalidArgs: args,
        })
      }
      return person
    }
}

유저 관리 및 로그인

- 이 예시에서는 비밀번호를 하드코딩하여 동일하게 만들었음

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    minlength: 3
  },
  friends: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Person'
    }
  ],
})

module.exports = mongoose.model('User', schema)

- friends 필드는 foreign-key 개념. 개별 유저의 화면을 맞춤으로 설정할 수 있다.

유저 식별

type User {
  username: String!
  friends: [Person!]!
  id: ID!
}

type Token {
  value: String!
}

type Query {
  // ..
  me: User
}

type Mutation {
  // ...
  createUser(
    username: String!
  ): User
  login(
    username: String!
    password: String!
  ): Token
}

- me query가 현재 로그인 된 유저를 리턴

- createUser mutation이 유저 등록

const jwt = require('jsonwebtoken')

const JWT_SECRET = 'NEED_HERE_A_SECRET_KEY'

Mutation: {
  // ..
  createUser: (root, args) => {
  	// 유저 생성
    const user = new User({ username: args.username })
	// 생성된 유저 저장
    return user.save()
      .catch(error => {
        throw new UserInputError(error.message, {
          invalidArgs: args,
        })
      })
  },
  login: async (root, args) => {
  	// User 모델에서 해당 유저 찾아
    const user = await User.findOne({ username: args.username })
	// 그러한 유저가 없거나 비밀번호가 틀린 경우
    if ( !user || args.password !== 'secred' ) {
      throw new UserInputError("wrong credentials")
    }
	// 정상적인 경우에는 Token 발생
    const userForToken = {
      username: user.username,
      id: user._id,
    }
	// 이를 jwt로 암호화하여 res body로 보낸다
    return { value: jwt.sign(userForToken, JWT_SECRET) }
  },
},

- 로그인한 유저는 로그인할 때 받은 토큰을 서버에 요청을 할 때마다 Authorization 헤더에 담아서 보낸다.

 서버는 요청을 받을 때마다 이를 식별하는 과정을 거쳐야 함

☞ server 객체의 세 번째 파라미터로 context를 설정

Context

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    const auth = req ? req.headers.authorization : null
    if (auth && auth.toLowerCase().startsWith('bearer ')) {
      const decodedToken = jwt.verify(
        auth.substring(7), JWT_SECRET
      )
      const currentUser = await User.findById(decodedToken.id).populate('friends')
      return { currentUser }
    }
  }
})

- context : user 식별처럼 여러 resolver가 모두 공유하는 작업이 이루어지는 곳 ( www.apollographql.com/blog/authorization-in-graphql-452b1c402a9/?_ga=2.45656161.474875091.1550613879-1581139173.1549828167 )

 

me query

Query: {
  // ...
  me: (root, args, context) => {
    return context.currentUser
  }
},

- context를 받아 현재 로그인한 유저를 확인할 수 있다.

- 없으면 query 리턴 값은 null

 

Friends List

- person을 추가하거나 수정하려면 로그인 여부를 확인해야 함

- person을 추가하면 자동으로 유저의 friend list에 추가되어야 함

Mutation: {
  addPerson: async (root, args, context) => {
  	// Person 인스턴스 생성
    const person = new Person({ ...args })
    // 로그인 여부 확인
    const currentUser = context.currentUser

    if (!currentUser) {
      throw new AuthenticationError("not authenticated")
    }

    try {
      // 새로운 person 저장한 뒤
      // 로그인한 유저의 friends에 추가
      await person.save()
      currentUser.friends = currentUser.friends.concat(person)
      await currentUser.save()
    } catch (error) {
      throw new UserInputError(error.message, {
        invalidArgs: args,
      })
    }

    return person
  },
  //...
}

addAsFriend Mutation

type Mutation {
  // ...
  addAsFriend(
    name: String!
  ): User
}

addAsFriend: async (root, args, { currentUser }) => {
	// 로그인한 유저의 friend 필드 배열에 매개변수로 들어온 값과 일치하는 값이 있는지 확인
    const nonFriendAlready = (person) => 
      !currentUser.friends.map(f => f._id).includes(person._id)

    if (!currentUser) {
      throw new AuthenticationError("not authenticated")
    }

    const person = await Person.findOne({ name: args.name })
    // 리스트에 등록한 적이 없는 친구인 경우에 추가
    if ( nonFriendAlready(person) ) {
      currentUser.friends = currentUser.friends.concat(person)
    }

    await currentUser.save()

    return currentUser
  },

 

 

 

'GraphQL' 카테고리의 다른 글

Fullstack Part 8 #5 Fragments and subscriptions  (0) 2021.03.12
Fullstack Part 8 #4 Login & Cache update  (0) 2021.03.11
Fullstack part 8 #2 React and GraphQL  (0) 2021.03.09
Fullstack Part 8 #1 GraphQL-server  (0) 2021.03.08
REST와 GraphQL  (0) 2021.03.08

댓글