Chapter 10無料公開

✅ユーザの入力値は値オブジェクトである

たった
たった
2021.06.25に更新
アプリケーションコード
src
├── middleware     ... 認証・認可とGraphQLのコンテキスト
├── domain         ... ビジネスロジックの共通化
├── usecases       ... アプリケーションロジック
├── infrastructure ... 外部サービスとのやりとり
├── entities       ... エンティティとGraphQLのフィールド
>├── resolvers      ... GraphQLのリゾルバー
└── inversify.config.ts ... 依存性の注入(以下、DI)の設定

このチャプターで使用するライブラリ

  • TypeGraphQL
  • class-validator

概要

ほとんどのWebアプリケーションではユーザの入力が発生します。そして入力値の扱いは単純なものではありません。

このチャプターでは、ユーザの入力値を「値オブジェクト[1]」とすることで入力値に怯えることなく実装できるようにします。

では実際にコードを見ていきましょう。
今回は以下の仕様を追加することにします。

  • ユーザを作成する
    • リクエストしてきたユーザを新しいユーザとして登録する

実装

値オブジェクトにしない場合

まずは入力値を値オブジェクトにしない場合を見ていきましょう。
InputTypeを手なりで実装してみます。

be/src/resolvers/types/UserInput.ts
import { Field, InputType } from 'type-graphql'

@InputType()
export class UserCreateInput {
  @Field()
  name: string

  @Field()
  email: string
}

当然、このままの実装でもresolverで使うことができます。

be/src/resolvers/userResolver.ts
import { UserCreateInput } from 'src/resolvers/types/UserInput'

@Resolver(() => User)
export class UserResolver {
  constructor (
    @inject(UserUsecase) usecase: UserUsecase
  ) {}

  @Mutation(() => User)
  async userCreate(
    @Ctx() ctx: IContext,
    @Arg('input') input: UserCreateInput
  ): Promise<User> {
    return await this.usecase.addUsers([input])
  }
}

ここで質問が2つあります。

  1. ユーザは必ず正しい値を入力してくれるでしょうか?
  2. プログラマは絶対に入力値を不正な値に書き換えないでしょうか?

これらの質問の答えはNoです。
こうなると、入力値を使うUsecaseはバリデーションを考える必要が出てきます。本来実現したい仕様以外のことを考えなくてはなりません。

入力値を値オブジェクトにすることで、上記の質問の答えをYesにできます。
値オブジェクトは以下の2つの利点を持っています。

  1. 値オブジェクトは、値の「正しさ」を表明・保証できる
  2. 値オブジェクトは、ライフサイクルの中で「不変」であることを表明・保証できる

この2つの利点によって本来実現したい仕様に集中できるようになります。
では具体的なコードを見ていきましょう。

値オブジェクトにする場合

このInputTypeを値オブジェクトにするために、制約とふるまいを与えます。
まずは、Usecaseの引数を値オブジェクトのインターフェースとして定義します。

be/src/usecases/types/UserType.ts
export interface UserAddVo {
  readonly name: string
  readonly email: string
}

そしてInputTypeはそのインターフェースを実装します。
これで入力値はImmutableになり、Usecaseの引数であることが保証されました。

be/src/resolvers/types/UserInput.ts
+ import { UserAddVo } from 'src/usecases/types/UserType'

@InputType()
- export class UserCreateInput {
+ export class UserCreateInput implements UserAddVo {
  @Field()
-  name: string
+  readonly name: string

  @Field()
-  email: string
+  readonly email: string
}

ただ、これだけではユーザの入力値が正しいことを保証できていません。
バリデータとふるまいを実装して、入力値を保証しましょう。

以下のように、抽象クラスに値オブジェクトのふるまいを共通化するのも良いと思います。

be/src/resolvers/types/helper.ts
import { UserInputError } from 'apollo-server-express'
import { Field, ObjectType, InputType } from 'type-graphql'
import { validate } from 'class-validator'

@InputType({ isAbstract: true })
export abstract class InputValueObject {
  async validate(): Promise<void> {
    for (const error of await validate(this)) {
      throw new UserInputError(JSON.stringify(error.constraints))
    }
  }
}

バリデータと継承を実装しましょう。

be/src/resolvers/types/UserInput.ts
+ import { IsEmail } from 'class-validator' 
+ import { InputValueObject } from 'src/resolvers/types/helper'

@InputType()
- export class UserCreateInput implements UserAddVo {
+ export class UserCreateInput extends InputValueObject implements UserAddVo {
  @Field()
  readonly name: string

  @Field()
+  @IsEmail()
  readonly email: string
}

Resolverではこういう形で使います。

be/src/resolvers/userResolver.ts
@Resolver(() => User)
export class UserResolver {
  constructor (
    @inject(UserUsecase) usecase: UserUsecase
  ) {}

  @Mutation(() => User)
  async userCreate(
    @Ctx() ctx: IContext,
    @Arg('input') input: UserCreateInput
  ): Promise<User> {
+    await input.validate() // もし入力間違いがあればエラーをthrow
    return await this.usecase.addUsers([input])
  }
}

これで入力値が不変であり、正しい入力であると保証されました。
Usecaseでは本来実現したい仕様に注力してコーディングすることができます。

脚注
  1. ドメイン駆動設計でいうValue Object ↩︎