[Advent Calendar 2021][Day22]カレンダーの更新削除を実装する(2)

はじめに

Advent Calendar 2021のDay22。 今回もカレンダーの更新削除を実装する続きです。

GraphQL Client

GoでHasuraGraphQLを呼び出すので、GraphQL Clientを選びます。 ピックアップした候補は3つです。

自動生成はしてくれないので、型を自分で作る必要がある。 呼び出しは簡単にできるので学習コストは低い。

自動生成できる。使ったことあり。

自動生成できる。使ったことはない。

Nuxt側と同様に自動生成できるライブラリが良いなと思います。今回は、まだ使ったことのないgqlgencを使おうと思います。

gqlgenc setup

go install github.com/Yamashou/gqlgenc
go get github.com/Yamashou/gqlgenc
go get github.com/99designs/gqlgen

gqlgencの設定ファイルを記述します。

  • .gqlgenc.yml
model:
  filename: ./gen/models_gen.go
client:
  filename: ./gen/client.go
models:
  Int:
    model: github.com/99designs/gqlgen/graphql.Int64
  Date:
    model: github.com/99designs/gqlgen/graphql.Time
endpoint:
  url: http://localhost:8080/v1/graphql
  headers:
    x-hasura-admin-secret: myadminsecretkey
query:
  - "./query/*.graphql"

Mutationを定義します。Actionが実行された時に下記のMutationをHasuraに送信します。

  • query/mutation.graphql
mutation InsertCalendarMutation($name: String!) {
  insert_calendars_one(object: {name: $name}) {
    id
    name
    uid
    created_at
    updated_at
  }
}
mutation UpdateCalendarMutation($id: Int!, $name: String = "") {
  update_calendars_by_pk(pk_columns: {id: $id}, _set: {name: $name}) {
    uid
    name
    id
    created_at
    updated_at
  }
}

コード生成してきます。指定したフォルダにコードが生成されればOKです。

gqlgenc

gqlgencで生成されたGraphQL Clientを使う

graphqlのクエリは、HasuraからAuthorizationヘッダを送ってもらって、そのヘッダをそのままつけて送ります。そうすることで、認可ユーザとしてHasuraを使うことができます。Admin権限はできるだけ使わないほうがセキュリティ的によいかと思います。

  • gqlclient.go
package main

import (
    "fmt"
    "net/http"

    "github.com/cloudandbuild/Advent-Calendar-2021/goapp/gen"
)

func GetGraphqlClient(token string) *gen.Client {
    baseURL := getenv("HASURA_BASE_URL", "http://localhost:8080/v1/graphql")
    return NewGraphqlClient(
        &GraphqlClientComponent{},
        &GraphqlClientConfig{
            BearerToken: token,
            BaseURL:     baseURL,
        },
    )
}

type GraphqlClientComponent struct {
}
type GraphqlClientConfig struct {
    BearerToken string
    BaseURL     string
}

func NewGraphqlClient(c *GraphqlClientComponent, cfg *GraphqlClientConfig) *gen.Client {
    authHeader := func(req *http.Request) {
        req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.BearerToken))
    }
    return gen.NewClient(http.DefaultClient, cfg.BaseURL, authHeader)
}

記事のタイトルは、更新削除とありますが、作成部分もActionに置き換えてバリデーションをしたほうが良いので、今回は試しに作成部分を置き換えていきます。

  • handler.goのSwitch部分
    switch actionPayload.Action.Name {
    case "actionCreateCalendar":
        args := actionCreateCalendarArgs{}
        err := mapstructure.Decode(actionPayload.Input, &args)
        if err != nil {
            break
        }
        err = args.Validate()
        if err != nil {
            break
        }
        result, err = actionCreateCalendar(ctx, args, token)

型定義はがっつり変更しています。Hasuraから送られてくるInput情報を生成されたものに変えて、Validationの定義を追加しました。Hasura Actionを使わない場合は、こまかなビジネスロジックやバリデーションを行うことが出来ませんが、Actionだとできるようになります。

  • types.go
package main

import (
    "github.com/cloudandbuild/Advent-Calendar-2021/goapp/gen"
    validation "github.com/go-ozzo/ozzo-validation"
)

type actionCreateCalendarArgs struct {
    In gen.ActionCreateCalendarInput `json:"in"`
}

func (r *actionCreateCalendarArgs) Validate() error {
    p := &r.In
    return validation.ValidateStruct(p,
        validation.Field(
            &p.Name,
            validation.Length(1, 20),
        ),
    )
}

変換する部分があるので、つらつらと書いていますが、InsertCalendarMutationを呼び出すだけのシンプルなコードです。

  • handler.goのアクション実行部分
func actionCreateCalendar(ctx context.Context, args actionCreateCalendarArgs, token string) (response *gen.ActionCreateCalendarOutput, err error) {
    cli := GetGraphqlClient(token)
    res, err := cli.InsertCalendarMutation(context.Background(), *args.In.Name)
    if err != nil {
        return response, err
    }
    return &gen.ActionCreateCalendarOutput{
        ID:        &res.InsertCalendarsOne.ID,
        Name:      &res.InsertCalendarsOne.Name,
        UID:       &res.InsertCalendarsOne.UID,
        CreatedAt: mustParseTime(res.InsertCalendarsOne.CreatedAt),
        UpdatedAt: mustParseTime(res.InsertCalendarsOne.UpdatedAt),
    }, nil
}

nuxt側のInsertMutationをActionに置き換える。

mutation ActionCreateCalendarMutation($name: String = "") {
  actionCreateCalendar(in: {name: $name}) {
    created_at
    id
    name
    uid
    updated_at
  }
}

コード生成します。

yarn graphql-codegen

差分だと分かりづらいのでScript部分の全コード。urqlに渡すMutation定義をActionに置き換えています。もともとエラーハンドリング出来ていなかったのですが、追加してみました。サーバーサイドで出力されたバリデーションエラーのメッセージを表示に使うようできます。

  • pages/index.vue
<script setup lang="ts">
import {
    ListCalendarsQueryDocument,
    ActionCreateCalendarMutationDocument,
    ActionCreateCalendarMutationMutationVariables
} from "~/gql/queries/calendar";
import { useClientHandle } from "@urql/vue";
import { useNuxtApp } from '#app'
const urql = useClientHandle();
const queryResult = await urql.useQuery({ query: ListCalendarsQueryDocument })
const { data, fetching } = await queryResult

const nuxtApp = useNuxtApp()
const user = useState('user')

const errorMessage = ref('')
const calendarName = ref('')
const createCalendarResult = urql.useMutation(ActionCreateCalendarMutationDocument);
const insert = async () => {
    const variables: ActionCreateCalendarMutationMutationVariables = {
        name: calendarName.value
    };
    const { error } = await createCalendarResult.executeMutation(variables)

    return error
}
const openModal = ref(false)
const onOkClick = async () => {
    const error = await insert()
    if (error) {
        console.error(error)
        errorMessage.value = error.message
        return
    }
    await queryResult.executeQuery({
        requestPolicy: 'network-only',
    })
    openModal.value = false
}
</script>

まとめ

gqlgencはこれまで触ってきたGraphQLクライアントと比べてかなり使いやすい印象です。 次回も、カレンダーの更新削除を実装する続きをしていきます。