普段、GraphQLを用いてAPI開発をしているのですが、複数のResolverをチェーンさせてビジネスロジックを実行する設計である場合、Query/Mutationで受け取ったArgumentを後続のResolverで利用したいケースがあるかと思います。
今回はそのやり方をまとめてみます。
前提
作成するGraphQLのスキーマは、こちらの記事で使ったものをもとにします。
Argumentを後続のResolverで利用する必要があるような要件にしたいので、このグラフを少しだけいじって、Residentノードに「sensitiveData」を追加します。
今回は、こちらのスキーマをもとに、Query「prefecture」がuserIdを引数に受け取り、そのユーザーが管理者権限をもつ場合のみ、sensitiveDataを返却するという要件を想定してみます。
実装例
結論としては、Contextに引数で受け取った値をセットして、それを後続のResolverで参照するというやり方があります。
具体的には、下記のようなコードになります。
まずは、Contextに引数で受け取った値をセットする部分のコードです。
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をセットしています。
@Query(() => [Prefecture])
public prefectures(@Args() args: PrefecturesArgs, @Context() context: any): Observable<Prefecture[]> {
// ContextにユーザーIDをセット
context.userId = args.userId;
参考:Contextとは?
GraphQLでは、コンテキストは特定の実行のすべてのリゾルバによって共有されるオブジェクトです。認証情報、現在のユーザー、データベース接続、データソースなど、ビジネスロジックの実行に必要なデータを保持するのに便利です。
次に、Contextにセットした値を参照する部分のコードです。
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';
}
}
簡単ですね。では、サーバーを起動してリクエストを投げてみましょう。
まずは異常系から。
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
}
]
}
]
}
]
}
}
正常系のパターン。
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で利用する方法でした。
詳細なソースコードはこちらです。