前回は、会員登録機能とログイン機能を実装しました。

今回は、バックエンドでPostgresSQLからMongoDBへ乗り換えます。

コードは、こちらです。

バックエンド:

MongoDBをセットアップする

まずは、MongoDBのセットアップから始めます。

セットアップが初めての方は、こちらで紹介しているので、ご確認ください。

MongoDBのダッシュボードの『Connect』をクリックします。

image2

Connect your applicationを選択しましょう。

image3

②Add your connection string into your application codeの内容をコピーします。

image4

バックエンドの.envファイルに移動して、DATABASE_URLのコードに貼り付けます。

DATABASE_URL="mongodb+srv://nao:<password>@cluster0.xks4y.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"

パスワードを覚えている方は、<password>をパスワードに入れ替えます。

パスワードを忘れてしまった方は、MongoDBに戻って、Database Accessをクリックします。

image5

ユーザー一覧のEDITをクリックします。

image6

Edit Passwordをクリックして、新しいパスワードを作成します。

image7

Update Userをクリックします。

image8

<password>を先程作成したパスワードに入れ替えます。

DATABASE_URL="mongodb+srv://nao:XRyGxG74al8mqDbr@cluster0.xks4y.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"

prismaの設定をする

prismaフォルダのschema.prismaを開きます。

generatorclientpreviewFeaturesを設定します。

previewFeaturesは、mongoDbを指定します。

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

datasourcedbに設定しているprovidermongodbへ修正します。

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

PostgresSQLの仕様になっていたmodelをMongoDBの仕様に修正します。

例えば、idInt型でしたが、String型で@db.ObjectIdを追加します。

また、MongoDBのIDは、_idになるので、@map("_id")を指定しないといけません。

model Book {
  id         String      @id @default(dbgenerated()) @map("_id") @db.ObjectId
  title      String
  author     String
  isRead     Boolean  @default(false)
  createdAt  DateTime @default(now())
  categoryId String @db.ObjectId
  category   Category @relation(fields: [categoryId], references: [id])
}

model Category {
  id         String      @id @default(dbgenerated()) @map("_id") @db.ObjectId
  name       String
  books      Book[]
}

model User {
  id         String      @id @default(dbgenerated()) @map("_id") @db.ObjectId
  email      String   @unique
  password   String   
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

idの型を修正する

mongoDBのIDはObjextIdになるので、shema.tsのidに関するInt型をID型へ修正します。

const { gql } = require("apollo-server");

export const typeDefs = gql`
  type Query {
    books(isRead: Boolean): [Book!]!
    book(id: ID!): Book
    categories: [Category!]!
    category(id: ID!): Category
  }

  type Mutation {
    addBook(input: AddBookInput!): BookPayload!
    deleteBook(id: ID!): BookPayload!
    updateBook(id: ID!, input: UpdateBookInput!): BookPayload!
    signup(email: String!, password: String!): AuthPayload!
    signin(email: String!, password: String!): AuthPayload!
  }

  type Book {
    id: ID!
    title: String!
    author: String!
    createdAt: String!
    category: Category!
    isRead: Boolean!
  }

  type Category {
    id: ID!
    name: String!
    books: [Book!]!
  }

  type User {
    id: ID!
    email: String!
    password: String!
  }

  type Error {
    message: String!
  }

  type BookPayload {
    errors: [Error!]!
    book: Book
  }

  type AuthPayload {
    errors: [Error!]!
    user: User
  }

  input BooksInput {
    isRead: Boolean
  }

  input AddBookInput {
    title: String!
    author: String!
    categoryId: ID!
    isRead: Boolean!
  }

  input UpdateBookInput {
    title: String
    author: String
    categoryId: ID
    isRead: Boolean
  }
`;

リゾルバーに設定しているidnumber型からstring型へ修正します。

Query.ts

import { Context } from "../index";

export const Query = {
  books: (_: any, { isRead }: { isRead: boolean }, { prisma }: Context) => {
    return prisma.book.findMany({
      where: {
        isRead,
      },
    });
  },
  book: (_: any, { id }: { id: string }, { prisma }: Context) => {
    return prisma.book.findUnique({
      where: {
        id,
      },
    });
  },
  categories: (_: any, __: any, { prisma }: Context) => {
    return prisma.category.findMany();
  },
  category: (_: any, { id }: {id: string}, { prisma }: Context) => {
    return prisma.category.findUnique({
      where : {
        id
      }
    })
  },
};

Book.ts

import { Context } from "../index";

export const Book = {
  category: (
    { categoryId }: { categoryId: string },
    _: any,
    { prisma }: Context
  ) => {
    return prisma.category.findUnique({
      where: {
        id: categoryId,
      },
    });
  },
};

Category.ts

import { Context } from "../index";

export const Category = {
  books: ({ id }: {id: string}, _: any, { prisma }: Context) => {
    return prisma.book.findMany({
      where: {
        categoryId: id,
      }
    })
  },
};

Mutation.ts

import { Book, Category, User } from "@prisma/client";
import validator from "validator";
import bcrypt from "bcryptjs";

import { Context } from "../index"

type MutationBook = {
  input : {
    title : string;
    author : string;
    categoryId : string;
    isRead: boolean;
  }
}

type MutationUser = {
  email: string;
  password: string;
}

type BookPayload = {
  errors: {
    message: string
  }[],
  book: Book | null,
}

type UserPayload = {
  errors: {
    message: string;
  }[];
  user: User | null;
};

export const Mutation = {
  addBook: async (
    _: any,
    { input }: MutationBook,
    { prisma }: Context
  ): Promise<BookPayload> => {
    const { title, author, categoryId, isRead } = input;
    if (!title || !author || !categoryId || !isRead) {
      return {
        errors: [
          {
            message: "本の内容を入力してください",
          },
        ],
        book: null,
      };
    }

    const newBook = await prisma.book.create({
      data: {
        title,
        author,
        categoryId,
        isRead,
      },
    });

    return {
      errors: [],
      book: newBook,
    };
  },
  deleteBook: async (
    _: any,
    { id }: { id: string },
    { prisma }: Context
  ): Promise<BookPayload> => {
    const book = await prisma.book.findUnique({
      where: {
        id,
      },
    });

    if (!book) {
      return {
        errors: [
          {
            message: "本のデータがありません",
          },
        ],
        book: null,
      };
    }

    await prisma.book.delete({
      where: {
        id,
      },
    });

    return {
      errors: [],
      book,
    };
  },
  updateBook: async (
    _: any,
    { id, input }: { id: string; input: MutationBook["input"] },
    { prisma }: Context
  ): Promise<BookPayload> => {
    const book = await prisma.book.findUnique({
      where: {
        id,
      },
    });

    if (!book) {
      return {
        errors: [
          {
            message: "本のデータがありません",
          },
        ],
        book: null,
      };
    }

    const updateBooks = await prisma.book.update({
      data: {
        ...input,
      },
      where: {
        id,
      },
    });

    return {
      errors: [],
      book: updateBooks,
    };
  },
  signup: async (
    _: any,
    { email, password }: MutationUser,
    { prisma }: Context
  ): Promise<UserPayload> => {
    const isEmail = validator.isEmail(email);

    if (!isEmail) {
      return {
        errors: [
          {
            message: "emailが正しくありません",
          },
        ],
        user: null,
      };
    }

    const isPassword = validator.isLength(password, {
      min: 4,
    });

    if (!isPassword) {
      return {
        errors: [
          {
            message: "4文字上のパスワードを入力してください",
          },
        ],
        user: null,
      };
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const newUser = await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
      },
    });

    return {
      errors: [],
      user: newUser,
    };
  },
  signin: async (
    _: any,
    { email, password }: MutationUser,
    { prisma }: Context
  ): Promise<UserPayload> => {
    const user = await prisma.user.findUnique({
      where: {
        email,
      },
    });

    if (!user) {
      return {
        errors: [
          {
            message: "アカウント情報が間違っています",
          },
        ],
        user: null,
      };
    }

    const comparePassword = await bcrypt.compare(password, user.password);

    if (!comparePassword) {
      return {
        errors: [
          {
            message: "パスワードが間違っています",
          },
        ],
        user: null,
      };
    }

    return {
      errors: [],
      user,
    };
  },
};

カテゴリのmutationを設定する

バックエンドからカテゴリを追加したいので、スキーマとリゾルバを作成します。

schema.ts

const { gql } = require("apollo-server");

export const typeDefs = gql`
  type Query {
    books(isRead: Boolean): [Book!]!
    book(id: ID!): Book
    categories: [Category!]!
    category(id: ID!): Category
  }

  type Mutation {
    addBook(input: AddBookInput!): BookPayload!
    deleteBook(id: ID!): BookPayload!
    updateBook(id: ID!, input: UpdateBookInput!): BookPayload!
    addCategory(name: String!): CategoryPayload!
    signup(email: String!, password: String!): AuthPayload!
    signin(email: String!, password: String!): AuthPayload!
  }

  type Book {
    id: ID!
    title: String!
    author: String!
    createdAt: String!
    category: Category!
    isRead: Boolean!
  }

  type Category {
    id: ID!
    name: String!
    books: [Book!]!
  }

  type User {
    id: ID!
    email: String!
    password: String!
  }

  type Error {
    message: String!
  }

  type BookPayload {
    errors: [Error!]!
    book: Book
  }

  type CategoryPayload {
    errors: [Error!]!
    category: Category
  }

  type AuthPayload {
    errors: [Error!]!
    user: User
  }

  input BooksInput {
    isRead: Boolean
  }

  input AddBookInput {
    title: String!
    author: String!
    categoryId: ID!
    isRead: Boolean!
  }

  input UpdateBookInput {
    title: String
    author: String
    categoryId: ID
    isRead: Boolean
  }
`;
import { Book, Category, User } from "@prisma/client";
import validator from "validator";
import bcrypt from "bcryptjs";

import { Context } from "../index"

type MutationBook = {
  input : {
    title : string;
    author : string;
    categoryId : string;
    isRead: boolean;
  }
}

type MutationCategory = {
  name: string;
};

type MutationUser = {
  email: string;
  password: string;
}

type BookPayload = {
  errors: {
    message: string
  }[],
  book: Book | null,
}

type CategoryPayload = {
  errors: {
    message: string;
  }[];
  category: Category | null;
};

type UserPayload = {
  errors: {
    message: string;
  }[];
  user: User | null;
};

export const Mutation = {
  addBook: async (
    _: any,
    { input }: MutationBook,
    { prisma }: Context
  ): Promise<BookPayload> => {
    const { title, author, categoryId, isRead } = input;
    if (!title || !author || !categoryId || !isRead) {
      return {
        errors: [
          {
            message: "本の内容を入力してください",
          },
        ],
        book: null,
      };
    }

    const newBook = await prisma.book.create({
      data: {
        title,
        author,
        categoryId,
        isRead,
      },
    });

    return {
      errors: [],
      book: newBook,
    };
  },
  deleteBook: async (
    _: any,
    { id }: { id: string },
    { prisma }: Context
  ): Promise<BookPayload> => {
    const book = await prisma.book.findUnique({
      where: {
        id,
      },
    });

    if (!book) {
      return {
        errors: [
          {
            message: "本のデータがありません",
          },
        ],
        book: null,
      };
    }

    await prisma.book.delete({
      where: {
        id,
      },
    });

    return {
      errors: [],
      book,
    };
  },
  updateBook: async (
    _: any,
    { id, input }: { id: string; input: MutationBook["input"] },
    { prisma }: Context
  ): Promise<BookPayload> => {
    const book = await prisma.book.findUnique({
      where: {
        id,
      },
    });

    if (!book) {
      return {
        errors: [
          {
            message: "本のデータがありません",
          },
        ],
        book: null,
      };
    }

    const updateBooks = await prisma.book.update({
      data: {
        ...input,
      },
      where: {
        id,
      },
    });

    return {
      errors: [],
      book: updateBooks,
    };
  },
  addCategory: async (
    _: any,
    { name }: MutationCategory,
    { prisma }: Context
  ): Promise<CategoryPayload> => {
    if (!name) {
      return {
        errors: [
          {
            message: "カテゴリを入力してください",
          },
        ],
        category: null,
      };
    }

    const newCategory = await prisma.category.create({
      data: {
        name
      },
    });

    return {
      errors: [],
      category: newCategory,
    };
  },
  signup: async (
    _: any,
    { email, password }: MutationUser,
    { prisma }: Context
  ): Promise<UserPayload> => {
    const isEmail = validator.isEmail(email);

    if (!isEmail) {
      return {
        errors: [
          {
            message: "emailが正しくありません",
          },
        ],
        user: null,
      };
    }

    const isPassword = validator.isLength(password, {
      min: 4,
    });

    if (!isPassword) {
      return {
        errors: [
          {
            message: "4文字上のパスワードを入力してください",
          },
        ],
        user: null,
      };
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const newUser = await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
      },
    });

    return {
      errors: [],
      user: newUser,
    };
  },
  signin: async (
    _: any,
    { email, password }: MutationUser,
    { prisma }: Context
  ): Promise<UserPayload> => {
    const user = await prisma.user.findUnique({
      where: {
        email,
      },
    });

    if (!user) {
      return {
        errors: [
          {
            message: "アカウント情報が間違っています",
          },
        ],
        user: null,
      };
    }

    const comparePassword = await bcrypt.compare(password, user.password);

    if (!comparePassword) {
      return {
        errors: [
          {
            message: "パスワードが間違っています",
          },
        ],
        user: null,
      };
    }

    return {
      errors: [],
      user,
    };
  },
};

エラーを識別するための!isReadは、falseにすると引っかかってしまうので削除しておきます。

addBook: async (
  _: any,
  { input }: MutationBook,
  { prisma }: Context
): Promise<BookPayload> => {
  const { title, author, categoryId, isRead } = input;
  if (!title || !author || !categoryId) {
    return {
      errors: [
        {
          message: "本の内容を入力してください",
        },
      ],
      book: null,
    };
  }

  const newBook = await prisma.book.create({
    data: {
      title,
      author,
      categoryId,
      isRead,
    },
  });

  return {
    errors: [],
    book: newBook,
  };
},

一通り完成したので、バックエンドのサーバーを起動してみます。

image9

問題なく、起動できました。

では、Apollo Studioで動作確認しましょう。

まずは、Categoryを作成します。

mutation AddCategory($name: String!) {
  addCategory(name: $name) {
    errors {
      message
    }
    category {
      name
    }
  }
}

Variablesに値を入力します。

image10

『AddCategory』ボタンをクリックすると、

image11

status 200が返ってきて、データが登録することができていそうです。

MongoDBを確認すると、

image12

Categoryのデータが追加されていました。

次は、Bookの登録を行います。

再び、Apollo Studioに戻って、Bookを追加するためにGraphQLを作成します。

mutation AddBook($input: AddBookInput!) {
  addBook(input: $input) {
    errors {
      message 
    }
    book {
      title
      author
      category {
        name
      }
      isRead
    }
  }
}

Variablesに指定するcategoryIdを調べます。

Category一覧を取得してみましょう。

query Categories {
  categories {
    id
    name
  }
}

image13

一覧で取得することができたidの値をコピーして、categoryIdの値に貼り付けます。

image14

AddBookをクリックすると、

image15

Bookのデータを追加することができました。

MongoDBを確認すると、

image16

無事、データが反映されていました。

全文は、こちらです。

次回は、バックエンドでページネーションを追加します。

ブログ一覧