解析器
解析器提供将 GraphQL 操作(查询、变异或订阅)转换为数据的指令。
它们返回我们在模式中指定的相同形状的数据,可以是同步的,也可以是解析为该形状的结果的承诺。
通常,您会手动创建解析器映射。
与此相反,@nestjs/graphql 包使用您用于注释类的装饰器提供的元数据自动生成解析器映射。
为了演示使用包功能创建 GraphQL API 的过程,我们将创建一个简单的作者 API。
代码优先
在代码优先方法中,我们不遵循手动编写 GraphQL SDL 来创建 GraphQL 模式的典型过程。
相反,我们使用 TypeScript 装饰器从 TypeScript 类定义生成 SDL。
@nestjs/graphql 包通过读取通过装饰器定义的元数据,自动生成模式。
模式优先
GraphQL 模式中的大多数定义都是对象类型。
您定义的每个对象类型都应该表示应用程序客户端可能需要与之交互的域对象。
例如,我们的示例 API 需要能够获取作者及其帖子列表,
因此我们应该定义 Author 类型和 Post 类型以支持此功能。
如果我们使用模式优先方法,我们将使用类似以下的 SDL 定义这样的模式:
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post!]!
}
在这种情况下,使用代码优先方法, 我们使用 TypeScript 类和 TypeScript 装饰器注释这些类的字段来定义模式。 在代码优先方法中,上述 SDL 的等效部分如下:
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from './post';
@ObjectType()
export class Author {
@Field(type => Int)
id: number;
@Field({ nullable: true })
firstName?: string;
@Field({ nullable: true })
lastName?: string;
@Field(type => [Post])
posts: Post[];
}
TypeScript 的元数据反射系统存在一些限制,使得无法确定类由哪些属性组成,或者识别给定属性是可选的还是必需的。
因此,由于这些限制,我们必须在模式定义类中显式使用 @Field() 装饰器,
以提供关于每个字段的 GraphQL 类型和选项性的元数据,或者使用 CLI 插件为我们生成这些元数据。
Author 对象类型,就像任何类一样,由一系列字段组成,每个字段声明一种类型。
字段的类型对应于 GraphQL 类型。
字段的 GraphQL 类型可以是另一个对象类型或标量类型。
GraphQL 标量类型是解析为单个值的原始类型(如 ID、String、Boolean 或 Int)。
除了 GraphQL 内置的标量类型外,您还可以定义自定义标量类型(了解更多)。
上述 Author 对象类型的定义将导致 Nest 生成我们上面显示的 SDL:
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post!]!
}
@Field() 装饰器接受一个可选的类型函数(例如,type => Int),以及可选的选项对象。
类型函数在 TypeScript 类型系统和 GraphQL 类型系统之间存在潜在的歧义时是必需的。 具体来说:对于字符串和布尔类型,它不是必需的;对于数字(必须映射到 GraphQL Int 或 Float), 它是必需的。类型函数应该简单地返回所需的 GraphQL 类型(正如本章中各种示例中所示)。
选项对象可以具有以下任意键/值对:
nullable: 用于指定字段是否可为空(在 SDL 中,默认情况下,每个字段都是非空的);booleandescription: 用于设置字段说明;stringdeprecationReason: 用于将字段标记为已弃用;string
例如:
@Field({ description: `Book title`, deprecationReason: 'Not useful in v2 schema' })
title: string;
您还可以为整个对象类型添加说明或将其标记为已弃用:@ObjectType({ description: 'Author model' })。
当字段是数组时,必须在 Field() 装饰器的类型函数中手动指定数组类型,如下所示:
@Field(type => [Post])
posts: Post[];
使用数组括号符号([]),我们可以指示数组的深度。例如,使用 [[Int]] 可表示整数矩阵。
要声明数组项(而不是数组本身)可为空,请将 nullable 属性设置为 'items',如下所示:
@Field(type => [Post], { nullable: 'items' })
posts: Post[];
如果数组及其项都可为空,请将 nullable 设置为 'itemsAndList'。
现在 Author 对象类型已创建,让我们定义 Post 对象类型。
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field(type => Int)
id: number;
@Field()
title: string;
@Field(type => Int, { nullable: true })
votes?: number;
}
Post 对象类型将导致在 SDL 中生成 GraphQL 架构的以下部分:
type Post {
id: Int!
title: String!
votes: Int
}
首先是代码解析器
到目前为止,我们已经定义了可以存在于我们的数据图中的对象(类型定义), 但客户端还没有一种方式与这些对象进行交互。 为了解决这个问题,我们需要创建一个解析器类。 在代码优先方法中,解析器类既定义解析器函数又生成Query类型
@Resolver(of => Author)
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField()
async posts(@Parent() author: Author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
所有装饰器(例如,@Resolver、@ResolveField、@Args 等)均从 @nestjs/graphql 包导出。
可以定义多个解析器类。Nest 将在运行时将它们合并。有关代码组织的更多信息, 请参见下面的模块部分。
AuthorsService 和 PostsService 类中的逻辑可以是简单或复杂的,取决于需要。
此示例的主要目的是展示如何构建解析器以及它们如何与其他提供者交互。
在上面的示例中,我们创建了 AuthorsResolver,定义了一个查询解析器函数和一个字段解析器函数。
要创建解析器,我们创建一个类,将解析器函数作为方法,并使用 @Resolver() 装饰器注释该类。
在这个例子中,我们定义了一个查询处理程序,根据请求中的 id 获取作者对象。
要指定该方法是查询处理程序,使用 @Query() 装饰器。
传递给 @Resolver() 装饰器的参数是可选的,但当我们的图变得非常复杂时会发挥作用。
它用于为字段解析器函数提供一个父对象,因为它们在对象图中向下遍历时使用。
在我们的例子中,由于类包括字段解析器函数(用于 Author 对象类型的 posts 属性),
因此我们必须向 @Resolver() 装饰器提供一个值,
以指示该类是所有在此类中定义的字段解析器的父类型(即相应的 ObjectType 类名)。
从示例中清楚可见,编写字段解析器函数时,需要访问父对象(正在解析的字段是其成员的对象)。
在这个例子中,我们使用字段解析器为作者的帖子数组填充了一个调用服务的字段解析器,该服务以作者的 id 作为参数。
因此,在 @Resolver() 装饰器中标识父对象的需求变得明显。
请注意在字段解析器中使用 @Parent() 方法参数装饰器来提取对父对象的引用。
我们可以定义多个 @Query() 解析器函数(在此类中以及在任何其他解析器类中),
它们将汇总到生成的 SDL 中的单个 Query 类型定义中,并在解析器映射中具有适当的条目。
这允许您在靠近使用的模型和服务的模块中定义查询,并将其组织得井井有条。
Nest CLI 提供了一个生成器(原理图),可以自动生成所有样板代码,以帮助我们避免执行所有这些步骤, 使开发人员体验更简单。阅读有关此功能的更多信息。
查询类型名称
在上面的示例 中,@Query() 装饰器根据方法名生成 GraphQL 模式查询类型名称。
例如,请考虑上面示例中的以下构造:
@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
这将在我们的模式中生成如下作者查询的条目(查询类型 使用与方法名相同的名称):
type Query {
author(id: Int!): Author
}
在这里了解更多关于 GraphQL 查询的信息。
通常,我们更喜欢解耦这些名称;
例如,我们更喜欢为查询处理程序方法使用 getAuthor() 这样的名称,但仍然使用 author 作为查询类型名称。
我们可以通过将名称映射作为 @Query() 和 @ResolveField() 装饰器的参数传递来轻松实现这一点,
如下所示:
@Resolver(of => Author)
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField('posts', returns => [Post])
async getPosts(@Parent() author: Author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
上面的 getAuthor 处理程序方法将导致在 SDL 中生成如下部分的 GraphQL 模式:
type Query {
author(id: Int!): Author
}
查询装饰器选项
@Query() 装饰器的选项对象(我们在上面传递了 {name: 'author'})接受许多键/值对:
name: 查询的名称;stringdescription: 将用于生成 GraphQL 模式文档的说明(例如,在 GraphQL playground 中);stringdeprecationReason: 将查询标记为已弃用的元数据(例如,在 GraphQL playground 中);stringnullable: 查询是否可以返回空数据响应;boolean或'items'或'itemsAndList'(有关'items'和'itemsAndList'的详细信息,请参见上文)
Args 装饰器选项
使用 @Args() 装饰器从请求中提取参数,以在方法处理程序中使用。
这与从 REST 路由参数中提取参数的方式非常相似。
通常,@Args() 装饰器会很简单,不需要像上面的 getAuthor() 方法那样需要对象参数。
例如,如果标识符的类型是字符串,则以下构造足够简单,只需从传入的 GraphQL 请求中提取命名字段以用作方法参数。
@Args('id') id: string
在 getAuthor() 的情况下,使用了 number 类型,这带来了挑战。
number TypeScript 类型不提供有关预期的 GraphQL 表示(例如,Int vs Float)的足够信息。
因此,我们必须显式传递类型引用。我们通过向 Args() 装饰器传递第二个参数来完成这个操作,包含参数选项,
如下所示:
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
options 对象允许我们指定以下可选的键值对:
type: 返回 GraphQL 类型的函数defaultValue: 默认值;anydescription: 描述元数据;stringdeprecationReason: 使字段过时并提供描述的元数据;stringnullable: 字段是否可为空
查询处理程序方法可以接受多个参数。假设我们想根据其 firstName 和 lastName 获取作者。
在这种情况下,我们可以调用 @Args 两次:
getAuthor(
@Args('firstName', { nullable: true }) firstName?: string,
@Args('lastName', { defaultValue: '' }) lastName?: string,
) {}
专用参数类
对于内联的 @Args() 调用,像上面的例子那样,代码会变得臃肿。
相反,您可以创建一个专用的 GetAuthorArgs 参数类,并在处理程序方法中访问它,
如下所示:
@Args() args: GetAuthorArgs
使用 @ArgsType() 创建 GetAuthorArgs 类,如下所示:
import { MinLength } from 'class-validator';
import { Field, ArgsType } from '@nestjs/graphql';
@ArgsType()
class GetAuthorArgs {
@Field({ nullable: true })
firstName?: string;
@Field({ defaultValue: '' })
@MinLength(3)
lastName: string;
}
由于 TypeScript 的元数据反射系统限制,必须使用 @Field 装饰器手动指示类型和可选性,
或使用 CLI 插件。
这将导致在 SDL 中生成如下部分的 GraphQL 模式:
type Query {
author(firstName: String, lastName: String = ''): Author
}
请注意,像 GetAuthorArgs 这样的参数类与 ValidationPipe 非常搭配(了解更多)。
类继承
可以使用标准的 TypeScript 类继承来创建具有通用实用程序类型功能(字段和字段属性、验证等)的基类,这些功能可以被扩展。 例如,您可能有一组总是包含标准偏移和限制字段的分页相关参数,但也包含其他类型特定的索引字段。 您可以设置如下的类层次结构。
基本 @ArgsType() 类:
@ArgsType()
class PaginationArgs {
@Field((type) => Int)
offset: number = 0;
@Field((type) => Int)
limit: number = 10;
}
基本 @ArgsType() 类的类型特定子类:
@ArgsType()
class GetAuthorArgs extends PaginationArgs {
@Field({ nullable: true })
firstName?: string;
@Field({ defaultValue: '' })
@MinLength(3)
lastName: string;
}
对于 @ObjectType() 对象可以采取相同的方法。在基类上定义通用属性:
@ObjectType()
class Character {
@Field((type) => Int)
id: number;
@Field()
name: string;
}
在子类上添加类型特定属性:
@ObjectType()
class Warrior extends Character {
@Field()
level: number;
}
您还可以在解析器中使用继承。可以通过组合继承和 TypeScript 泛型来确保类型安全。
例如,要创建具有通用 findAll 查询的基类,请使用以下结构:
function BaseResolver<T extends Type<unknown>>(classRef: T): any {
@Resolver({ isAbstract: true })
abstract class BaseResolverHost {
@Query((type) => [classRef], { name: `findAll${classRef.name}` })
async findAll(): Promise<T[]> {
return [];
}
}
return BaseResolverHost;
}
注意以下事项:
- 需要明确的返回类型(上面是
any):否则 TypeScript 将抱怨使用私有类定义。建议:定义接口而不是使用any。 Type从@nestjs/common包中导入isAbstract: true属性表示不应为此类生成 SDL(模式定义语言语句)。请注意,您也可以为其他类型设置此属性以抑制 SDL 生成。
以下是如何生成 BaseResolver 的具体子类的示例:
@Resolver((of) => Recipe)
export class RecipesResolver extends BaseResolver(Recipe) {
constructor(private recipesService: RecipesService) {
super();
}
}
这个结构将生成以下的 SDL:
type Query {
findAllRecipe: [Recipe!]!
}
泛型
我们在上面看到了泛型的一个用法。这个强大的 TypeScript 特性可以用于创建有用的抽象。 例如,下面是基于此文档的样本基于游标的分页实现:
import { Field, ObjectType, Int } from '@nestjs/graphql';
import { Type } from '@nestjs/common';
interface IEdgeType<T> {
cursor: string;
node: T;
}
export interface IPaginatedType<T> {
edges: IEdgeType<T>[];
nodes: T[];
totalCount: number;
hasNextPage: boolean;
}
export function Paginated<T>(classRef: Type<T>): Type<IPaginatedType<T>> {
@ObjectType(`${classRef.name}Edge`)
abstract class EdgeType {
@Field((type) => String)
cursor: string;
@Field((type) => classRef)
node: T;
}
@ObjectType({ isAbstract: true })
abstract class PaginatedType implements IPaginatedType<T> {
@Field((type) => [EdgeType], { nullable: true })
edges: EdgeType[];
@Field((type) => [classRef], { nullable: true })
nodes: T[];
@Field((type) => Int)
totalCount: number;
@Field()
hasNextPage: boolean;
}
return PaginatedType as Type<IPaginatedType<T>>;
}
有了上面定义的基类,我们现在可以轻松地创建继承此行为的专业类型。例如:
@ObjectType()
class PaginatedAuthor extends Paginated(Author) {}
首先定义模式
如前一章所述, 采用模式优先方法,我们首先手动在 SDL 中定义模式 类型(了解更多)。 考虑以下 SDL 类型定义。
为了方便起见,在本章中,我们已经将所有 SDL 聚合在一个位置(例如,一个 .graphql 文件,如下所示)。
在实践中,您可能会发现以模块化的方式组织代码更合适。
例如,创建包含每个领域实体的类型定义以及相关服务、解析器代码和 Nest 模块定义类的个体 SDL 文件,
放在专用的实体目录中,Nest 将在运行时汇总所有单独的模式类型定义。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String!
votes: Int
}
type Query {
author(id: Int!): Author
}
首先模式解析器
上面的模式公开了一个单一的查询 - author(id: Int!): Author。
在此处了解有关 GraphQL 查询的更多信息。
现在让我们创建一个 AuthorsResolver 类来解析作者查询:
@Resolver('Author')
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query()
async author(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField()
async posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
所有装饰器(例如,@Resolver、@ResolveField、@Args 等)都是从 @nestjs/graphql 包中导出的。
AuthorsService 和 PostsService 类中的逻辑可以是简单或复杂的,取决于需要。 此示例的主要目的是展示如何构建解析器以及它们如何与其他提供者交互。
@Resolver() 装饰器是必需的。
它接受一个可选的字符串参数,该参数是类的名称。
每当类包括 @ResolveField() 装饰器时,都需要此类名称,
以通知 Nest 装饰的方法与父类型关联(在我们当前的示例中是 Author 类)。
或者,可以在每个方法上设置 @Resolver(),而不是在类的顶部设置:
@Resolver('Author')
@ResolveField()
async posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
在这种情况下(在方法级别上的 @Resolver() 装饰器),如果在类中有多个 @ResolveField() 装饰器,
必须为所有这些装饰器都添加 @Resolver()。这被认为不是最佳实践(因为它会创建额外的开销)。
传递给 @Resolver() 的任何类名参数都不影响查询(@Query() 装饰器)或变异(@Mutation() 装饰器)。
在方法级别使用 @Resolver 装饰器不支持代码优先方法。
在上面的示例中,@Query() 和 @ResolveField() 装饰器根据方法名与基于 GraphQL 模式类型关联。
例如,考虑上面示例中的以下构造:
@Query()
async author(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
这将在我们的模式中生成如下作者查询的条目(查询类型使用与方法名相同的名称):
type Query {
author(id: Int!): Author
}
传统上,我们更喜欢解耦这些,为解析器方法使用 getAuthor() 或 getPosts() 这样的名称。
我们可以通过将映射名称作为参数传递给装饰器来轻松实现这一点,如下所示:
@Resolver('Author')
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query('author')
async getAuthor(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField('posts')
async getPosts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
Nest CLI 提供了一个生成器(原理图),它会自动生成所有样板代码,以帮助我们避免做所有这些工作, 并使开发人员体验变得更加简单。 在这里了解更多关于此功能的信息。
生成类型
假设我们使用模式优先方法并启用了生成类型的功能(如上一章节中所示,使用 outputAs: 'class'),
一旦运行应用程序,它将生成以下文件(在您在 GraphQLModule.forRoot() 方法中指定的位置)。
例如,在 src/graphql.ts 中:
export (class Author {
id: number;
firstName?: string;
lastName?: string;
posts?: Post[];
})
export class Post {
id: number;
title: string;
votes?: number;
}
export abstract class IQuery {
abstract author(id: number): Author | Promise<Author>;
}
通过生成类(而不是生成 接口)的方式,您可以在模式优先方法中与声明式验证装饰器结合使用,这是一种非常有用的技术。
例如,您可以向生成的 CreatePostInput 类添加 class-validator 装饰器,
如下所示,以强制执行title字段的最小和最大字符串长度:
import { MinLength, MaxLength } from 'class-validator';
export class CreatePostInput {
@MinLength(3)
@MaxLength(50)
title: string;
}
要启用输入(和参数)的自动验证,请使用 ValidationPipe。
在这里了解更多关于验证的信息,更具体地了解管道的信息。
但是,如果直接在自动生成的文件中添加装饰器,它们将在每次生成文件时被覆盖。 相反,创建一个单独的文件,并简单地扩展生成的类。
import { MinLength, MaxLength } from 'class-validator';
import { Post } from '../../graphql.ts';
export class CreatePostInput extends Post {
@MinLength(3)
@MaxLength(50)
title: string;
}
GraphQL 参数装饰器
我们可以使用专用装饰器访问标准的 GraphQL 解析器参数。下面是 Nest 装饰器和它们所代表的普通 Apollo 参数的比较。
@Root() 和 @Parent() | root 或 parent |
|---|---|
@Context(param?: string) | context 或 context[param] |
@Info(param?: string) | info 或 info[param] |
@Args(param?: string) | args 或 args[param] |
这些参数具有以下含义:
root:包含从父字段解析器返回的结果的对象,或者在顶级Query字段的情况下,从服务器配置传递的rootValue。context:在特定查询中所有解析器共享的对象;通常用于包含每个请求的状态。info:包含有关查询执行状态的信息的对象。args:包含在查询中传递到字段的参数的对象。
模块
完成上述步骤后,我们已经以声明方式指定了 GraphQLModule 生成解析器映射所需的所有信息。
GraphQLModule 使用反射来检查通过装饰器提供的元数据,并自动将类转换为正确的解析器映射。
唯一需要注意的另一件事是提供者(即在某个模块中列出为提供者)解析器类(AuthorsResolver),
并在某处导入该模块(AuthorsModule),以便 Nest 能够利用它。
例如,我们可以在 AuthorsModule 中完成这个操作,该模块还可以提供在此上下文中需要的其他服务。
确保在某个地方导入 AuthorsModule(例如,在根模块中,或者在根模块导入的某个其他模块中)。
@Module({
imports: [PostsModule],
providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}
按照所谓的领域模型组织代码是有帮助的(类似于在 REST API 中组织入口点的方式)。
在这种方法中,将您的模型(ObjectType 类)、解析器和服务组合在一起,形成一个表示领域模型的 Nest 模块。
将所有这些组件放在每个模块的单个文件夹中。
当您这样做,并使用 Nest CLI 生成 每个元素时,
Nest 将自动为您将所有这些部分连接在一起(在适当的文件夹中查找文件,为provider和imports数组生成条目等)。