😤

【graphql-go】Goで始めるGraphQL その1【オブジェクト/スカラー/列挙型の実装】

2021/03/31に公開

前回はGraphQLの考え方をなぞった上で扱う/扱わない話題と使用する言語・ライブラリ等のバージョンについて確認しました。

今回はオブジェクト、スカラー、列挙型の定義について書いていきます。

例えばユーザー情報をこんな構造体で定義していたとします。

model/user.go
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オブジェクトのスキーマは以下のように定義します。

user.graphql
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型のスカラーを定義していきましょう。

gql/user.scalar.go
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はオブジェクト型のリゾルバがどんな値を返したかを元にSeriarizeParseValueParseLiteralを実行します。

  • もし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 :凍結されたユーザー

の三つの値を持っているとします。

gql/user.enum.go
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オブジェクトとして定義しましょう。

gql/user.object.go
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フィールドはフィールドの型を定義します。後述のリゾルバ関数で返される値にしたがって指定します。

ここまでuserIDScalarTypeunixtimeScalarTypeuserStatusEnumTypeを定義しましたがライブラリ側では標準で以下の型が定義されてます。

  • graphql.Int : intint64など整数データが返される場合
  • graphql.Float : float32float64など小数データが返される場合
  • 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.IntuserStatusEnumTypeなど)がよしなにしてくれます。

お疲れ様でした

今回はオブジェクト、スカラー、列挙型の定義を書いてみました。次回は投稿オブジェクトを作成し、ユーザーオブジェクトと投稿オブジェクトを紐づけてみます。

シリーズのリスト

第0回 イントロダクション
第1回 スカラー、オブジェクト、列挙型の実装 👈
第2回 オブジェクトとオブジェクトを関連付ける(執筆中)
第3回 Query(予定)
第4回 Mutation(予定)

自己紹介

脚注
  1. 僕の実際の実装ではEmailも本人以外にはレスポンスしないようになっていますが今回は簡略化のために他のフィールド同様にしておきます。 ↩︎

Discussion