Mutation Root
GraphQL mutation 은 모두 mutation
keyword 로 시작한다.
mutation($accountNumber: ID!, $newBalance: Int!) {
# ^^^^ here
setAccountBalance(accountNumber: $accountNumber, newBalance: $newBalance) {
# ...
}
}
mutation
으로 시작하는 동작들은 GraphQL 런타임에 의해서 특별한 취급을 받는다. 루트 필드들은 순차적으로 실행되도록 보장된다. 이러한 방법으로 여러개의 mutation 의 결과를 예측할 수 있다.
mutation 은 특정한 GraphQL 객체인 Mutation 에 의해 실행된다. 이 객체는 다른 GraphQL 객체들과 같이 정의되어 있다.
class Types::Mutation < Types::BaseObject
# ...
end
그리고 나면 이 것은 스키마에 mutation(...)의 형태로 연결된다.
class Schema < GraphQL::Schema
# ...
mutation(Types::Mutation)
end
이제 들어오는 요청이 mutation 키워드를 사용하면 이 것은 Mutation으로 간다.
Mutation Classes
GraphQL mutation 은 특수한 필드이다. 데이터를 읽고 계산을 수행하는 대신에 그들은 어플리케이션 상태를 수정한다. 예를 들어 mutation 필드는 다음과 같은 것 들을 할 수 있다.
- 데이터의 생성, 수정, 제거
- 이미 존재하는 데이터와의 관계를 구축
- 카운터 증가
- 파일 생성, 수정, 제거
- 캐쉬 클리어
이러한 액션들은 side effects 라고 불리운다.
모든 GraphQL 필드와 같이 mutation 필드는
- arguments 라고 불리우는 인풋을 받는다.
- fields 를 통해 값을 리턴한다.
GraphQL-Ruby 는 mutation 생성을 돕기 위해 두 개의 클래스를 포함한다.
GraphQL::Schema::Mutation
, 최소한의 베이스 클래스GraphQL::Schema::RelayClassicMutation
, Relay Classic mutation 스펙을 지원하는 베이스 클래스
두 개 외에도 단순히 field API를 사용하여 mutation 필드를 작성할 수 있다.
추가적인 null 헬퍼 메서드는 mutation 에 null 설정을 허용하기 위해서 GraphQL::Schema::Mutation 를 상속받은 클래스로부터 제공된다. 이 것은 필수가 아니며 기본값은 true 이다.
Example mutation class
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
end
그리고 이 것을 너의 mutation 으로 확장한다.
class Mutations::CreateComment < Mutations::BaseMutation
null true
argument :body, String, required: true
argument :post_id, ID, required: true
field :comment, Types::Comment, null: true
field :errors, [String], null: false
def resolve(body:, post_id:)
post = Post.find(post_id)
comment = post.comments.build(body: body, author: context[:current_user])
if comment.save
# 성공적으로 생성, 생성된 객체를 빈 에러와 함께 리턴한다.
{
comment: comment,
errors: [],
}
else
# 저장 실패, 에러와 함께 클라이언트에게 리턴한다.
{
comment: nil,
errors: comment.errors.full_messages
}
end
end
end
#resolve
메서드는 field 이름들과 매치하는 심볼들과 함께 해쉬를 리턴해야 한다.
(Mutation Errors 에 대한 더 자세한 정보)
Hooking up mutations
Mutation 은 반드시 mutation root에 mutation:
키워드와 함께 연결되어야 한다.
class Types::Mutation < Types::BaseObject
field :create_comment, mutation: Mutations::CreateComment
end
Auto-loading arguments
대부분의 케이스에서 GraphQL mutation 은 주어진 글로벌 relay ID 에 반해서 동작할 것이다. 이 글로벌 relay ID 를 이용한 객체의 로딩은 mutation 의 resolver 에 있는 반복되는 코드를 필요로 한다.
대안적인 방법은 argument 를 정의할 때 loads:
argument 를 사용하는 것이다.
class Mutations::AddStar < Mutations::BaseMutation
argument :post_id, ID, required: true, loads: Types::Post
field :post, Types::Post, null: true
def resolve(post:)
post.star
{
post: post,
}
end
end
post_id
argument 가 Types::Post
객체 타입을 로드함을 명시함으로써, Post 객체는 Schema#object_from_id
를 통해서 제공된 post_id와 함께 로드될 것이다.
_id 로 끝나거나 loads:
메서드를 사용하는 argument 의 _id 접미사는 모두 제거된다. 예를 들어, 위의 mutation resolver 는 post_id
argument 대신에 객체가 로드된 post: 를 받는다.
loads:
옵션은 또한 ID 들의 리스트와도 사용할 수 있다.
class Mutations::AddStars < Mutations::BaseMutation
argument :post_ids, [ID], required: true, loads: Types::Post
field :posts, [Types::Post], null: true
def resolve(posts:)
posts.map(&:star)
{
posts: posts,
}
end
end
_ids 로 끝나거나 loads:
메서드를 사용하는 변수의 _id 접미사는 모두 제거되며 s
가 이름 뒤에 붙는다. 예를 들어, mutation resolver 은 post_ids
변수 대신에 모든 객체가 로드된 posts
변수를 받는다.
어떤 경우에는 결과 변수 이름을 변경하고 싶을 것이다. 이 때는 as: 변수를 사용하여 할 수 있다.
class Mutations::AddStar < Mutations::BaseMutation
argument :post_id, ID, required: true, loads: Types::Post, as: :something
field :post, Types::Post, null: true
def resolve(something:)
something.star
{
post: something
}
end
end
위의 예시에서는 loads:
는 구체적인 타입으로 제공되었지만 추상 타입 또한 지원된다.(예를 들어 interace 나 union 같은)
Mutation errors
mutation 내에서 에러는 어떻게 핸들링 할까? 몇 가지 옵션을 살펴보자.
Raising Errors
에러를 핸들링 하는 한 가지 방법은 다음과 같다.
def resolve(id:, attributes:)
# 만약 데이터가 유효하지 않다면 crash 될 것이다.
Post.find(id).update!(attributes.to_h)
# ...
end
또는
def resolve(id:, attributes:)
if post.update(attributes)
{ post: post }
else
raise GraphQL::ExecutionError, post.errors.full_messages.join(", ")
end
end
이러한 종류의 에러 핸들링은 에러 상태를 표현한다.( HTTP 500
또는 top-level "errors"
키를 통해서) 그러나 이 것은 GraphQL의 타입 시스템의 이점을 사용하지 않을 뿐더러 오직 한번에 하나의 에러만을 표현할 수 있다. 이렇게 하는 것은 되긴 되나 더 나은 방안은 에러를 데이터처럼 취급하는 것이다.
Errors as Data
에러를 핸들링하는 또 다른 방법은 에러타입을 스키마에 추가하는 것이다.
class Types::UserError < Types::BaseObject
description "A user-readable error"
field :message, String, null: false,
description: "A description of the error"
field :path, [String], null: true,
description: "Which input value this error came from"
end
그리고 해당 에러 필드를 사용하고자 하는 mutation 에 추가한다.
class Mutations::UpdatePost < Mutations::BaseMutation
# ...
field :errors, [Types::UserError], null: false
end
그리고 mutation의 resolve
메서드에 errors:가 hash 형태로 리턴되도록 한다.
def resolve(id:, attributes:)
post = Post.find(id)
if post.update(attributes)
{
post: post,
errors: [],
}
else
# Rails 모델 에러를 GraphQL-ready error hash로 변환
user_errors = post.errors.map do |attribute, message|
# This is the GraphQL argument which corresponds to the validation error:
path = ["attributes", attribute.camelize]
{
path: path,
message: message,
}
end
{
post: post,
errors: user_errors,
}
end
end
이제 필드가 페이로드에 errors
를 리턴하므로 이것은 errors 를 들어오는 mutation 의 일부로써 지원한다. 예를 들어보자.
mutation($postId: ID!, $postAttributes: PostAttributes!) {
updatePost(id: $postId, attributes: $postAttributes) {
# 이 부분은 성공,실패 모든 경우에 보여진다
post {
title
comments {
body
}
}
# 실패 했을 경우, 에러가 이 리스트 안에 있게된다
errors {
path
message
}
}
}
실패했을 경우, 너는 아래와 같은 응답을 받게 될 것이다.
{
"data" => {
"createPost" => {
"post" => nil,
"errors" => [
{ "message" => "Title can't be blank", "path" => ["attributes", "title"] },
{ "message" => "Body can't be blank", "path" => ["attributes", "body"] }
]
}
}
}
그 후 클라이언트 앱은 사용자가 필드를 올바르게 수정할 수 있도록해당 에러 메시지를 사용자에게 보여줄 것이다.
Nullable Mutation Payload Fields
위에 설명한 것 처럼 "에러를 데이터로써 취급하는 것" 의 혜택을 받으려면 mutation 필드가 반드시 null: true
를 가져야 한다. 왜 그런 것일까?
non-null 필드( null: false
) 의 경우, 만약 nil 을 리턴하면 GraphQl 은 쿼리를 중단하고 해당 필드를 응답으로부터 모두 제거한다. mutation 에서 에러가 발생하면 다른 필드들도 nil 을 리턴할 수 있다. 그러므로 만약 다른 필드가 null: false
를 가지고 있는데 nil 을 리턴하게 된다면 GraphQL 은 당황하게 되고 에러를 포함해서 전체 mutation 을 응답으로부터 제거하게 될 것이다!
풍부한 에러 데이터를 갖기 위해서는, 심지어 다른 필드가 nil 일 때에도 오류가 발생할 때 타입 시스템을 준수할 수 있도록 해당 필드는 반드시 null: true를 가져야 한다.
Here’s an example of a nullable field (good!):
class Mutations::UpdatePost < Mutations::BaseMutation
# rich errors 을 사용하기 위해서는 반드시 `null: true` 를 사용해야 한다.
field :post, Types::Post, null: true
# ...
end
Mutation authorization
mutation 을 실행하기 전에, 몇가지 해야할 것이 있다.
- 현재 사용자가 해당 mutation 을 수행할 수 있는 권한이 있는지 확인
-
ID
인풋을 사용하여 데이터베이스로부터 몇개의 객체 로드 - 해당 사용자가 로드된 객체를 수정할 수 있는지 권한 확인
Checking the user permissions
데이터베이스로 부터 데이터를 로드하기 앞서, 해당 사용자가 권한이 있는지를 확인하고 싶을 것이다. 예를 들어 오직 .admin?사용자만 Mutation.promoteEmployee 를 실행할 수 있다고 보자.
muation #ready?의 메서드를 이용해서 구현할 수 있다.
class Mutations::PromoteEmployee < Mutations::BaseMutation
def ready?(**args)
if !context[:current_user].admin?
raise GraphQL::ExecutionError, "Only admins can run this mutation"
else
# 해당 mutation 을 계속 하기 위해서 true 값 리턴
true
end
end
# ...
end
이제 non-admin
사용자가 mutation 을 시도한다면 이는 실행되지 않을 것이다. 대신에 에러를 응답메시지로 받게 될 것이다.
추가적으로 #ready?
는 errors as data 를 사용하여 false, { ... }
를 리턴할 수 있다.
def ready?
if !context[:current_user].allowed?
return false, { errors: ["You don't have permission to do this"]}
else
true
end
end
Loading and authorizing objects
때때로 mutation 은 ID 를 인풋으로 받은 뒤 이를 데이터베이스로부터 정보를 로드하는데 사용한다. GraphQL-Ruby 는 옵션이 있다면 ID 를 로드할 수 있다.
간단히 말해서 여기 예시가 있다.
class Mutations::PromoteEmployee < Mutations::BaseMutation
# `employeeId` 는 ID 이고 Types::Employee 는 객체의 타입이다
argument :employee_id, ID, required: true, loads: Types::Employee
# `:employee_id` 는 데이터베이스로부터 객체를 가져오도록 사용된다.
# 그리고 나면 객체는`Employee.authorized?` 로 권한이 부여되고
# 모든게 유효하다면 객체는 여기로 삽입된다.
def resolve(employee:)
employee.promote!
end
end
이 것의 동작원리는 다음과 같다. 만약 너가 loads:
옵션을 전달하면 이 것은
- 자동적으로 이름으로부터
_id
를 제거하고 이를as:
옵션으로 전달한다 - 주어진
ID
(Schema.object_from_id 를 사용하여
)를 이용하여 객체를 가져오기 위해서 prepare hook 를 추가한다 - 가져온 객체의 타입이
loads:
type 과 매치하는지 확인한다 (Schema.resolve_type 를 사용하여
) - 가져온 객체를 해당 타입의 .authorized? 후크를 사용하여 실행하기(더보기 Authorization)
object-style name (
에 삽입하기employee:
)을 사용하여 #resolve
위의 경우에서 만약 object_from_id 로 제공받은 변수의 값이 값을 리턴하지 않을 때, mutation 은 에러와 함께 실패로 처리된다.
만약 이렇게 동작하는 것을 원하지 않는다면 사용하지 않으면 된다. 대신에 타입 ID 를 사용해서 매개변수를 생성할 수 있다.
# 특별한 loading 을 사용하지 않음
argument :employee_id, ID, required: true
Can this user perform this action?
가끔은 특정한 사용자-객체-행동의 조합을 권한부여를 체크해야 할 수도 있다. 예를 들어서 사용자는 모든 employees 를 승진 시킬 수 없다. 그들은 오직 그들이 관리하는 employees 만 승진시킬 수 있어야 한다.
이 것은 #authorized?
메서드 구현을 통해서 이뤄질 수 있다. 예를 들어,
def authorized?(employee:)
context[:current_user].manager_of?(employee)
end
#authorized?
가 false
를 리턴하면 mutation 은 중단될 것이고 true
를 리턴하면 이어질 것이다.
Adding errors
에러를 데이터로써 추가하기 위해서는 false
와 함께 리턴할 수 있다. 예를 들어
def authorized?(employee:)
if context[:current_user].manager_of?(employee)
true
else
return false, { errors: ["Can't promote an employee you don't manage"] }
end
end
대안으로, GraphQL::ExecutionError 를 발생시킴으로써 top-level error 를 추가할 수 있다. 예를 들어
def authorized?(employee:)
if !context[:current_user].manager_of?(employee)
raise GraphQL::ExecutionError, "You can only promote your _own_ employees"
end
end
위의 두 경우 모두 mutation 은 중단될 것이다.
Finally, doing the work
이제 사용자가 권한 확인이 되었고, 데이터가 로드되었고, 객체가 유효성 검사를 거쳤으니 #resolve
를 통해서 데이터베이스를 수정할 수 있다.
def resolve(employee:)
if employee.promote
{
employee: employee,
errors: [],
}
else
# See "Mutation Errors" for more:
{
errors: employee.errors.full_messages
}
end
end
* 해당 글은 번역기 돌리다가 크롬 번역기 말도 안되는 해석에 지친 본인이 나중에 참고할 의도로 대충대충 발로 해석한 것이니 참고용으로만 사용하시길 바랍니다.
* 출처: http://graphql-ruby.org/mutations/mutation_root.html
* 출처: http://graphql-ruby.org/mutations/mutation_classes.html
* 출처: http://graphql-ruby.org/mutations/mutation_errors.html
* 출처: http://graphql-ruby.org/mutations/mutation_authorization.html
'배움의 즐거움 > 프로그래밍' 카테고리의 다른 글
Rails Authorization with Pundit (0) | 2019.01.01 |
---|---|
(33) Graphql-ruby - 에러 핸들링 (0) | 2019.01.01 |
(31) Graphql-ruby - 필드(fields) 의 모든 것 (0) | 2019.01.01 |
(30) Graphql-ruby - 범위설정(scoping) (0) | 2019.01.01 |
(29) Graphql-ruby - Pundit Integration (0) | 2019.01.01 |