NestJS + graphQL + Prisma + Firebase でToDOリストを作ろう!〜機能作成編〜
はじめに
今回から実際に機能を実装していきます!
前回の記事はコチラ
この記事で制作したコードはコチラ
Todoモジュール作成
まずはTodo周りの機能を作っていきましょう。
モジュール作成
まずはtodo.module.ts
を作成します。
nest generate module todo
モデル作成
次に、queryやmutationが呼ばれた際に返却する型であるmodelを作成します。
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;
}
モデルの書き方はコチラregisterEnumType
を使用することでprismaで作成したEnum型を型として利用できるようになります。
リポジトリ作成
次に、DBとのやりとりを担うRepository
クラスを作成します。
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を組み合わせて色々する層としてユースケース等をおくべきですが、今回は省略します。
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,
},
});
}
}
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,
},
});
}
}
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,
},
});
}
}
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として定義しておきます。
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
を作成します。
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を実装した際にリクエストコンテキストから取得するので、とりあえず適当な値を入れておきます。
モジュール確認
最終的に以下のようになります。
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
モデル作成
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;
}
リポジトリ作成
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,
},
});
}
}
サービス作成
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,
},
});
}
}
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,
},
});
}
}
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,
},
});
}
}
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への操作が入るので、ユースケースを作成しておきます。
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,
});
}
}
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,
});
}
}
リゾルバー作成
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,
});
}
}
モジュール確認
最終的に以下のようになります。
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
ファイルが生成されます。定義したリゾルバー通りにちゃんと型が生成されていますね!
以後、リゾルバーに変更がある場合、アプリケーションを起動するたびに更新されます。
# ------------------------------------------------------
# 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が入るので今は適当な値を入力しておきます。
いい感じですね!データベースとの接続もうまくいっているようです。
今回はここまでです!
Discussion