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 |
댓글