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

はじめに

Advent Calendar 2021のDay21。 今回は、カレンダーの更新削除を実装していきます。

Hasura Action

HasuraConsoleでActionを定義します。 HasuraConsoleのEditorを使ってGraphQLのクエリを定義します カレンダー、参加者の作成・更新のアクションを追加していきます。 削除は、HasuraのMutationをそのまま使おうと思いますので、Nuxtの実装だけになります。次回以降対応します。 エクスポートしたデータをみるとイメージができるかと思います。

hasura metadata export
  • metadata/actions.graphql
type Mutation {
  actionCreateCalendar(
    in: ActionCreateCalendarInput!
  ): ActionCreateCalendarOutput
}

type Mutation {
  actionCreateParticipant(
    in: ActionCreateParticipantInput!
  ): ActionCreateParticipantOutput
}

type Mutation {
  actionUpdateCalendar(
    in: ActionUpdateCalendarInput!
  ): ActionUpdateCalendarOutput
}

type Mutation {
  actionUpdateParticipant(
    in: ActionUpdateParticipantInput!
  ): ActionUpdateParticipantOutput
}

input ActionCreateCalendarInput {
  name: String
}

input ActionUpdateCalendarInput {
  id: Int!
  name: String
}

input ActionCreateParticipantInput {
  calendar_id: Int!
  day: Int!
  article_title: String
  article_url: String
}

input ActionUpdateParticipantInput {
  calendar_id: Int!
  day: Int!
  article_title: String
  article_url: String
}

type ActionCreateCalendarOutput {
  id: Int
  name: String
  uid: String
  created_at: Time
  updated_at: Time
}

type ActionUpdateCalendarOutput {
  id: Int
  name: String
  uid: String
  created_at: Time
  updated_at: Time
}

type ActionCreateParticipantOutput {
  calendar_id: Int!
  day: Int!
  uid: String
  article_title: String
  article_url: String
  created_at: Time
  updated_at: Time
}

type ActionUpdateParticipantOutput {
  calendar_id: Int!
  day: Int!
  uid: String
  article_title: String
  article_url: String
  created_at: Time
  updated_at: Time
}

scalar Map

scalar Time

scalar Void

permissionにUserを指定しています。Actionを実装したら、Hasuraが提供するMutationのInsertやUpdateは許可設定を拒否するようにするとよいです。

  • metadata/actions.yaml
actions:
- name: actionCreateCalendar
  definition:
    kind: synchronous
    handler: '{{ACTION_BASE_URL}}/events/hasura/action'
  permissions:
  - role: user
- name: actionCreateParticipant
  definition:
    kind: synchronous
    handler: '{{ACTION_BASE_URL}}/events/hasura/action'
  permissions:
  - role: user
- name: actionUpdateCalendar
  definition:
    kind: synchronous
    handler: '{{ACTION_BASE_URL}}/events/hasura/action'
  permissions:
  - role: user
- name: actionUpdateParticipant
  definition:
    kind: synchronous
    handler: '{{ACTION_BASE_URL}}/events/hasura/action'
  permissions:
  - role: user
custom_types:
  enums: []
  input_objects:
  - name: ActionCreateCalendarInput
  - name: ActionUpdateCalendarInput
  - name: ActionCreateParticipantInput
  - name: ActionUpdateParticipantInput
  objects:
  - name: ActionCreateCalendarOutput
  - name: ActionUpdateCalendarOutput
  - name: ActionCreateParticipantOutput
  - name: ActionUpdateParticipantOutput
  scalars:
  - name: Map
  - name: Time
  - name: Void

Action Handlerを実装する(途中)

HasuraのAction HandlerをGoで実装します。 HasuraのCLIをつかってCodegenできればよいのですが、バグなのかきちんと動作してくれないので、自分で実装します。Hasura Consoleに表示されるCodegenの内容を参考にします。エンドポイントは1箇所で実装しています。

mkdir goapp
cd goapp 
go mod init
  • goapp/main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    port := os.Getenv("PORT")
    mux := http.NewServeMux()
    mux.HandleFunc("/events/hasura/action", handler)

    err := http.ListenAndServe(fmt.Sprintf(":%s", port), mux)
    log.Fatal(err)
}

ハンドラー部分は、Day22で追加実装していきます。Hasura Actionの説明にあるようにデータバリデーションやビジネスロジックなどを実装する用途で使用します。

参考

https://hasura.io/docs/latest/graphql/core/actions/index.html#what-are-actions

  • goapp/handler.go
package main

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"

    "github.com/mitchellh/mapstructure"
)

type Action struct {
    Name string `json:"name"`
}
type ActionPayload struct {
    Action           Action                 `json:"action"`
    SessionVariables map[string]interface{} `json:"session_variables"`
    Input            interface{}            `json:"input"`
}

type GraphQLError struct {
    Message string `json:"message"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    log.Println("Received request")

    // set the response header as JSON
    w.Header().Set("Content-Type", "application/json")

    // read request body
    reqBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        log.Println(err)
        http.Error(w, "invalid payload", http.StatusBadRequest)
        return
    }

    // parse the body as action payload
    var actionPayload ActionPayload
    err = json.Unmarshal(reqBody, &actionPayload)
    if err != nil {
        log.Println(err)
        http.Error(w, "invalid payload", http.StatusBadRequest)
        return
    }

    // Send the request params to the Action's generated handler function
    var result interface{}
    switch actionPayload.Action.Name {
    case "actionCreateCalendar":
        args := actionCreateCalendarArgs{}
        if err := mapstructure.Decode(actionPayload.Input, &args); err != nil {
            log.Println(err)
            http.Error(w, "invalid payload", http.StatusBadRequest)
            return
        }
        result, err = actionCreateCalendar(args)
    case "actionUpdateCalendar":
        // TODO: implement
    case "actionCreateParticipant":
        // TODO: implement
    case "actionUpdateParticipant":
        // TODO: implement
    }

    // throw if an error happens
    if err != nil {
        errorObject := GraphQLError{
            Message: err.Error(),
        }
        errorBody, _ := json.Marshal(errorObject)
        w.WriteHeader(http.StatusBadRequest)
        w.Write(errorBody)
        return
    }

    // Write the response as JSON
    data, _ := json.Marshal(result)
    w.Write(data)

}

// Auto-generated function that takes the Action parameters and must return it's response type
func actionCreateCalendar(args actionCreateCalendarArgs) (response ActionCreateCalendarOutput, err error) {
    // TODO: implement
    response = ActionCreateCalendarOutput{}
    return response, nil
}
func actionCreateParticipant(args actionCreateParticipantArgs) (response ActionCreateParticipantOutput, err error) {
    // TODO: implement
    response = ActionCreateParticipantOutput{}
    return response, nil
}
func actionUpdateCalendar(args actionUpdateCalendarArgs) (response ActionUpdateCalendarOutput, err error) {
    // TODO: implement
    response = ActionUpdateCalendarOutput{}
    return response, nil
}
func actionUpdateParticipant(args actionUpdateParticipantArgs) (response ActionUpdateParticipantOutput, err error) {
    // TODO: implement
    response = ActionUpdateParticipantOutput{}
    return response, nil
}

型情報もHasuraConsoleに表示されますが、JSONタグがなかったりするので、この辺は、Codegenほしいなと思いますね。Actionの数が増えてくると辛くなります。

  • goapp/types.go
package main

type Map string

type Time string

type Void string

type ActionCreateCalendarInput struct {
    Name *string `json:"name,omitempty"`
}

type ActionUpdateCalendarInput struct {
    Id   int     `json:"id"`
    Name *string `json:"name,omitempty"`
}

type ActionCreateParticipantInput struct {
    Calendar_id   int     `json:"calendar_id"`
    Day           int     `json:"day"`
    Article_title *string `json:"article_title,omitempty"`
    Article_url   *string `json:"article_url,omitempty"`
}

type ActionUpdateParticipantInput struct {
    Calendar_id   int     `json:"calendar_id"`
    Day           int     `json:"day"`
    Article_title *string `json:"article_title,omitempty"`
    Article_url   *string `json:"article_url,omitempty"`
}

type ActionCreateCalendarOutput struct {
    Id         *int    `json:"id,omitempty"`
    Name       *string `json:"name,omitempty"`
    Uid        *string `json:"uid,omitempty"`
    Created_at *Time   `json:"created_at,omitempty"`
    Updated_at *Time   `json:"updated_at,omitempty"`
}

type ActionUpdateCalendarOutput struct {
    Id         *int    `json:"id,omitempty"`
    Name       *string `json:"name,omitempty"`
    Uid        *string `json:"uid,omitempty"`
    Created_at *Time   `json:"created_at,omitempty"`
    Updated_at *Time   `json:"updated_at,omitempty"`
}

type ActionCreateParticipantOutput struct {
    Calendar_id   int     `json:"calendar_id,omitempty"`
    Day           int     `json:"day,omitempty"`
    Uid           *string `json:"uid,omitempty"`
    Article_title *string `json:"article_title,omitempty"`
    Article_url   *string `json:"article_url,omitempty"`
    Created_at    *Time   `json:"created_at,omitempty"`
    Updated_at    *Time   `json:"updated_at,omitempty"`
}

type ActionUpdateParticipantOutput struct {
    Calendar_id   int     `json:"calendar_id,omitempty"`
    Day           int     `json:"day,omitempty"`
    Uid           *string `json:"uid,omitempty"`
    Article_title *string `json:"article_title,omitempty"`
    Article_url   *string `json:"article_url,omitempty"`
    Created_at    *Time   `json:"created_at,omitempty"`
    Updated_at    *Time   `json:"updated_at,omitempty"`
}

type Mutation struct {
    ActionCreateCalendar    *ActionCreateCalendarOutput
    ActionCreateParticipant *ActionCreateParticipantOutput
    ActionUpdateCalendar    *ActionUpdateCalendarOutput
    ActionUpdateParticipant *ActionUpdateParticipantOutput
}

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

type actionCreateParticipantArgs struct {
    In ActionCreateParticipantInput `json:"in"`
}

type actionUpdateCalendarArgs struct {
    In ActionUpdateCalendarInput `json:"in"`
}

type actionUpdateParticipantArgs struct {
    In ActionUpdateParticipantInput `json:"in"`
}

これもCloudRunで動かそうと思いますので、Dockerfileを準備しておきます。

FROM golang:1.17-bullseye as build

WORKDIR /go/src/app
ADD . /go/src/app

RUN go get -d -v ./...

RUN go build -o /go/bin/app

FROM gcr.io/distroless/base-debian11
COPY --from=build /go/bin/app /
CMD ["/app"]

まとめ

今回は、カレンダーの更新削除を実装しました。 次回も引き続き、カレンダーの更新削除を実装していきます。 主にハンドラー部分の実装をします。