[GraphQL] Query/Mutationで受け取った引数を、後続のResolverで利用する

普段、GraphQLを用いてAPI開発をしているのですが、複数のResolverをチェーンさせてビジネスロジックを実行する設計である場合、**Query/Mutationで受け取ったArgumentを後続のResolverで利用したい**ケースがあるかと思います。

今回はそのやり方をまとめてみます。

# 前提
作成するGraphQLのスキーマは、こちらの記事で使ったものをもとにします。

https://qiita.com/nakawodayo/private/69acaf210c41fb4877cf

Argumentを後続のResolverで利用する必要があるような要件にしたいので、このグラフを少しだけいじって、Residentノードに「sensitiveData」を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/f22592f6-4f9c-6c29-ddf5-db1b49469f11.png)

今回は、こちらのスキーマをもとに、**Query「prefecture」がuserIdを引数に受け取り、そのユーザーが管理者権限をもつ場合のみ、sensitiveDataを返却する**という要件を想定してみます。

# 実装例
結論としては、**Contextに引数で受け取った値をセットして、それを後続のResolverで参照する**というやり方があります。

具体的には、下記のようなコードになります。

まずは、Contextに引数で受け取った値をセットする部分のコードです。

```typescript:prefectures.resolver.ts
import { Args, Context, Query, Resolver } from '@nestjs/graphql';
import { Prefecture } from '../prefecture/models/prefecture.model';
import { PrefecturesArgs } from './models/prefectures.args';
import { EntityModelResident } from './models/entityModelResident';
import { map, Observable, of } from 'rxjs';

@Resolver(() => [Prefecture])
export class PrefecturesResolver {
    @Query(() => [Prefecture])
    public prefectures(@Args() args: PrefecturesArgs, @Context() context: any): Observable<Prefecture[]> {
        // ContextにユーザーIDをセット
        context.userId = args.userId;

        return this.fetchMockData().pipe(
            map((residents) => {
                // 都道府県単位でデータをグループ化
                // key: prefectureId, value: PrefectureのMapを作成
                const prefectureMap = new Map<string, Prefecture>();

                residents.forEach((resident) => {
                    const prefectureId = resident.prefectureId;

                    // 都道府県をマップに追加（初回のみ）
                    if (!prefectureMap.has(prefectureId)) {
                        prefectureMap.set(prefectureId, {
                            id: prefectureId,
                            name: resident.prefectureName,
                            cities: residents.filter((resident) => resident.prefectureId === prefectureId) // ここではcitiesを解決しない（PrefectureResolverの責務）
                        });
                    }
                })

                return Array.from(prefectureMap.values());
            })
        );
    }

    /**
     * Backend APIを呼び出すメソッドのモック
     * @returns 居住する都道府県、市区町村の情報をもった住民の一覧
     */
    private fetchMockData(): Observable<EntityModelResident[]> {
        return of([
            // 前提に記載したモックデータ 
        ]);
    }
}

```

↓ ポイントはここです。引数に受け取ってきたcontextに、ArgumentのuserIdをセットしています。
```typescript:prefectures.resolver.ts
    @Query(() => [Prefecture])
    public prefectures(@Args() args: PrefecturesArgs, @Context() context: any): Observable<Prefecture[]> {
        // ContextにユーザーIDをセット
        context.userId = args.userId;
```

参考：Contextとは？
> GraphQLでは、コンテキストは特定の実行のすべてのリゾルバによって共有されるオブジェクトです。認証情報、現在のユーザー、データベース接続、データソースなど、ビジネスロジックの実行に必要なデータを保持するのに便利です。

https://the-guild.dev/graphql/modules/docs/essentials/context

次に、Contextにセットした値を参照する部分のコードです。

```typescript:resident.resolver.ts
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Resident } from './models/resident.model';
import { GraphQLError } from 'graphql';

@Resolver(() => Resident)
export class ResidentResolver {
    @ResolveField(() => String)
    public sensitiveData(@Parent() parent: Resident, @Context() context: any): string {
        const { userId } = context;
        if (!this.isAuthorized(userId)) {
            throw new GraphQLError('機密情報を閲覧する権限がありません。');
        }
        return parent.sensitiveData;
    }

    /**
     * 機密情報を閲覧できるユーザーかどうかを判定する
     * @param userId
     * @returns
     */
    private isAuthorized(userId?: string) {
        // userIdが'admin'の場合は、機密情報を閲覧できる
        return userId === 'admin';
    }
}

```

簡単ですね。では、サーバーを起動してリクエストを投げてみましょう。
まずは異常系から。

```graphql
query {
  prefectures(userId: "notAdmin"){
    id
    name
    cities {
        id
        name
        residents {
            id
            name
            sensitiveData
        }
    }
  }
}
```
↓ ちゃんとエラーが返ってきます。
```
{
    "errors": [
        {
            "message": "機密情報を閲覧する権限がありません。",
            "locations": [
                {
                    "line": 11,
                    "column": 13
                }
            ],
            "path": [
                "prefectures",
                0,
                "cities",
                0,
                "residents",
                0,
                "sensitiveData"
            ],
            "extensions": {
                "code": "INTERNAL_SERVER_ERROR",
                "stacktrace": [
                    "GraphQLError: 機密情報を閲覧する権限がありません。",
                    "    at ResidentResolver.sensitiveData (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/src/graphql/resident/resident.resolver.ts:11:19)",
                    "    at /home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:67:33",
                    "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
                    "    at async Object.target [as sensitiveData] (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:74:28)"
                ]
            }
        },
        {
            "message": "機密情報を閲覧する権限がありません。",
            "locations": [
                {
                    "line": 11,
                    "column": 13
                }
            ],
            "path": [
                "prefectures",
                0,
                "cities",
                0,
                "residents",
                1,
                "sensitiveData"
            ],
            "extensions": {
                "code": "INTERNAL_SERVER_ERROR",
                "stacktrace": [
                    "GraphQLError: 機密情報を閲覧する権限がありません。",
                    "    at ResidentResolver.sensitiveData (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/src/graphql/resident/resident.resolver.ts:11:19)",
                    "    at /home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:67:33",
                    "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
                    "    at async Object.target [as sensitiveData] (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:74:28)"
                ]
            }
        },
        {
            "message": "機密情報を閲覧する権限がありません。",
            "locations": [
                {
                    "line": 11,
                    "column": 13
                }
            ],
            "path": [
                "prefectures",
                0,
                "cities",
                1,
                "residents",
                0,
                "sensitiveData"
            ],
            "extensions": {
                "code": "INTERNAL_SERVER_ERROR",
                "stacktrace": [
                    "GraphQLError: 機密情報を閲覧する権限がありません。",
                    "    at ResidentResolver.sensitiveData (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/src/graphql/resident/resident.resolver.ts:11:19)",
                    "    at /home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:67:33",
                    "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
                    "    at async Object.target [as sensitiveData] (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:74:28)"
                ]
            }
        },
        {
            "message": "機密情報を閲覧する権限がありません。",
            "locations": [
                {
                    "line": 11,
                    "column": 13
                }
            ],
            "path": [
                "prefectures",
                1,
                "cities",
                0,
                "residents",
                0,
                "sensitiveData"
            ],
            "extensions": {
                "code": "INTERNAL_SERVER_ERROR",
                "stacktrace": [
                    "GraphQLError: 機密情報を閲覧する権限がありません。",
                    "    at ResidentResolver.sensitiveData (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/src/graphql/resident/resident.resolver.ts:11:19)",
                    "    at /home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:67:33",
                    "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
                    "    at async Object.target [as sensitiveData] (/home/pbnakao/ghq/github.com/backend-bff-tutorial/04.graphql-insights/bff/node_modules/.pnpm/@nestjs+core@10.4.11_@nestjs+common@10.4.11_reflect-metadata@0.2.2_rxjs@7.8.1__@nestjs+platfo_2oyf73vunjakn27phs5bu35ali/node_modules/@nestjs/core/helpers/external-context-creator.js:74:28)"
                ]
            }
        }
    ],
    "data": {
        "prefectures": [
            {
                "id": "p001",
                "name": "Tokyo",
                "cities": [
                    {
                        "id": "c001",
                        "name": "Shinjuku",
                        "residents": [
                            {
                                "id": "r001",
                                "name": "Taro Yamada",
                                "sensitiveData": null
                            },
                            {
                                "id": "r002",
                                "name": "Hanako Suzuki",
                                "sensitiveData": null
                            }
                        ]
                    },
                    {
                        "id": "c002",
                        "name": "Shibuya",
                        "residents": [
                            {
                                "id": "r003",
                                "name": "Kenji Tanaka",
                                "sensitiveData": null
                            }
                        ]
                    }
                ]
            },
            {
                "id": "p002",
                "name": "Osaka",
                "cities": [
                    {
                        "id": "c003",
                        "name": "Umeda",
                        "residents": [
                            {
                                "id": "r004",
                                "name": "Mika Sato",
                                "sensitiveData": null
                            }
                        ]
                    }
                ]
            }
        ]
    }
}
```
正常系のパターン。
```graphql
query {
  prefectures(userId: "admin"){
    id
    name
    cities {
        id
        name
        residents {
            id
            name
            sensitiveData
        }
    }
  }
}
```
↓ sensitiveDataを閲覧できています。
```
{
    "data": {
        "prefectures": [
            {
                "id": "p001",
                "name": "Tokyo",
                "cities": [
                    {
                        "id": "c001",
                        "name": "Shinjuku",
                        "residents": [
                            {
                                "id": "r001",
                                "name": "Taro Yamada",
                                "sensitiveData": "xxxxxx"
                            },
                            {
                                "id": "r002",
                                "name": "Hanako Suzuki",
                                "sensitiveData": "yyyyyy"
                            }
                        ]
                    },
                    {
                        "id": "c002",
                        "name": "Shibuya",
                        "residents": [
                            {
                                "id": "r003",
                                "name": "Kenji Tanaka",
                                "sensitiveData": "zzzzzz"
                            }
                        ]
                    }
                ]
            },
            {
                "id": "p002",
                "name": "Osaka",
                "cities": [
                    {
                        "id": "c003",
                        "name": "Umeda",
                        "residents": [
                            {
                                "id": "r004",
                                "name": "Mika Sato",
                                "sensitiveData": null
                            }
                        ]
                    }
                ]
            }
        ]
    }
}
```

以上、Query/Mutationで受け取ったArgumentを後続のResolverで利用する方法でした。

詳細なソースコードはこちらです。

https://github.com/pbnakao/backend-bff-tutorial/tree/main/04.graphql-insights/bff/src/graphql