Nest.js学習にっき2

前回に引き続き、notiz-dev/nestjs-prisma-starterで始める Nest.js 学習記録にっきだよ。

※画像引用:https://nestjs.com/

今回、学習したいことを列挙しておく。

  • スキーマの description を書いてみる
  • Mutation を作ってみる
  • ArgsTypeで入力の検証を行う

一方で、長いから下記は次回にまわすよ。

  • Serviceを実装して、prisma でDBとやり取りする

早速やっていこう。

スキーマの description を書いてみる

各種デコレータに「description」オプションが用意されている。そこに説明テキストを渡せば、schema.graphql にコメントとして書き込まれるみたいだ。やってみよう。

// ---- src/bbs/models/bbs.model.ts ----
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { BbsThread } from './bbs-thread.model'

@ObjectType({
  description: '掲示板',
})
export class Bbs {
  @Field((type) => ID, {
    description: '掲示板のID',
  })
  bbsId: string

  @Field((type) => String, {
    description: '掲示板の名前',
  })
  bbsName: string

  @Field((type) => [BbsThread], {
    nullable: true,
    description: '掲示板に立てられたスレッドのリスト',
  })
  threads: BbsThread[]
}
// ---- src/bbs/models/bbs.resolver.ts ----
import {
  Args,
  ID,
  Parent,
  Query,
  ResolveField,
  Resolver,
} from '@nestjs/graphql'
import { Bbs } from './models/bbs.model'

@Resolver((of) => Bbs)
export class BbsResolver {
  constructor() {
    // 今後Serviceを実装してコンストラクタに追加する
  }

  @Query((returns) => Bbs, {
    description: '掲示板をIDで取得する',
  })
  async bbs(
    @Args('id', { type: () => ID, description: '掲示板のID' }) id: string
  ) {
    return {
      bbsId: id,
      bbsName: 'bbsName',
    }
  }

  @ResolveField()
  async threads(@Parent() bbs: Bbs) {
    // Parentの情報を元に、今後Serviceを使って情報を取得する
    return [
      {
        threadId: 'threadId',
        threadName: 'threadName',
        description: 'description',
      },
    ]
  }
}

ここまで書いた状態で nest start --watch を起動しっぱなしか、あるいは nest start を再実行すれば、該当のスキーマにコメントが正しく書き込まれる。やってみたけど、無事成功した。

Mutation を作ってみる

Mutation は引数を Input type(入力型)で作りたいね。

そういうわけだから、まずは掲示板、ユーザー、スレッド、レスそれぞれの Create 用 Input type を実装するよ。

// ---- src/bbs/dto/create-bbs.input.ts ----
import { IsNotEmpty, Length } from 'class-validator'
import { InputType, Field } from '@nestjs/graphql'

@InputType({
  description: '新しい掲示板を立てる',
})
export class CreateBbsInput {
  @Field({
    description: '掲示板の名前',
  })
  @IsNotEmpty()
  @Length(1, 100)
  bbsName: string
}
// ---- src/bbs/dto/create-bbs-user.input.ts ----
import { IsNotEmpty, Length } from 'class-validator'
import { InputType, Field } from '@nestjs/graphql'

@InputType({
  description: '新しいユーザーを作成する',
})
export class CreateBbsUserInput {
  @Field({
    description: 'ユーザーの名前',
  })
  @IsNotEmpty()
  @Length(1, 100)
  userName: string
}
// ---- src/bbs/dto/create-bbs-thread.input.ts ----
import { IsNotEmpty, IsUUID, Length } from 'class-validator'
import { InputType, Field } from '@nestjs/graphql'

@InputType({
  description: '新しいスレッドを立てる',
})
export class CreateBbsThreadInput {
  @Field({
    description: 'スレッドの名前',
  })
  @IsNotEmpty()
  @Length(1, 100)
  threadName: string

  @Field({
    description: 'スレッドの説明',
  })
  @IsNotEmpty()
  @Length(1, 2000)
  description: string

  @Field({
    description: 'スレッドを立てる掲示板のID',
  })
  @IsNotEmpty()
  @IsUUID()
  bbsId: string
}
// ---- src/bbs/dto/create-bbs-thread-response.input.ts ----
import { IsNotEmpty, IsUUID, Length } from 'class-validator'
import { InputType, Field } from '@nestjs/graphql'

@InputType({
  description: '新しいレスを作成する',
})
export class CreateBbsThreadResponseInput {
  @Field({
    description: 'レスの内容',
  })
  @IsNotEmpty()
  @Length(1, 2000)
  content: string

  @Field({
    description: 'レスを作成するユーザーのID',
  })
  @IsNotEmpty()
  @IsUUID()
  userId: string

  @Field({
    description: 'レスを作成するスレッドのID',
  })
  @IsNotEmpty()
  @IsUUID()
  threadId: string
}

ポイントは class-validator からインポートした@IsNotEmpty などのデコレータを使って、入力値のバリデーションを行っているところだね。

これで一通りの Input type ができた。あとは Resolver に Mutation を追加していけばそれで完了だよ。概ね Query と一緒だ。さくっと書くよ。

// ---- src/bbs/bbs.resolver.ts ----
@Resolver((of) => Bbs)
export class BbsResolver {
  // ---- 中略 ----

  @Mutation((returns) => Bbs, {
    description: '新しい掲示板を作成する',
  })
  async createBbs(@Args('input') input: CreateBbsInput): Promise<Bbs> {
    return {
      bbsName: input.bbsName,
      bbsId: 'bbsId',
    }
  }
}
// ---- src/bbs/bbs-user.resolver.ts ----
@Resolver((of) => BbsUser)
export class BbsUserResolver {
  // ---- 中略 ----

  @Mutation((returns) => BbsUser, {
    description: '新しい掲示板ユーザーを作成する',
  })
  async createBbsUser(
    @Args('input') input: CreateBbsUserInput
  ): Promise<BbsUser> {
    return {
      userId: 'userId',
      userName: input.userName,
    }
  }
}
// ---- src/bbs/bbs-thread.resolver.ts ----
@Resolver((of) => BbsThread)
export class BbsThreadResolver {
  // ---- 中略 ----

  @Mutation((returns) => BbsThread, {
    description: '新しい掲示板ユーザーを作成する',
  })
  async createBbsThread(
    @Args('input') input: CreateBbsThreadInput
  ): Promise<Omit<BbsThread, 'bbs'>> {
    return {
      description: input.description,
      threadName: input.threadName,
      threadId: 'threadId',
    }
  }
}
// ---- src/bbs/bbs-thread-response.resolver.ts ----
@Resolver((of) => BbsThreadResponse)
export class BbsThreadResponseResolver {
  // ---- 中略 ----

  @Mutation((returns) => BbsThreadResponse, {
    description: '新しい掲示板ユーザーを作成する',
  })
  async createBbsThreadResponse(
    @Args('input') input: CreateBbsThreadResponseInput
  ): Promise<Omit<BbsThreadResponse, 'thread' | 'user'>> {
    return {
      content: input.content,
      postedAt: new Date(),
      responseId: 'responseId',
    }
  }
}

こんな感じだね。Queryと同じくフィールドリゾルバで必要に応じたエンティティ辿りもできるよ。フィールドリゾルバの実装内容は前回と同じダミーだから、今回は割愛しているからね。

参考までに、上記Mutationで生成されたschema.graphqlの中身を見せるよ。

"""新しい掲示板を立てる"""
input CreateBbsInput {
  """掲示板の名前"""
  bbsName: String!
}

"""新しいスレッドを立てる"""
input CreateBbsThreadInput {
  """スレッドを立てる掲示板のID"""
  bbsId: String!

  """スレッドの説明"""
  description: String!

  """スレッドの名前"""
  threadName: String!
}

"""新しいレスを作成する"""
input CreateBbsThreadResponseInput {
  """レスの内容"""
  content: String!

  """レスを作成するスレッドのID"""
  threadId: String!

  """レスを作成するユーザーのID"""
  userId: String!
}

"""新しいユーザーを作成する"""
input CreateBbsUserInput {
  """ユーザーの名前"""
  userName: String!
}

type Mutation {
  changePassword(data: ChangePasswordInput!): User!

  """新しい掲示板を作成する"""
  createBbs(input: CreateBbsInput!): Bbs!

  """新しい掲示板ユーザーを作成する"""
  createBbsThread(input: CreateBbsThreadInput!): BbsThread!

  """新しい掲示板ユーザーを作成する"""
  createBbsThreadResponse(input: CreateBbsThreadResponseInput!): BbsThreadResponse!

  """新しい掲示板ユーザーを作成する"""
  createBbsUser(input: CreateBbsUserInput!): BbsUser!
}

ArgsTypeで入力の検証を行う

Input typeではなく単純な(フラットな)引数でも検証デコレータを使うには、ArgsTypeで引数クラスを実装する必要があるよ。

さくさく説明するね。

これを

  @Query((returns) => Bbs, {
    description: '掲示板をIDで取得する',
  })
  async bbs(
    // フラットな引数
    @Args('id', { type: () => ID, description: '掲示板のID' }) id: string
  ) {
    return {
      bbsId: args.id,
      bbsName: 'bbsName',
    };
  }

こうする。

  @Query((returns) => Bbs, {
    description: '掲示板をIDで取得する',
  })
  async bbs(
    // ArgsTypeで書き換え
    @Args() args: BbsIdArgs
  ) {
    return {
      bbsId: args.id,
      bbsName: 'bbsName',
    };
  }

で、肝心のArgsTypeはこう。

import { IsUUID } from 'class-validator';
import { Field, ArgsType } from '@nestjs/graphql';

@ArgsType()
export class BbsIdArgs {
  @Field({
    description: '掲示板のID',
  })
  @IsUUID()
  id: string;
}

これで完了! 検証を必要としないことってあまりないし、肥大化にも対応できるから、InputTypeではないフラットな引数なら、基本的にはArgsTypeで実装してあげると良さそうだね。

一応、検証がちゃんと機能するかやってみるよ。引数idは@IsUUIDデコレータでUUID以外の値ならエラーになってるから、それを確認するよ。

# ▼こういうクエリを投げると
query getBbs {
  bbs(id: "cb1ff66b-9909-1a09-70d0-a76bc9b8645b") {
    bbsId
    bbsName
  }
}

# ▼こんな応答がくる
# {
#   "data": {
#     "bbs": {
#       "bbsId": "cb1ff66b-9909-1a09-70d0-a76bc9b8645b",
#       "bbsName": "bbsName"
#     }
#   }
# }

# ▼一方でUUIDの形式に反した引数を投げると
query getBbs {
  bbs(id: "hoge") {
    bbsId
    bbsName
  }
}

# ▼こんなエラー応答になる
# {
#   "errors": [
#     {
#       "message": "Bad Request Exception",
#       "locations": [
#         {
#           "line": 2,
#           "column": 3
#         }
#       ],
#       "path": [
#         "bbs"
#       ],
#       "extensions": {
#         "code": "BAD_REQUEST",
#         "stacktrace": [
#           // ---- 略 ----
#         ],
#         "originalError": {
#           "statusCode": 400,
#           "message": [
#             "id must be a UUID"
#           ],
#           "error": "Bad Request"
#         }
#       }
#     }
#   ],
#   "data": null
# }

というわけで無事にバリデータが効いてることが確認できたね。


ひとまず今日はここまでかな。明日は次のことをやるよ。

  • Serviceを実装して、prisma でDBとやり取りする

次回:Nest.js学習にっき3