Help us understand the problem. What is going on with this article?

GraphQL PaginationのNestJSでの実装

はじめに

CADDiでバックエンドエンジニアをしている狭間と申します。
この記事は CADDi Advent Calendar 18日目の記事です。昨日は,@wolf_cppさんによるValgrindでコード解析してみるでした!

CADDiではBFFにGraphQL用いているシステムが幾つかあり、私が担当しているシステムでもGraphQLを利用しています。
しばらく前にPaginationを実装したのですが、そこを改善したいと思っており、今回はそれについて書きたいと思います。
私が担当しているシステムではNestJSを使用しており、NestJSを使った実装例を紹介できればと思います。

GraphQLでのPagination

GraphQLの公式ではベストプラクティスとして、こういった方法が上げらています。Relay-style cursor pagination とか Relay-style pagination と呼ばれているようです。
かなりリッチな機能なので用途によってはtoo muchな気がしますが、拡張性も考えてこれに従って実装することにしました。
Apollo Client 3.0に Relay-style cursor pagination 向けのキャッシュ機能があるそうです。
明日の記事で @gushernobindsme さんがこのあたりを解説してくれているので、そちらもぜひ読んでみてください。

NestJSでの実装

NestJSの公式にPaginationの例がいくつか上がっているのですが(Resolverの例とかModelの例)、部分的にしか記載がないので、そのあたりを解説できればと思います。
NestJSにちょうどよいサンプルがあったのでこれを拡張していきます。
NestJSではGraphQLのスキーマを事前に定義しておいて、そこからコードをジェネレートする方法とコードからスキーマを生成する方法があるのですが、今回は後者を使用します。

Paginationのモデル定義

Paginationの実装にあたって、まずはこれを参考にPaginationの定義を用意します。

page-info.model.ts
import { ObjectType } from "@nestjs/graphql";

@ObjectType("PageInfo")
export class PageInfo {
  startCursor: string;
  endCursor: string;
  hasNextPage: boolean;
  hasPreviousPage: boolean;
}
pagnated-connection.model.ts
import { Field, ObjectType, Int } from "@nestjs/graphql";
import { Type } from "@nestjs/common";
import { PageInfo } from "./page-info.model";

export function PaginatedConnection<T>(classRef: Type<T>): any {
  @ObjectType({ isAbstract: true })
  class AbstractConnectionType {
    @Field((type) => Int)
    totalCount: number;

    @Field((type) => [AbstractEdgeType], { nullable: true })
    edges: AbstractEdgeType[];

    @Field((type) => PageInfo)
    pageInfo: PageInfo;
  }

  @ObjectType(`${classRef.name}Edge`)
  abstract class AbstractEdgeType {
    @Field((type) => String)
    cursor: string;

    @Field((type) => classRef)
    node: T;
  }
  return AbstractConnectionType;
}

page-info.model.tsについては通常のclassなので説明不要かと思います。
問題はpagnated-connection.model.tsの方かなと思います。
PaginatedConnectionでclassを動的に生成するような処理になっています。(ここでanyを返してしまうのが微妙なのですが、解決できず...)

Paginationモデルの適用

実際のモデルに適用するには下記のようにします。

paginated-recipe.model.ts
import { Recipe } from "./recipe.model";

import { ObjectType } from "@nestjs/graphql";
import { PaginatedConnection } from "src/common/pagination/model/pagnated-connection.model";

@ObjectType()
export class PaginatedRecipe extends PaginatedConnection(Recipe) {}

先程紹介した PaginatedConnection により動的にclassが生成され、それを継承しています。
なので概念的には下記のような形になっているはずです。

class PaginatedRecipe {
  @Field((type) => Int)
  totalCount: number;

  @Field((type) => [AbstractEdgeType], { nullable: true })
  edges: AbstractEdgeType[];

  @Field((type) => PageInfo)
  pageInfo: PageInfo;

  @ObjectType("RecipeEdge")
  class AbstractEdgeType {
    @Field((type) => String)
    cursor: string;

    @Field((type) => Recipe)
    node: Recipe;
  }
}

あとはこれをresolverで返すようにしてあげれば完成です。

(抜粋)recipes.resolver.ts
@Resolver((of) => Recipe)
export class RecipesResolver {
  constructor(private readonly recipesService: RecipesService) {}

  // @Query(returns => [Recipe])
  // recipes(@Args() recipesArgs: RecipesArgs): Promise<Recipe[]> {
  //  return this.recipesService.findAll(recipesArgs);
  // }

 // 元のクエリをPaginationしたものに置き換え
  @Query((returns) => PaginatedRecipe)
  recipes(@Args() args: PaginationRecipesArgs): Promise<PaginatedRecipe> {
    return this.recipesService.findAll(args);
  }
}

パラメータは下記のようにしてあります。

pagination.args.ts
import { ArgsType, Field, Int } from "@nestjs/graphql";

@ArgsType()
export class PaginationArgs {
  @Field((type) => Int)
  first?: number;

  @Field((type) => Int)
  after?: number;

  @Field((type) => String)
  last?: string;

  @Field((type) => String)
  before?: string;
}
pagination.args.ts
import { ArgsType, Field } from '@nestjs/graphql';
import { PaginationArgs } from 'src/common/pagination/dto/pagination.args';

@ArgsType()
export class PaginationRecipesArgs extends PaginationArgs{
  @Field(type => String)
  title?: string;
}

起動した結果をPlaygroundで確認してみると、期待通りのスキーマ定義になっていることが確認できました。

スクリーンショット 2020-12-16 23.58.01.png

まとめ

NestJSのサンプルを参考にPaginationの定義を共通化することができました。
class定義にanyが残ってしまったので、そこだけもう少しうまくできれば良かったなと思っています。
ただ影響は受けるのはresolverだけなので、service側とのインターフェイスを工夫してあまり影響がないようにできるような気はします。次回そのあたりが書けたらなと思います。

冒頭でも少し触れましたが、明日は @gushernobindsme さんの「Apollo Client 3.0 ではじめる快適キャッシュ生活」です。お楽しみに!

caddi
製造業の受発注プラットフォーム「CADDi」を提供しています。 モノづくりに携わるすべての人が、本来持っている力を最大限に発揮できる社会を実現する。産業の常識を変える「新たな仕組み」をつくります。 「CADDi」は金属加工品のCAD・設計図の解析から複雑な物流を表現するUIまで幅広い開発をしており、常に開発環境に最新の技術をとり入れて、より良いプロダクトを作るように心がけております。
https://corp.caddi.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away