Series: gqlgen
Tags: Go Golang GraphQL

【Go】gqlgen directive で複合バリデーション

gqlgen の directive で複合バリデーションする。

開発環境

  • go1.18 linux/amd64
  • github.com/99designs/gqlgen v0.17.2

本文

『公開オプションが「パスワード」のときだけパスワードを必須入力にし、それ以外の場合はパスワードの入力を禁止する』という複合バリデーションをしたかったので directive で実装した。

前回以前からの続きでやっており、下記が完成品。

https://gitlab.com/k1350/daybreak_sample/-/tree/graphql_mutation_validation_sample

Directive の設計

Laravel を参考にして “required_if” という名前にして「パスワード」側に directive をつけたかったが、仕組み的に無理なので、今回は “requireIf” という名前にして「公開オプション」側に directive をつけた。

関係あるフィールドだけ抜き出すとこう。

input CreateBlogInput {
    publishOption: PublishOption! @publishOptionValue(requireIf: "password,PASSWORD")
    password: String
}

directive @publishOptionValue(
    requireIf: String
) on INPUT_FIELD_DEFINITION

“requireIf” は「必須入力にしたいフィールド名」と「必須入力になる条件」をカンマ区切りで指定するようにした。

このルールは PublishOption という特定の enum でしか使わないので、実装を簡単にするために PublishOption 専用にした。

バリデーション実装

gqlgen 使用時、INPUT_FIELD_DIFINITION の directive ならば obj に入力値全体が格納されている。

obj を map[string]interface{} にキャストしてキーの存在をチェックする。

func PublishOptionValue(ctx context.Context, obj interface{}, next graphql.Resolver, requireIf *string) (res interface{}, err error) {
	v, err := next(ctx)
	if err != nil {
		return nil, err
	}

	if requireIf != nil {
		constraints := strings.Split(*requireIf, ",")
		if len(constraints) != 2 {
			return nil, fmt.Errorf("RequireIf directive should be '[require field name],[require condition value]")
		}
        m := obj.(map[string]interface{})
		if v.(model.PublishOption).String() == constraints[1] {
			if _, ok := m[constraints[0]]; !ok {
				return nil, fmt.Errorf(constraints[0] + ": is required when publishOption is " + constraints[1])
			}
		} else {
			if _, ok := m[constraints[0]]; ok {
				return nil, fmt.Errorf(constraints[0] + ": is prohibited when publishOption is not " + constraints[1])
			}
		}
	}

	return v, nil
}

これで複合バリデーションができた。

なおパスワードが null の場合はこれでは通ってしまうのだが、それはパスワード単独の値チェックでやるほうがいいかと思ったので無視している。

おまけ:パスワードの値バリデーション

v == nil では null 判定が不足するということを知った。

参考: 絶対ハマる、不思議なnil

func PasswordValue(ctx context.Context, obj interface{}, next graphql.Resolver, isValidPassword *bool) (res interface{}, err error) {
	v, err := next(ctx)
	if err != nil {
		return nil, err
	}

	if isValidPassword != nil && *isValidPassword {
		if (v == nil) || reflect.ValueOf(v).IsNil() {
			return nil, fmt.Errorf("password is not valid.")
		}
		value := v.(*string)
		if !passwordPattern.MatchString(*value) {
			return nil, fmt.Errorf("password is not valid.")
		}
	}

	return v, nil
}

以上