【graphql-go】Goで始めるGraphQL その1【オブジェクト/スカラー/列挙型の実装】
前回はGraphQLの考え方をなぞった上で扱う/扱わない話題と使用する言語・ライブラリ等のバージョンについて確認しました。
今回はオブジェクト、スカラー、列挙型の定義について書いていきます。
例えばユーザー情報をこんな構造体で定義していたとします。
package model
// ユーザーのID
type UserID string
// Unix時間
type UnixTime int
// ユーザーを表す構造体
type User struct {
ID UserID `gorm:"column:id;primaryKey;"`
CreatedAt UnixTime `gorm:"column:created_at"`
UpdatedAt UnixTime `gorm:"column:updated_at"`
DeletedAt UnixTime `gorm:"column:deleted_at"`
ScreenID string `gorm:"column:screen_id"`
Email string `gorm:"column:email"`
HashedPassword string `gorm:"column:hashed_password"`
Name string `gorm:"column:name"`
Status UserStatus `gorm:"column:status"`
}
// ユーザーの状態
type UserStatus int
const (
Inactive UserStatus = iota // 無効なユーザー
Active // 有効なユーザー
Frozen // 凍結されたユーザー
)
ID
はstring型と混同しないようUserID
型を宣言しています。このUserID
型はGraphQLスキーマではスカラーとして後ほど定義します。UnixTime
型も後ほどスカラーとして定義します。
HashedPassword
はハッシュ化されてデータベースに保存されたパスワードです。絶対にレスポンスしないのでオブジェクトの定義には書きません。[1]
User
オブジェクトのスキーマは以下のように定義します。
type User {
id: UserID
createdAt: UnixTime
updatedAt: UnixTime
deletedAt: UnixTime
screenID: string
email: string
name: string
status: UserStatus
}
scalar UserID
scalar UnixTime
enum UserStatus {
INACTIVE
ACTIVE
FROZEN
}
スカラーを定義
それではUserID
型とUnixTime
型のスカラーを定義していきましょう。
package gql
import (
"strconv"
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/language/ast"
"{your-project}/model"
)
var (
userIDScalarType = graphql.NewScalar(graphql.ScalarConfig{
Name: "userID",
Serialize: func(value interface{}) interface{} {
switch value := value.(type) {
case model.UserID:
return string(value)
case *model.UserID:
v := *value
return string(v)
default:
return ""
}
},
ParseValue: func(value interface{}) interface{} {
switch value := value.(type) {
case string:
return model.UserID(value)
case *string:
v := *value
return model.UserID(v)
default:
return model.UserID("")
}
},
ParseLiteral: func(valueAST ast.Value) interface{} {
switch valueAST := valueAST.(type) {
case *ast.StringValue:
return model.UserID(valueAST.Value)
default:
return model.UserID("")
}
},
})
unixtimeScalarType = graphql.NewScalar(graphql.ScalarConfig{
Name: "UnixTime",
Serialize: func(value interface{}) interface{} {
switch value := value.(type) {
case model.UnixTime:
return int64(value)
case *model.UnixTime:
v := *value
return int64(v)
default:
return ""
}
},
ParseValue: func(value interface{}) interface{} {
switch value := value.(type) {
case int64:
return model.UnixTime(value)
case *int64:
v := *value
return model.UnixTime(v)
default:
return model.UnixTime(0)
}
},
ParseLiteral: func(valueAST ast.Value) interface{} {
switch valueAST := valueAST.(type) {
case *ast.IntValue:
if intValue, err := strconv.Atoi(valueAST.Value); err == nil {
return model.UnixTime(intValue)
}
return model.UnixTime(0)
default:
return model.UnixTime(0)
}
},
})
)
まずUserID
型をスカラーとして実装したuserIDScalarType
について見ていきましょう。
userIDScalarType
はオブジェクト型のリゾルバがどんな値を返したかを元にSeriarize
、ParseValue
、ParseLiteral
を実行します。
- もし
UserID
型がリゾルバから返ってきた場合、Seriarize
関数が値をstring
型にキャストしていることがわかります。 - 逆に
string
型が返された場合、ParseValue
関数がUserID
型に返り値をキャストしてます。 -
ParseLiteral
関数はクエリ解析器です。クエリの値がstring
っぽいとき、UserID
型にキャストします。
こうしてstring
を基に独自に定義した型をGraphQLスカラーとして定義します。
次にUnixTime
型をスカラーとして実装したunixTimeScalarType
を見てください。UnixTime
型はint
型の拡張ですがUserID
と同様の実装です。違う点はParseLiteral
関数内でstrconv.Atoi
関数を使ってint
型に変換しているところだけです。
列挙型(Enum)を定義
UserStatus
型をスキーマで定義したUserStatus
として定義しましょう。
スキーマのUserStatus
は
- INACTIVE :無効なユーザー
- ACTIVE :有効なユーザー
- FROZEN :凍結されたユーザー
の三つの値を持っているとします。
package gql
import (
"github.com/graphql-go/graphql"
"{your-project}/model"
)
var (
userStatusEnumType = graphql.NewEnum(graphql.EnumConfig{
Name: "userStatus",
Values: graphql.EnumValueConfigMap{
"INACTIVE": &graphql.EnumValueConfig{
Value: model.Inactive,
},
"ACTIVE": &graphql.EnumValueConfig{
Value: model.Active,
},
"FROZEN": &graphql.EnumValueConfig{
Value: model.Frozen,
},
},
})
)
レスポンスの際の形式はgraphql.EnumValueConfigMap
のフィールド名を値とした文字列データです。(例えば{"status": "INACTIVE"}
のようにレスポンスされます。)
オブジェクトを定義
それではここまで作成したスカラー、列挙型を使ってUser
型をスキーマ定義のUser
オブジェクトとして定義しましょう。
package gql
import (
"errors"
"github.com/graphql-go/graphql"
"{your-project}/model"
)
var (
userObjType = graphql.NewObject(graphql.ObjectConfig{
Name: "user",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: userIDScalarType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.ID, nil
}
return model.UserID(""), errors.New("type assertion failed")
},
},
"createdAt": &graphql.Field{
Type: unixtimeScalarType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.CreatedAt, nil
}
return model.UnixTime(0), errors.New("type assertion failed")
},
},
"updatedAt": &graphql.Field{
Type: unixtimeScalarType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.UpdatedAt, nil
}
return model.UnixTime(0), errors.New("type assertion failed")
},
},
"deletedAt": &graphql.Field{
Type: unixtimeScalarType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
if user.Timestamp.IsNil() {
return model.UnixTime(0), nil
}
return user.DeletedAt, nil
}
return model.UnixTime(0), errors.New("type assertion failed")
},
},
"screenID": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.ScreenID, nil
}
return "", errors.New("type assertion failed")
},
},
"email": &graphql.Field{
Type: emailScalarType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.Email, nil
}
return "", errors.New("type assertion failed")
},
},
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.Name, nil
}
return "", errors.New("type assertion failed")
},
},
"status": &graphql.Field{
Type: userStatusEnumType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.Status, nil
}
return model.Inactive, errors.New("type assertion failed")
},
},
},
})
)
それでは"status"
フィールドを例にスキーマ実装を理解しましょう。
"status": &graphql.Field{
Type: userStatusEnumType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if user, ok := p.Source.(model.User); ok {
return user.Status, nil
}
return model.Inactive, errors.New("type assertion failed")
},
},
オブジェクト内の各フィールドの型などの詳細はgraphql.Field
に定義していきます。
Type
フィールド
Type
フィールドはフィールドの型を定義します。後述のリゾルバ関数で返される値にしたがって指定します。
ここまでuserIDScalarType
、unixtimeScalarType
、userStatusEnumType
を定義しましたがライブラリ側では標準で以下の型が定義されてます。
-
graphql.Int :
int
やint64
など整数データが返される場合 -
graphql.Float :
float32
やfloat64
など小数データが返される場合 -
graphql.Boolean :
bool
が返される場合 -
graphql.String :
string
など文字列データが返される場合 -
graphql.ID :
string
もしくはint
など文字列・整数データが返されるIDフィールドの場合
これらの定義済み型情報もまた、NewScalar
関数で実装されています。
Resolver
フィールド
Resolver
フィールドにはGoの値をスキーマに合うよう取得したり加工したりするリゾルバ関数を書いていきます。
引数の(p graphql.ResolveParams)
には前段のリゾルバで返された値やcontext
が仕込まれています。
p.Source
には前段のリゾルバで返された値がinterface{}
型として仕込まれています。型アサーションでUser
型かどうか判定し、User
型の場合はUser
型の該当フィールドとnil
、そうでない場合は空値とerror
を返します。
Type
で指定した型である値をリゾルバ関数でreturnしてあげることであとは型の関数(graphql.Int
やuserStatusEnumType
など)がよしなにしてくれます。
お疲れ様でした
今回はオブジェクト、スカラー、列挙型の定義を書いてみました。次回は投稿オブジェクトを作成し、ユーザーオブジェクトと投稿オブジェクトを紐づけてみます。
シリーズのリスト
第0回 イントロダクション
第1回 スカラー、オブジェクト、列挙型の実装 👈
第2回 オブジェクトとオブジェクトを関連付ける(執筆中)
第3回 Query(予定)
第4回 Mutation(予定)
自己紹介
-
僕の実際の実装では
Email
も本人以外にはレスポンスしないようになっていますが今回は簡略化のために他のフィールド同様にしておきます。 ↩︎
Discussion