在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上检查启动结果时,我确认它已经符合预期的模式定义。

总结
我能参考NestJS样本将分页定义共通化。只是类定义中残留了一些“any”的东西,所以如果能再稍微优化一下那一部分就好了。不过只受到了解析器的影响,所以我觉得可以通过巧妙地设计与服务端的接口来减少影响。下次我希望能写出相关内容。
明天的活动是 @gushernobindsme 先生的「在Apollo Client 3.0下开始愉快的缓存生活」。敬请期待!