Nest.js学習にっき3

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

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

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

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

早速やっていこう。

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

まずはschema.prismaに掲示板、スレッド、レス、ユーザーのテーブルを追加しよう。

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
  // previewFeatures = []
}

generator dbml {
  provider = "prisma-dbml-generator"
}

model User {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email     String   @unique
  password  String
  firstname String?
  lastname  String?
  posts     Post[]
  role      Role
}

model Post {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean
  title     String
  content   String?
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  String?
}

enum Role {
  ADMIN
  USER
}

model Bbs {
  id      String @id @default(uuid()) @db.Uuid
  bbsName String @db.VarChar(100)

  BbsThread BbsThread[]
}

model BbsThread {
  id          String @id @default(uuid()) @db.Uuid
  bbsId       String @db.Uuid
  threadName  String @db.VarChar(100)
  description String @db.VarChar(2000)

  bbs               Bbs                 @relation(fields: [bbsId], references: [id])
  BbsThreadResponse BbsThreadResponse[]
}

model BbsThreadResponse {
  id          String   @id @default(uuid()) @db.Uuid
  bbsThreadId String   @db.Uuid
  userId      String   @db.Uuid
  postedAt    DateTime @default(now())
  content     String   @db.VarChar(2000)

  bbsThread BbsThread @relation(fields: [bbsThreadId], references: [id])
  user      BbsUser   @relation(fields: [userId], references: [id])
}

model BbsUser {
  id       String @id @default(uuid()) @db.Uuid
  userName String @db.VarChar(100)

  BbsThreadResponse BbsThreadResponse[]
}

スキーマを書いたら、 npm run migrate:dev でマイグレーションしよう。そうするとschema.prismaに書いたテーブル定義が実際のDBに反映される。このプロジェクトの場合、DBはPostgreSQLだね。

次にServiceを実装しよう。

// ---- src/bbs/bbs.service.ts ----
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'nestjs-prisma';
import { CreateBbsInput } from './dto/create-bbs.input';
import { BbsIdArgs } from './dto/bbs-id.args';
import { Bbs } from './models/bbs.model';
import { BbsThread } from './models/bbs-thread.model';

@Injectable()
export class BbsService {
  constructor(private prisma: PrismaService) {}

  async bbs(args: BbsIdArgs): Promise<Bbs | null> {
    const bbs = await this.prisma.bbs.findUnique({
      where: {
        id: args.id,
      },
      select: {
        id: true,
        bbsName: true,
      },
    });
    if (!bbs) return null;
    return {
      bbsId: bbs.id,
      bbsName: bbs.bbsName,
    };
  }

  async threads(
    bbs: Bbs,
    limit: number | undefined
  ): Promise<Omit<BbsThread, 'bbs'>[]> {
    const threads = await this.prisma.bbsThread.findMany({
      where: {
        bbsId: bbs.bbsId,
      },
      select: {
        id: true,
        threadName: true,
        description: true,
      },
      take: limit,
    });
    return threads.map((thread) => {
      return {
        threadId: thread.id,
        threadName: thread.threadName,
        description: thread.description,
      };
    });
  }

  async createBbs(input: CreateBbsInput): Promise<Bbs> {
    const bbs = await this.prisma.bbs.create({
      data: {
        bbsName: input.bbsName,
      },
      select: {
        id: true,
        bbsName: true,
      },
    });
    return {
      bbsId: bbs.id,
      bbsName: bbs.bbsName,
    };
  }
}

Service内でPrismaServiceを利用しているけど、これはグローバルに使えるよう事前に登録済みだったよね。

他にもbbs-user.service.ts、bbs-thread.service.ts、bbs-thread-response.service.tsを作ったけど長いから割愛するよ。

Serviceを作ったら、モジュールに登録しよう。出ないと使えないからね。

// ---- src/bbs/bbs.module.ts ----
import { Module } from '@nestjs/common';
import { BbsResolver } from './bbs.resolver';
import { BbsUserResolver } from './bbs-user.resolver';
import { BbsThreadResolver } from './bbs-thread.resolver';
import { BbsThreadResponseResolver } from './bbs-thread-response.resolver';
import { BbsService } from './bbs.service';
import { BbsUserService } from './bbs-user.service';
import { BbsThreadService } from './bbs-thread.service';
import { BbsThreadResponseService } from './bbs-thread-response.service';

@Module({
  imports: [],
  providers: [
    BbsResolver,
    BbsUserResolver,
    BbsThreadResolver,
    BbsThreadResponseResolver,
    BbsService,
    BbsUserService,
    BbsThreadService,
    BbsThreadResponseService,
  ],
})
export class BbsModule {}

さて、あとは登録したServiceを使って、これまでダミー値しか返していなかったQueryやMutationを改修するね。

案の定、長くなるからbbs.resolver.tsだけ書くよ。

// ---- src/bbs/bbs.resolver.ts ----
import {
  Args,
  Int,
  Mutation,
  Parent,
  Query,
  ResolveField,
  Resolver,
} from '@nestjs/graphql';
import { Bbs } from './models/bbs.model';
import { CreateBbsInput } from './dto/create-bbs.input';
import { BbsIdArgs } from './dto/bbs-id.args';
import { BbsService } from './bbs.service';
import { BbsThread } from './models/bbs-thread.model';

@Resolver(() => Bbs)
export class BbsResolver {
  constructor(private bbsService: BbsService) {}

  @Query(() => Bbs, {
    description: '掲示板をIDで取得する',
    nullable: true,
  })
  async bbs(@Args() args: BbsIdArgs): Promise<Bbs | null> {
    return this.bbsService.bbs(args);
  }

  @ResolveField()
  async threads(
    @Parent() bbs: Bbs,
    @Args('limit', {
      type: () => Int,
      description: '取得件数の上限',
      nullable: true,
    })
    limit?: number
  ): Promise<Omit<BbsThread, 'bbs'>[]> {
    return this.bbsService.threads(bbs, limit);
  }

  @Mutation(() => Bbs, {
    description: '新しい掲示板を作成する',
  })
  async createBbs(@Args('input') input: CreateBbsInput): Promise<Bbs> {
    return this.bbsService.createBbs(input);
  }
}

こんな感じだね。

ちなみにBbsのthreadsフィールドリゾルバには、ParentだけでなくArgsを引数に持たせることで、必要に応じて「ネストしたクエリ」みたいに使えるようにしているよ。ちなみにこの「ネストしたクエリ」という言葉は、GraphQLにおける正式名称ではなくて、特に表現が思いつかなかったから自分でつけた仮称だよ。正式名称なにかあるのかな?

nest start でサーバーを起動したら、実装したクエリを試してみよう。

オペレーションの内容は例えばこんな感じ。

query getThreadsFromBbs01 {
  bbs(id: "157e19b6-0630-411d-a710-3637494fc979") {
    bbsName
    threads(limit: 3) {
      threadName
    }
  }
}

この場合には、次のような応答が来るはずだよ。

{
  "data": {
    "bbs": {
      "bbsName": "掲示板その1",
      "threads": [
        {
          "threadName": "TT3"
        },
        {
          "threadName": "TT5"
        },
        {
          "threadName": "TT4"
        }
      ]
    }
  }
}

もしDBの中身を比較して操作の結果を見たいなら、Prisma Studioも起動しよう。このnpx prisma studioでDBの中身を参照したりいじったりできるブラウザベースのツールが起動するからね。

ちゃんと動いてることが確認できたら、これでPrismaを使ったNest.jsのResolver、Service、そしてDB間の繋ぎこみは完了だね!