🍈

NestJS + graphQL + Prisma + Firebase でToDOリストを作ろう!〜機能作成編〜

2024/07/08に公開

はじめに

今回から実際に機能を実装していきます!
前回の記事はコチラ
https://zenn.dev/ouka031/articles/c80b6819fae8d0

この記事で制作したコードはコチラ
https://github.com/Shige031/nestjs-graphql-prisma-starter

Todoモジュール作成

まずはTodo周りの機能を作っていきましょう。

モジュール作成

まずはtodo.module.tsを作成します。

ターミナル
nest generate module todo

モデル作成

次に、queryやmutationが呼ばれた際に返却する型であるmodelを作成します。

src/todo/models/todo.model.ts
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { TodoStatus } from '@prisma/client';

registerEnumType(TodoStatus, {
  name: 'TodoStatus',
  description: Object.keys(TodoStatus).join(','),
});

@ObjectType()
export class TodoModel {
  @Field(() => String)
  id: string;

  @Field(() => String)
  userId: string;

  @Field(() => String)
  title: string;

  @Field(() => String)
  description: string;

  @Field(() => TodoStatus)
  status: TodoStatus;

  @Field(() => Date)
  createdAt: Date;

  @Field(() => Date, { nullable: true })
  updatedAt: Date | null;
}

モデルの書き方はコチラ
https://docs.nestjs.com/graphql/resolvers
registerEnumTypeを使用することでprismaで作成したEnum型を型として利用できるようになります。
https://docs.nestjs.com/graphql/unions-and-enums#code-first-1

リポジトリ作成

次に、DBとのやりとりを担うRepositoryクラスを作成します。

src/todo/repository/todo.repository.ts
import { Injectable } from '@nestjs/common';
import { Prisma, PrismaPromise, Todo } from '@prisma/client';
import { PrismaService } from 'src/prisma.service';

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

  findMany({ input }: { input: Prisma.TodoWhereInput }): PrismaPromise<Todo[]> {
    return this.prisma.todo.findMany({
      where: {
        ...input,
      },
    });
  }

  create({ input }: { input: Prisma.TodoUncheckedCreateInput }): PrismaPromise<Todo> {
    return this.prisma.todo.create({
      data: {
        ...input,
      },
    });
  }

  update({
    id,
    input,
  }: {
    id: string;
    input: Prisma.TodoUpdateInput;
  }): PrismaPromise<Todo> {
    return this.prisma.todo.update({
      data: {
        ...input,
      },
      where: {
        id,
      },
    });
  }

  delete({ id }: { id: string }): PrismaPromise<Todo> {
    return this.prisma.todo.delete({
      where: {
        id,
      },
    });
  }

  deleteMany({ input }: { input: Prisma.TodoWhereInput }) {
    return this.prisma.todo.deleteMany({
      where: {
        ...input,
      },
    });
  }
}

サービス作成

使い回すロジックの最小単位としてServiceクラスを作成します。本来はServiceを組み合わせて色々する層としてユースケース等をおくべきですが、今回は省略します。

src/todo/service/findManyTodo.service.ts
import { Injectable } from '@nestjs/common';
import { Todo } from '@prisma/client';
import { TodoRepository } from '../repository/todo.repository';

@Injectable()
export class FindManyTodoService {
  constructor(private readonly todoRepository: TodoRepository) {}

  async handle({ userId }: { userId: string }): Promise<Todo[]> {
    return await this.todoRepository.findMany({
      input: {
        userId,
      },
    });
  }
}
src/todo/service/createTodo.service.ts
import { Injectable } from '@nestjs/common';
import { Todo } from '@prisma/client';
import { TodoRepository } from '../repository/todo.repository';

@Injectable()
export class CreateTodoService {
  constructor(private readonly todoRepository: TodoRepository) {}

  async handle({
    userId,
    title,
    description,
  }: {
    userId: string;
    title: string;
    description: string;
  }): Promise<Todo> {
    return await this.todoRepository.create({
      input: {
        userId,
        title,
        description,
      },
    });
  }
}
src/todo/service/updateTodo.service.ts
import { Injectable } from '@nestjs/common';
import { Todo, TodoStatus } from '@prisma/client';
import { TodoRepository } from '../repository/todo.repository';

@Injectable()
export class UpdateTodoService {
  constructor(private readonly todoRepository: TodoRepository) {}

  async updateContent({
    id,
    title,
    description,
  }: {
    id: string;
    title: string;
    description: string;
  }): Promise<Todo> {
    return await this.todoRepository.update({
      id,
      input: {
        title,
        description,
      },
    });
  }

  async updateStatus({
    id,
    status,
  }: {
    id: string;
    status: TodoStatus;
  }): Promise<Todo> {
    return await this.todoRepository.update({
      id,
      input: {
        status,
      },
    });
  }
}
src/todo/service/deleteTodo.service.ts
import { Injectable } from '@nestjs/common';
import { Todo, TodoStatus } from '@prisma/client';
import { TodoRepository } from '../repository/todo.repository';

@Injectable()
export class DeleteTodoService {
  constructor(private readonly todoRepository: TodoRepository) {}

  async handle({ id }: { id: string }): Promise<Todo> {
    return await this.todoRepository.delete({ id });
  }
}

dto作成

updateTodoStatusへの入力値にenum型があるのでdtoとして定義しておきます。

src/todo/dto/todo.dto.ts
import { Field, InputType, registerEnumType } from '@nestjs/graphql';
import { TodoStatus } from '@prisma/client';

registerEnumType(TodoStatus, {
  name: 'TodoStatus',
  description: Object.keys(TodoStatus).join(','),
});

@InputType()
export class UpdateTodoStatusInput {
  @Field(() => String)
  id!: string;

  @Field(() => TodoStatus)
  status!: TodoStatus;
}

リゾルバー作成

最後にResolverを作成します。

src/todo/resolver/todo.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { TodoModel } from '../model/todo.model';
import { CreateTodoService } from '../service/createTodo.service';
import { UpdateTodoService } from '../service/updateTodo.service';
import { UpdateTodoStatusInput } from '../dto/todo.dto';
import { DeleteTodoService } from '../service/deleteTodo.service';
import { FindManyTodoService } from '../service/findManyTodo.service';

@Resolver(() => TodoModel)
export class TodoResolver {
  constructor(
    private readonly findManyTodoService: FindManyTodoService,
    private readonly createTodoService: CreateTodoService,
    private readonly updateTodoService: UpdateTodoService,
    private readonly deleteTodoService: DeleteTodoService,
  ) {}

  @Query(() => [TodoModel])
  async todos() {
    return await this.findManyTodoService.handle({
      userId: 'xxx',
    });
  }

  @Mutation(() => TodoModel)
  async createTodo(
    @Args('title') title: string,
    @Args('description') description: string,
  ): Promise<TodoModel> {
    return await this.createTodoService.handle({
      userId: 'xxx',
      title,
      description,
    });
  }

  @Mutation(() => TodoModel)
  async updateTodoContent(
    @Args('id') id: string,
    @Args('title') title: string,
    @Args('description') description: string,
  ): Promise<TodoModel> {
    return await this.updateTodoService.updateContent({
      id,
      title,
      description,
    });
  }

  @Mutation(() => TodoModel)
  async updateTodoStatus(
    @Args('input') input: UpdateTodoStatusInput,
  ): Promise<TodoModel> {
    return await this.updateTodoService.updateStatus({
      ...input,
    });
  }

  @Mutation(() => TodoModel)
  async deleteTodo(@Args('id') id: string): Promise<TodoModel> {
    return await this.deleteTodoService.handle({ id });
  }
}

userIdは後から認可に使用するGuardを実装した際にリクエストコンテキストから取得するので、とりあえず適当な値を入れておきます。

モジュール確認

最終的に以下のようになります。

src/todo/todo.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';
import { TodoRepository } from './repository/todo.repository';
import { CreateTodoService } from './service/createTodo.service';
import { UpdateTodoService } from './service/updateTodo.service';
import { DeleteTodoService } from './service/deleteTodo.service';
import { FindManyTodoService } from './service/findManyTodo.service';
import { TodoResolver } from './resolver/todo.resolver';

@Module({
  providers: [
    PrismaService,
    TodoResolver,
    TodoRepository,
    FindManyTodoService,
    CreateTodoService,
    UpdateTodoService,
    DeleteTodoService,
  ],
})
export class TodoModule {}

Userモジュール作成

続けてUser周りの機能を作っていきましょう。

モジュール作成

ターミナル
nest generate module user

モデル作成

src/user/model/user.model.ts
import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class UserModel {
  @Field(() => String)
  id: string;

  @Field(() => String)
  name: string;

  @Field(() => Date)
  createdAt: Date;

  @Field(() => Date, { nullable: true })
  updatedAt: Date | null;
}

リポジトリ作成

src/user/repository/user.repository.ts
import { Injectable } from '@nestjs/common';
import { Prisma, PrismaPromise, User } from '@prisma/client';
import { PrismaService } from 'src/prisma.service';

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

  findUniqueOrThrow({
    input,
  }: {
    input: Prisma.UserWhereUniqueInput;
  }): PrismaPromise<User> {
    return this.prisma.user.findFirstOrThrow({
      where: {
        ...input,
      },
    });
  }

  create({
    input,
  }: {
    input: Prisma.UserUncheckedCreateInput;
  }): PrismaPromise<User> {
    return this.prisma.user.create({
      data: {
        ...input,
      },
    });
  }

  update({
    id,
    input,
  }: {
    id: string;
    input: Prisma.UserUpdateInput;
  }): PrismaPromise<User> {
    return this.prisma.user.update({
      where: {
        id,
      },
      data: {
        ...input,
      },
    });
  }

  delete({ id }: { id: string }): PrismaPromise<User> {
    return this.prisma.user.delete({
      where: {
        id,
      },
    });
  }
}

サービス作成

src/user/service/findUser.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { UserRepository } from '../repository/user.repository';

@Injectable()
export class FindUserService {
  constructor(private readonly userRepository: UserRepository) {}

  async findById({ id }: { id: string }): Promise<User> {
    return this.userRepository.findUniqueOrThrow({
      input: {
        id,
      },
    });
  }

  async findByUId({ uid }: { uid: string }): Promise<User> {
    return this.userRepository.findUniqueOrThrow({
      input: {
        firebaseUId: uid,
      },
    });
  }
}
src/user/service/createUser.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { UserRepository } from '../repository/user.repository';

@Injectable()
export class CreateUserService {
  constructor(private readonly userRepository: UserRepository) {}

  async handle({
    firebaseUId,
    name,
  }: {
    firebaseUId: string;
    name: string;
  }): Promise<User> {
    return this.userRepository.create({
      input: {
        firebaseUId,
        name,
      },
    });
  }
}
src/user/service/updateUser.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { UserRepository } from '../repository/user.repository';

@Injectable()
export class UpdateUserService {
  constructor(private readonly userRepository: UserRepository) {}

  async handle({ id, name }: { id: string; name: string }): Promise<User> {
    return this.userRepository.update({
      id,
      input: {
        name,
      },
    });
  }
}
src/user/service/deleteUser.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { UserRepository } from '../repository/user.repository';

@Injectable()
export class DeleteUserService {
  constructor(private readonly userRepository: UserRepository) {}

  async handle({ id }: { id: string }): Promise<User> {
    return this.userRepository.delete({
      id,
    });
  }
}

ユースケース作成

firebaseへの操作が入るので、ユースケースを作成しておきます。

src/user/usecase/createUser.usecase.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { CreateUserService } from '../service/createUser.service';

@Injectable()
export class CreateUserUseCase {
  constructor(private readonly createUserService: CreateUserService) {}

  async handle({ name, uid }: { name: string; uid: string }): Promise<User> {
    // firebaseとuidを突合する処理が入る
    return this.createUserService.handle({
      firebaseUId: uid,
      name,
    });
  }
}
src/usr/usecase/deleteUser.usecase.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { DeleteUserService } from '../service/deleteUser.service';
import { FindUserService } from '../service/findUser.service';

@Injectable()
export class DeleteUserUseCase {
  constructor(
    private readonly findUserService: FindUserService,
    private readonly deleteUserService: DeleteUserService,
  ) {}

  async handle({ id }: { id: string }): Promise<User> {
    const user = this.findUserService.findById({ id });
    // firebaseからユーザーを削除する処理が入る
    return this.deleteUserService.handle({
      id,
    });
  }
}

リゾルバー作成

src/user/resolver/user.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UserModel } from '../model/user.model';
import { FindUserService } from '../service/findUser.service';
import { CreateUserUseCase } from '../usecase/createUser.usecase';
import { UpdateUserService } from '../service/updateUser.service';
import { DeleteUserUseCase } from '../usecase/deleteUser.usecase';

@Resolver(() => UserModel)
export class UserResolver {
  constructor(
    private readonly findUserService: FindUserService,
    private readonly createUserUseCase: CreateUserUseCase,
    private readonly updateUserService: UpdateUserService,
    private readonly deleteUserService: DeleteUserUseCase,
  ) {}

  @Query(() => UserModel)
  async user(@Args('id') id: string) {
    return await this.findUserService.findById({
      id,
    });
  }

  @Mutation(() => UserModel)
  async createUser(
    @Args('name') name: string,
    @Args('uid') uid: string,
  ): Promise<UserModel> {
    return await this.createUserUseCase.handle({
      name,
      uid,
    });
  }

  @Mutation(() => UserModel)
  async updateUser(
    @Args('id') id: string,
    @Args('name') name: string,
  ): Promise<UserModel> {
    return await this.updateUserService.handle({
      id,
      name,
    });
  }

  @Mutation(() => UserModel)
  async deleteUser(@Args('id') id: string): Promise<UserModel> {
    return await this.deleteUserService.handle({
      id,
    });
  }
}

モジュール確認

最終的に以下のようになります。

src/user/user.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';
import { UserRepository } from './repository/user.repository';
import { CreateUserService } from './service/createUser.service';
import { UpdateUserService } from './service/updateUser.service';
import { DeleteUserService } from './service/deleteUser.service';
import { CreateUserUseCase } from './usecase/createUser.usecase';
import { FindUserService } from './service/findUser.service';
import { DeleteUserUseCase } from './usecase/deleteUser.usecase';
import { UserResolver } from './resolver/user.resolver';

@Module({
  providers: [
    PrismaService,
    UserRepository,
    UserResolver,
    FindUserService,
    CreateUserService,
    UpdateUserService,
    DeleteUserService,
    CreateUserUseCase,
    DeleteUserUseCase,
  ],
  exports: [FindUserService],
})
export class UserModule {}

疎通確認

schema.graphqlファイル更新

早速アプリケーションを起動してみましょう。

ターミナル
npm run start:dev

するとsrc下にschema.gqlファイルが生成されます。定義したリゾルバー通りにちゃんと型が生成されていますね!
以後、リゾルバーに変更がある場合、アプリケーションを起動するたびに更新されます。

src/schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type Mutation {
  createTodo(description: String!, title: String!): TodoModel!
  createUser(name: String!, uid: String!): UserModel!
  deleteTodo(id: String!): TodoModel!
  deleteUser(id: String!): UserModel!
  updateTodoContent(description: String!, id: String!, title: String!): TodoModel!
  updateTodoStatus(input: UpdateTodoStatusInput!): TodoModel!
  updateUser(id: String!, name: String!): UserModel!
}

type Query {
  todos: [TodoModel!]!
  user(id: String!): UserModel!
}

type TodoModel {
  createdAt: DateTime!
  description: String!
  id: String!
  status: TodoStatus!
  title: String!
  updatedAt: DateTime
  userId: String!
}

"""NOT_STARTED,IN_PROGRESS,COMPLETED"""
enum TodoStatus {
  COMPLETED
  IN_PROGRESS
  NOT_STARTED
}

input UpdateTodoStatusInput {
  id: String!
  status: TodoStatus!
}

type UserModel {
  createdAt: DateTime!
  id: String!
  name: String!
  updatedAt: DateTime
}

疎通確認

Postmanを使用してクエリを実行してみましょう。
uidはfirebaseのidが入るので今は適当な値を入力しておきます。

いい感じですね!データベースとの接続もうまくいっているようです。
今回はここまでです!

https://zenn.dev/ouka031/articles/92d30ed1414b35

Discussion