在NestJS中实现GraphQL分页

首先

我是CADDi的后端工程师狭间。
这篇文章是CADDi Advent Calendar的第18天的文章。昨天是由@wolf_cpp先生介绍的使用Valgrind进行代码分析!

CADDi在多个系统中使用了GraphQL作为BFF,并且我负责的系统也使用了GraphQL。前段时间我实施了分页功能,但我想对其进行改进,这次我想写一下关于这个改进的内容。我负责的系统使用了NestJS,希望能够介绍使用NestJS的实现示例。

使用GraphQL进行分页操作

据GraphQL官方文档中的最佳实践,推荐使用类Relay样式的游标分页或Relay样式分页。这是一个非常丰富的功能,根据使用情况可能会感觉过于繁琐,但考虑到可扩展性,我们决定按照此方法进行实现。
据说Apollo Client 3.0提供了针对Relay样式游标分页的缓存功能。
@gushernobindsme先生将在明天的文章中对此进行解释,请一定要阅读。

使用NestJS进行实现

尽管NestJS的官方文档中有关于分页的示例(例如Resolver示例和Model示例),但只有部分内容被提及,因此希望能够对此进行解释。
由于在NestJS中有一个合适的样例,我们将对其进行扩展。
在NestJS中,可以预先定义GraphQL模式,然后通过该模式生成代码,也可以从代码生成模式,而本次将使用后者的方法。

Pagination的模型定义

在实现Pagination之前,首先参考这个来准备Pagination的定义。

import { ObjectType } from "@nestjs/graphql";

@ObjectType("PageInfo")
export class PageInfo {
  startCursor: string;
  endCursor: string;
  hasNextPage: boolean;
  hasPreviousPage: boolean;
}
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,我认为它是一个普通的类,所以不需要解释。
问题可能出在pagnated-connection.model.ts。
在PaginatedConnection中有一些动态生成类的处理。(虽然在这里返回any类型有点微妙,但无法解决……)

应用分页模型

要将其应用于实际模型,请按以下方式操作。

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 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((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);
  }
}

参数已进行如下设定。

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;
}
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样本将分页定义共通化。只是类定义中残留了一些“any”的东西,所以如果能再稍微优化一下那一部分就好了。不过只受到了解析器的影响,所以我觉得可以通过巧妙地设计与服务端的接口来减少影响。下次我希望能写出相关内容。

明天的活动是 @gushernobindsme 先生的「在Apollo Client 3.0下开始愉快的缓存生活」。敬请期待!

bannerAds