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とやり取りする