graphql-rubyのinterface機能が便利だった
motivation:
ユーザーのロールに応じたデータをGraphQLで返したい。
全部返してフロント側で表示を制御しても良いが、それだと検証ツールを使った時にNetworkを開けばデータが見えてしまう。
interfaceの機能とは
Interfaces are lists of fields which may be implemented by object types.
と、ある通り、オブジェクト型が実装する可能性のあるフィールドのリストです。
この場合はこのフィールドを返す、といったインターフェースを定義することができます。
サンプルコードを引用:
code:sample.gql
interface Customer {
name: String!
outstandingBalance: Int!
}
type Company implements Customer {
name: String!
outstandingBalance: Int!
}
type Individual implements Customer {
company: Company
name: String!
outstandingBalance: Int!
}
この場合は、顧客というインターフェースを使って個人と法人に対して返すフィールドを変更している。
実際にどう使うか
筆者の環境はRails+Reactです。
サンプルコードです。
まず、interfaceの実装をします。
code:payment_method_interface.rb
module Types
module Interfaces
module PaymentMethodInterface
include Types::BaseInterface
field :id, ID, null: false
field :type, String, null: false
field :last_used, GraphQL::Types::ISO8601DateTime, null: true
field :is_default, Boolean, null: false
definition_methods do
def resolve_type(object, context)
case object
when CreditCard
Types::Objects::CreditCardType
when BankTransfer
Types::Objects::BankTransferType
else
end
end
end
def is_valid
# 支払い方法が有効かどうかを確認する抽象メソッド
raise NotImplementedError, "#{self.class}でis_validメソッドが実装されていません"
end
end
end
end
id, type, last_used, is_defaultはどの場合でも返却できるフィールドです。
公式ドキュメントでいうnameとoutstandingBalanceの部分が該当します。
resolve_typeの中で、objectの型に応じて返却するTypeを分けることができます。
この返却するTypeの中で返却したいものを出し分けられるという訳です。
code:user_type.rb
field :payment_method, Types::Interfaces::PaymentMethodInterface, null: true do
argument :id, ID, required: true
end
user_typeの中で取得することを想定します。
fieldの第2引数にPaymentMethodInterfaceを渡します。
それぞれのtypeもサンプルコードは以下のような感じになります。
code:credit_card_type.rb
module Types
module Objects
class CreditCardType < Types::BaseObject
implements Types::Interfaces::PaymentMethodInterface
field :card_brand, String, null: false
field :last_four_digits, String, null: false
field :expiration_month, Integer, null: false
field :expiration_year, Integer, null: false
def is_valid
# クレジットカードの有効性チェックのロジック
!object.expired? && object.active?
end
def card_brand
object.brand.upcase
end
def last_four_digits
object.last4
end
end
end
end
code:bank_transfer_type.rb
module Types
module Objects
class BankTransferType < Types::BaseObject
implements Types::Interfaces::PaymentMethodInterface
field :bank_name, String, null: false
field :account_number, String, null: false
field :account_type, String, null: false
field :account_holder_name, String, null: false
def is_valid
# 銀行振込の有効性チェックのロジック
object.verified? && object.active?
end
def account_number
# 口座番号の一部をマスク
"****#{object.account_number.last(4)}"
end
end
end
end
この状態でリクエストをしてみましょう。
code:sample.gql
query GetUserPaymentMethod($userId: ID!, $paymentMethodId: ID!) {
user(id: $userId) {
paymentMethod(id: $paymentMethodId) {
id
type
lastUsed
isDefault
... on CreditCardType {
cardBrand
lastFourDigits
expirationMonth
expirationYear
}
... on BankTransferType {
bankName
accountNumber
accountType
accountHolderName
}
}
}
}
...on という記法で指定します。
これで、paymentMethodがCreditCardTypeの場合は...と言ったことを表現できます。
code:credit_card_sample_response.json
{
"data": {
"user": {
"paymentMethod": {
"id": "cc_123456",
"type": "クレジットカード",
"lastUsed": "2023-04-15T10:30:00Z",
"isDefault": true,
"cardBrand": "VISA",
"lastFourDigits": "4321",
"expirationMonth": 12,
"expirationYear": 2025
}
}
}
}
code:bank_transfer_sample_response.json
{
"data": {
"user": {
"paymentMethod": {
"id": "bt_789012",
"type": "銀行振込",
"lastUsed": "2023-03-20T14:45:00Z",
"isDefault": false,
"bankName": "サンプル銀行",
"accountNumber": "****5678",
"accountType": "普通",
"accountHolderName": "山田太郎"
}
}
}
}
便利ですね。