Prisma + Apollo ServerでGraphQLサーバーを建てる

# 概要

Prismaを触ってみて、ApolloでGraphQLサーバー建てるところまでやってみたのでその備忘録。

参考にしたチュートリアル
https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-typescript-postgres/

この記事のコードのリポジトリ
https://github.com/yuuyu00/prisma-apollo

# プロジェクト作成

サクッと雛形を作る。
とりあえずPrismaの依存だけインストールする。

```sh
mkdir prisma-apollo && cd prisma-apollo
yarn init -y
yarn add @prisma/cli typescript ts-node @types/node
```

`npx prisma` で、インストールした `@prisma/cli` を実行することができる。

## tsconfig.json

以下のtsconfig.jsonを追加する。

```json
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}
```

# Prismaのセットアップ

## prisma init

`npx prisma init` を実行して、Prismaのスキーマやマイグレーションファイルを生成する。
生成されたファイルは `prisma/` 以下にある。

## DB設定

今回はpostgresを使用するので、使っていなければ先に起動できる状態までセットアップしておく。
postgresの使用するユーザー名、パスワード、ホスト、DB名がわかれば、 `prisma init` によって生成された.envにDB接続情報を書く。

```
# DATABASE_URLは以下の書式に従う。
# postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA

# 例えばDBユーザー名がtarou, パスワードがhoge, ホスト名がlocalhost, ポート番号が5432, DB名がmydb, スキーマ名がpublic（デフォルトではpublic）の場合、
DATABASE_URL="postgresql://tarou:hoge@localhost:5432/mydb?schema=public"
# のように記述する。

# macOSにpostgresをインストールすると、ログインユーザーがデフォルトのユーザーとして設定される。
# その場合、ユーザー名とパスワードがどちらもMacのログインユーザー名になる。
```

## Prismaのスキーマを編集する

`prisma/prisma.schema` を開き、以下の内容を追加する。

```
model Post {
  id        Int      @default(autoincrement()) @id
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}
model Profile {
  id     Int     @default(autoincrement()) @id
  bio    String?
  user   User    @relation(fields: [userId], references: [id])
  userId Int     @unique
}
model User {
  id      Int      @default(autoincrement()) @id
  email   String   @unique
  name    String?
  posts   Post[]
  profile Profile?
}
```

モデルの記述については以下を参照（読まなくてもまあなんとなくわかると思う）
https://www.prisma.io/docs/concepts/components/prisma-schema/data-model

## マイグレーションファイルを生成＆DBに適用する

以下のコマンドを実行する。

```sh
npx prisma migrate dev --name init --preview-feature
# dev -> マイグレーションファイルを生成して、すぐにDBに変更を適用する
# --name -> マイグレーションファイルの末尾につける名前を指定する
# --preview-feature -> プレビュー版の機能を使う。業務のプロジェクトで使うなよ！
```

これを実行すると、prisma.schemaからマイグレーションファイルが生成され、その内容がただちにDBに適用される。

## prisma clientを使う

`@prisma/client` をインストールして、PrismaClientを使ってDBにアクセスする。
使い方はこんな感じ：

```typescript
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

// userモデルのすべてのレコードを取得する
const getUsers = async () => {
  const users = await prisma.user.findMany();
  return users;
}

// userモデルへのリレーションを持つauthorプロパティを含めたpostモデルのレコードを、idを指定して1件取得する
const getPost = async (postId: number) => {
  const post = await prisma.post.findUnique({
    where: { id: 1 },
    include: { author: true },
  });
  return post
}

// userモデルへのリレーションを持つauthorプロパティを含めたpostモデルのレコードを、1件作成する
const createPost = async (
    title: string,
    published: boolean,
    authorId: number,
    content?: string
  ) => {
    context.prisma.post.create({
      data: {
        title,
        content,
        published,
        // connectプロパティによってUserテーブルの既存のレコードと結合する
        author: { connect: { id: authorId } },
      },
    });
  };
```


# GraphQLサーバーの実装

## ライブラリインストール

スクリプトとか含めて全部書くの大変なので概要に書いたリポジトリのpackage.json参照
スクリプトも同じように書いておく。

## GraphQLスキーマを書く

PrismaのモデルをGraphQLスキーマのtypeに移し、queryやmutationを書く。

```schema.gql
type Post {
  id: Int!
  createdAt: String!
  updatedAt: String!
  title: String!
  content: String
  published: Boolean!
  author: User!
}

type Profile {
  id: Int!
  bio: String
  user: User!
}

type User {
  id: Int!
  email: String!
  name: String
  posts: [Post!]!
  profile: Profile
}

input CreateUserInput {
  email: String!
  name: String!
}

input CreatePostInput {
  title: String!
  content: String
  published: Boolean!
  authorId: Int!
}

type Mutation {
  createUser(input: CreateUserInput): User!
  createPost(input: CreatePostInput): Post!
}

type Query {
  user(id: Int!): User
  posts: [Post!]!
}
```

authorIdやuserIdなどは冗長なので、モデルのtypeからは除外した。

## Apolloサーバー追加

srcディレクトリを追加し、server.tsに以下のようにApolloServerを建てるためのコードを書く。
まだほとんどのファイルを実装してないのでエラーになるが無視

```typescript:server.ts

import { ApolloServer, gql, IResolvers } from "apollo-server";
import { Resolvers } from "../gqlTypes";
import { readFileSync } from "fs";
import { createContext } from "./context";
import { resolvers } from "./resolvers";

const getTypeDefs = () => {
  const schemaStr = readFileSync("schema.gql", "utf8");
  return gql`
    ${schemaStr}
  `;
};

new ApolloServer({
  typeDefs: getTypeDefs(),
  context: createContext,
  resolvers: resolvers as IResolvers<any, any> & Resolvers,
}).listen({ port: 9000 }, () => console.log("listening on 9000"));

```

## context追加

resolverに必ず渡ってくる値であるcontextを追加する。
prismaのようなデータソースへのアクセスをcontextを介して行う。

```typescript:context.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export interface Context {
  prisma: PrismaClient;
}

export const createContext: () => Context = () => {
  return { prisma };
};

```

## graphql-code-generatorでGraphQLスキーマからTSの型を生成する

プロジェクトルートにcodegen.ymlを追加し、以下のように書く。

```yml:codegen.yml
schema: "./schema.gql"
generates:
  gqlTypes.ts:
    plugins:
      - typescript
      - typescript-resolvers
    config:
      mapperTypeSuffix: Model
      mappers:
        Post: "@prisma/client/index.d#Post"
        Profile: "@prisma/client/index.d#Profile"
        User: "@prisma/client/index.d#User"
      contextType: ./src/context#Context
```

書いたら、 `graphql-codegen --config codegen.yml` でTSの型を生成する。
（package.jsonのscriptsを概要のリポジトリと同じように書いていれば `yarn generate` でも可）

### mappersって何？

codegen.ymlのconfig -> mappersは、
「この名前に該当するGraphQLスキーマのtypeについては、スキーマからTSへの変換を行わず指定した型を使うこととする」
という設定。

### なぜ使うのか
GraphQLのtypeとモデルの型のミスマッチを解消する作業をトリビアルリゾルバに任せるために設定する。
もしそうしない場合、そのミスマッチをqueryやmutationの戻り値ごとに解消しなければならず、とても面倒なことになる。

[トリビアルリゾルバとは](https://graphql.org/learn/execution/#trivial-resolvers)

例えばmappersを設定しなかった場合、このようなエラーが出るとする

![スクリーンショット 2021-01-20 15.08.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258226/41863a15-7e13-2c9e-ace1-59857cafa4cb.png)


GraphQLの型にはauthorというUser型のプロパティがあるはずだが、それがないというエラーになる。
find時にauthorのリレーションまで含めて取っていないので当然だが、含めて取ったとしてもprismaからの戻り値がUser型ではなくなってしまうため、エラーは解消できない

![スクリーンショット 2021-01-20 15.08.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258226/0b15e7f1-7e1d-f51b-4e28-4423476c4893.png)


型アサーションで無理やり通せば解決できるかもしれないが、型安全性が損なわれるし毎回そんなことをするのは治安が悪すぎる。
しかも、この方法だとauthorプロパティにアクセスしない時でも常にauthorを取得するので効率が悪いうえ、authorから先のリレーションは取っていないのでアクセスできない。それを解決しようとincludeを増やせばさらに無駄なデータ取得を増やすことになる。

これを解決するには、queryやmutationの単位では一旦prismaの型の値を返して、トリビアルリゾルバによってそれ以降のミスマッチを解決すればよい。
そのために、TSの型としてのUserはprismaが生成したUserの型を使うこととする、という設定をmappersで行う。

### トリビアルリゾルバでauthorプロパティを解決する

includeオプションを付けずにPostのレコードを取得した場合、authorプロパティは含まれないのでそれを解決する必要がある。

```typescript
import { PostResolvers } from "../../../gqlTypes";

export const Post: PostResolvers = {
  // Post型のauthorプロパティはこの関数の実行結果を返す
  // parentは、返却されようとしたこのリゾルバを通る前のPost型の値
  author: async (parent, _, context) => {
    const author = await context.prisma.user.findUnique({
      where: { id: parent.authorId },
    });
    if (!author) throw new Error("author not found");

    return author;
  },
};


```

このリゾルバをQueryやMutationとともにApolloServerに渡せば、authorプロパティにアクセスした際にこのリゾルバが実行され、値が解決される。


## Queryを書く

src/resolvers/queriesとディレクトリを切ってQueryを実装する。

```typescript:post.ts
import { QueryResolvers } from "../../../gqlTypes";

export const posts: QueryResolvers["posts"] = ({}, _args, context) =>
  context.prisma.post.findMany({ include: { author: true } });
```

他も同様なのであとはリポジトリ参照

## Mutationを書く

Queryと同様にディレクトリ切ってMutationを実装する。

```typescript:post.ts
import { prismaVersion } from "@prisma/client";
import { MutationResolvers } from "../../../gqlTypes";

export const createPost: MutationResolvers["createPost"] = (
  {},
  { input: { title, content, published, authorId } },
  context
) =>
  context.prisma.post.create({
    data: { title, content, published, author: { connect: { id: authorId } } },
  });

```

他も同様なのであとはリポジトリ参照

## トリビアルリゾルバを書く

前述したように、QueryやMutationの戻り値では解決されていないプロパティを解決するためのリゾルバを書く。

```typescript:post.ts
import { PostResolvers } from "../../../gqlTypes";

export const Post: PostResolvers = {
  author: async (parent, _, context) => {
    const author = await context.prisma.user.findUnique({
      where: { id: parent.authorId },
    });
    if (!author) throw new Error("author not found");

    return author;
  },
};

```

他も同様なのであとはリポジトリ参照

## リゾルバをまとめる

resolvers/index.tsにリゾルバをまとめてexportする。

```typescript:index.ts
import { Resolvers } from "../../gqlTypes";
import { user, posts } from "./queries";
import { createUser, createPost } from "./mutations";
import { Post, Profile, User } from "./trivials";

const Query: Resolvers["Query"] = {
  user,
  posts,
};
const Mutation: Resolvers["Mutation"] = {
  createUser,
  createPost,
};

export const resolvers: Resolvers = {
  Query,
  Mutation,
  Post,
  Profile,
  User,
};
```

## 動作確認

`yarn dev` でサーバーを起動して、localhost:9000にアクセスして正常に動いてることを確認したら完了。