【GraphQL】Apollo Server v4・Prismaでcontextによるデータ共有方法

# はじめに
`GraphQL`学習で`Apollo Server`の**v4**系を使用していたが、`context`実装時にかなり詰まり、参考文献等も少なく、公式を見つつ手探りでようやく実装できたので、備忘録として本記事を残そうと思います。

実装内容はタイトルの通り`Apollo Server`の`context`を用いてリゾルバーやプラグイン全体でデータを共有するといった内容になっています。

# スキーマ
まずは、テーブル定義でもあるスキーマを見ていきます。

## Prismaスキーマ
```scheme:shcema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

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

model Link {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  description String
  url         String
  user        User?    @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId      Int?
}

model User {
  id       Int    @id @default(autoincrement())
  name     String
  email    String @unique
  password String
  links    Link[]
}
```
ここで重要なのはリレーションの部分でLinkモデルの`user`カラムとUserモデルの`links`カラムです。

この定義を**GraphQLのスキーマ定義にも適用させる**ようにします

## GraphQLスキーマ
```graphql:schema.graphql
type Query {
  feed: [Link]!
}

type Mutation {
  post(url: String!, description: String!): Link!
  signUp(email: String!, password: String!, name: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
}

type Link {
  id: ID!
  description: String!
  url: String!
  user: User
}

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

type AuthPayload {
  token: String
  user: User
}
```
重要な点を解説すると、GraphQL側のスキーマ定義では、Prismaのスキーマと整合性がとれる形で定義しているという点です。

### QueryとMutation
::: note info
初学者向けの解説
:::
GraphQL初学者の方向けに解説をするとQueryとMutationは以下の通りです。

- Query:SQLのSELECT文、CRUDのREADにあたり、データ取得に使用
- Mutation:SQLのINSERT・UPDATE・DELETE文、CRUDのREAD以外にあたり、データの追加・変更に使用
※以下が対応表です

| SQL/REST |GraphQL |
|---|---|
| SELECT | Query  |
| INSERT | Mutation |
| UPDATE | Mutation |
| DELETE | Mutation |

さらに、QueryやMutationで`feed`や`post`などと定義していますが、これは`リゾルバー`という関数の型を定義しています。

### リゾルバーとは
::: note info
リゾルバーとは・・・クエリの型に対して情報を入れること
:::
簡単に言うと、**スキーマで定義した型に対して実際の値や操作を定義**するということです。
例えば、`Query`のリゾルバーは以下のようになっています。

```typescript
export const feed = async (_: unknown, __: unknown, context: Context) => {
  return context.prisma.link.findMany()
}
```
スキーマの型情報が以下なので、リゾルバーでは、feed関数を定義し、返り値にLinkを配列で返すようにしています。
```graphql:schema.graphql
type Query {
  feed: [Link]!
}
```
つまり、スキーマでは関数名（リゾルバーの名称）と返す型を定義しているということです。

Mutationについても同様です。
```typescript
const APP_SECRET = process.env.APP_SECRET as string

// ユーザー新規登録
export const singUp = async (
  _: unknown,
  args: { email: string; password: string; name: string },
  context: Context
) => {
  // パスワードの設定
  const password = await bcrypt.hash(args.password, 10)

  if (!args.email || !password || !args.name) {
    throw new Error('Email、Password or Name is required')
  }

  // ユーザー新規作成
  const user = await context.prisma.user.create({
    data: {
      ...args,
      password,
    },
  })

  const token = jwt.sign({ userId: user.id }, APP_SECRET)

  if (!token) {
    throw new Error('Token is required')
  }

  return {
    token,
    user,
  }
}
```
```graphql:schema.graphql
type Mutation {
  signUp(email: String!, password: String!, name: String!): AuthPayload
}

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

type AuthPayload {
  token: String
  user: User
}
```
`signUp`を例にしますが、引数にemail・password・nameをとり、返り値は`AuthPayload`とし、`token`と`user`を返却します。
そして、リゾルバーでは詳細な処理を記述します。

:::note info
カスタムリゾルバーについて
:::

先ほどの、スキーマ定義ではただ、User型やLink型を実装していたように思えますが、これらの型の`user`や`links`は`カスタムリゾルバー`として定義しています。
つまり、Prismaのスキーマではリレーションを組んでいますが、実際にGraphQLでUserに紐づくLink配列のデータを取得するなどの処理を行うには、リゾルバーが必要となります。

このように、QueryやMutationとは別に自分でリゾルバーを実装することもできます。
これを**カスタムリゾルバー**といいます。
（以下のlinksがカスタムリゾルバー）

```graphql:schema.graphql
type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}
```
以下が、リゾルバーの処理内容です。
やっていることは簡単でユーザーに紐づくlinksを配列で取得しているだけです。

特徴的なのは、引数の部分で、`parent.id`となっていますが、これは親のリゾルバーが返すオブジェクトからの値を扱うことができます。
簡単に言うと、PrismaでUserとLinkはリレーションを組んでいましたが、親テーブルがUserとなるため、取得できるデータは以下のようになるということです。
`parent.id = user.id`
```typescript
export const links = (parent: { id: any }, __: unknown, context: Context) => {
  return context.prisma.user
    .findUnique({
      where: { id: parent.id },
    })
    .links()
}
```

### リゾルバーの引数について
引数についての解説もここでしておこうと思います。
以下の表が全てを表しています。
細かいことは各自で調べてみてください。

| 引数 |解説 |
|---|---|
| parent | 親のリゾルバーが返すオブジェクトの値  |
| args | クライアントから渡される値（フォームの入力値など） |
| context | リクエスト全体で共有される値（認証情報やDB接続情報など） |
| info | 実行されるクエリに関する詳細情報（ほとんど使用しない） |

# Contextでデータを共有する方法
前提の解説が終わったところで、本題です。
引数の解説でもでた`context`でデータを共有する方法について解説します。

Apollo Server v4系を扱っている記事が公式+αくらいしかないので、ベースは公式のものをベースとしています。
以下が、実装コードです。

```typescript:server.ts
import 'dotenv/config'

import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
import { loadSchemaSync } from '@graphql-tools/load'
import { addResolversToSchema } from '@graphql-tools/schema'
import { PrismaClient } from '@prisma/client'
import { join } from 'path'

import { user } from './resolvers/Link'
import { login, post, singUp } from './resolvers/Mutation'
import { feed } from './resolvers/Query'
import { links } from './resolvers/User'
import { getUserId } from './utils'

const prisma = new PrismaClient()

const schema = loadSchemaSync(join(__dirname, './schema.graphql'), {
  loaders: [new GraphQLFileLoader()],
})

// リゾルバー関数
const resolvers = {
  Query: {
    feed: feed,
  },

  Mutation: {
    signUp: singUp,
    login: login,
    post: post,
  },

  Link: {
    user: user,
  },

  User: {
    links: links,
  },
}

const schemaWithResolvers = addResolversToSchema({ schema, resolvers })
const server = new ApolloServer({
  schema: schemaWithResolvers,
})

const startServer = async () => {
  const { url } = await startStandaloneServer(server, {
    context: async ({ req }) => ({
      ...req,
      prisma,
      userId: req && req.headers.authorization ? getUserId(req) : null,
    }),
    listen: { port: 4000 },
  })
  console.log(`🚀  Server ready at: ${url}`)
}

startServer()
```

contextの実装部分は以下です。
```typescript
const startServer = async () => {
  const { url } = await startStandaloneServer(server, {
    context: async ({ req }) => ({
      ...req,
      prisma,
      userId: req && req.headers.authorization ? getUserId(req) : null,
    }),
    listen: { port: 4000 },
  })
  console.log(`🚀  Server ready at: ${url}`)
```
公式によるとContextの役割は以下とのことです。
>GraphQL 操作中に、contextValue という名前のオブジェクトを作成することで、サーバーのリゾルバーとプラグイン全体でデータを共有できます。
>
>contextValue を介して、認証スコープ、データをフェッチするソース、データベース接続、カスタムフェッチ関数など、リゾルバーに必要な便利なものを渡すことができます。 データローダーを使用してリゾルバー間でリクエストをバッチ処理している場合は、データローダーを共有 contextValue にアタッチすることもできます。
>
>コンテキスト関数は非同期であり、オブジェクトを返す必要があります。 このオブジェクトは、contextValue という名前を使用してサーバーのリゾルバーとプラグインにアクセスできるようになります。
>
>コンテキスト関数を選択した統合関数 (expressMiddleware や startStandaloneServer など) に渡すことができます。
>
>サーバーはリクエストごとに context 関数を 1 回呼び出し、各リクエストの詳細 (HTTP ヘッダーなど) を使用して contextValue をカスタマイズできるようにします。

要約するに、`startStandaloneServer`などの統合関数にContextを渡すことで、認証情報などリゾルバーに必要な情報をリゾルバーやプラグイン全体で共有できるというもののようです。
また、以下の2つの要件を満たす必要もあるようです。
- 非同期で実装する必要
- オブジェクトを返す必要がある

つまり、私の実装では、以下をリゾルバー全体で共有できるということです。

- req:リクエスト情報
- prisma: prismaClientのインスタンス
- userId: 認証情報（ユーザーIDからトークンを生成するためuserIdが認証情報となる）

## contextの利用例
実際にいい例として投稿APIの実装を見てみましょう。
```typescript:Mutation.ts
export const post = async (
  _: unknown,
  args: { description: string; url: string },
  context: Context
) => {
  const { userId } = context

  const newLink = await context.prisma.link.create({
    data: {
      url: args.url,
      description: args.description,
      user: { connect: { id: userId } },
    },
  })

  return newLink
}
```
Contextの型は以下のようにしています。
```typescript:Context.ts
import type { Prisma, PrismaClient } from '@prisma/client'
import type { DefaultArgs } from '@prisma/client/runtime'

export type Context = {
  prisma: PrismaClient<
    Prisma.PrismaClientOptions,
    never,
    Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined,
    DefaultArgs
  >
  userId: number
}
```

contextが使用されているのは、`userIdの分割代入`と、`context.prisma`の部分です。
このようにすることで、他のリゾルバー関数でも同様に`context.prisma.メソッド`と実装できます。
メリットとしては、いちいち認証の実装やPrismaClientのインスタンス化が不要なるので、記述量が減ったり、メモリの使用量が減るなどが挙げられます。

ちなみに、`user: { connect: { id: userId } }`の部分ですが、PrismaでLinkとUserはリレーションを組んでいますよね。この記述はその関係性を表しています。

`connect`オプションを使用することで既存のUserレコード（既存ユーザー）にLinkのレコード（ここでは新規作成されたLink）を関連づけられます。キーとしては、userIdで紐付けを行うので、`{ id: userId }`としています。
よくある勘違いですが、「`{ id: userId }`がなぜ、`userId`になるのか」「LinkのIDではないのか」という勘違いがあります。
これについては、`user: {connect: ・・・}`と最初に`user`という項目を指定しているので、`{ id: userId}`であっているということになります。

# おわりに
最後になりますが、当然、公式の方が情報が正確ですので、その点はご留意ください。
「参考文献」のセクションの「**Context and contextValue**」が公式ドキュメントになりますので、気になる方は、そちらもチェックしてみてください。

# 参考文献
> - [Context and contextValue](https://www-apollographql-com.translate.goog/docs/apollo-server/data/context/?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=ja&_x_tr_pto=sc)
> - [Apollo Server v4、Next.js、Prisma でGraphQLのセットアップ](https://zenn.dev/s_takashi/articles/4f918f47779de2)