跳到主要内容
目录

GraphQL

发挥 TypeScript 和 GraphQL 的威力

GraphQL 是一种用于 API 的强大查询语言,也是用现有数据实现这些查询的运行时。 它是一种解决 REST API 通常存在的许多问题的优雅方法。 有关背景信息,建议阅读 GraphQL 和 REST 之间的比较。 GraphQL 与 TypeScript 结合使用, 帮助您在 GraphQL 查询中实现更好的类型安全,为您提供端到端的类型支持。

在本章中,我们假设您对 GraphQL 有基本的了解,并专注于如何使用内置的 @nestjs/graphql 模块。 GraphQLModule 可以配置为使用 Apollo server(使用 @nestjs/apollo 驱动)和 Mercurius(使用 @nestjs/mercurius)。 我们为这些经过验证的 GraphQL 包提供了官方集成,以提供在 Nest 中使用 GraphQL 的简单方式 (在此处查看更多集成)。

您还可以构建自己的专用驱动程序(在此处阅读更多信息)。

安装

首先安装所需的软件包:

# 对于 Express 和 Apollo(默认)
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

# 对于 Fastify 和 Apollo
# npm i @nestjs/graphql @nestjs/apollo @apollo/server @as-integrations/fastify graphql

# 对于 Fastify 和 Mercurius
# npm i @nestjs/graphql @nestjs/mercurius graphql mercurius
注意

@nestjs/graphql@>=9@nestjs/apollo^10 软件包与 Apollo v3 兼容 (有关详细信息,请查看 Apollo Server 3 迁移指南), 而 @nestjs/graphql@^8 仅支持 Apollo v2(例如,apollo-server-express@2.x.x 软件包)。

概述

Nest 提供了两种构建 GraphQL 应用程序的方式,即代码优先和模式优先方法。 您应选择最适合您的方法。本 GraphQL 部分的大多数章节分为两个主要部分: 如果采用代码优先,应遵循其中一个部分, 如果采用模式优先,应使用另一个部分。

代码优先方法中,您使用装饰器和 TypeScript 类来生成相应的 GraphQL 模式。 如果您更喜欢仅使用 TypeScript 并避免在语言语法之间切换上下文,此方法将很有用。

模式优先方法中,真相的源头是 GraphQL SDL(模式定义语言)文件。 SDL 是在不同平台之间共享模式文件的一种与语言无关的方式。 Nest 根据 GraphQL 模式自动生成 TypeScript 定义(使用类或接口),以减少编写冗余样板代码的需要。

开始使用 GraphQL 和 TypeScript

提示

在接下来的章节中,我们将集成 @nestjs/apollo 包。如果您想使用 mercurius 包, 请导航到这一节

安装完软件包后,我们可以导入 GraphQLModule 并使用 forRoot() 静态方法进行配置。

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
}),
],
})
export class AppModule {}
备注

对于 mercurius 集成,您应该使用 MercuriusDriverMercuriusDriverConfig。 两者都是从 @nestjs/mercurius 包中导出的。

forRoot() 方法接受一个选项对象作为参数。 这些选项将传递给底层的驱动程序实例(有关可用设置的更多信息,请阅读 ApolloMercurius 文档)。 例如,如果要禁用 playground 并关闭debug模式(对于 Apollo),可以传递以下选项:

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: false,
}),
],
})
export class AppModule {}

在这种情况下,这些选项将被转发到 ApolloServer 构造函数。

GraphQL playground

Playground 是一个图形化、交互式的、内置的 GraphQL IDE, 在与 GraphQL 服务器相同的 URL 上默认可用。要访问 Playground, 您需要配置并运行一个基本的 GraphQL 服务器。要立即查看它, 您可以安装并构建 这里的工作示例。 或者,如果您正在跟随这些代码示例,完成 解析器章节 中的步骤后, 您可以访问 Playground。

安装完毕,并在后台运行应用程序后,可以打开 Web 浏览器, 导航到 http://localhost:3000/graphql(主机和端口可能因您的配置而异)。 然后,您将看到下面显示的 GraphQL Playground。

备注

@nestjs/mercurius 集成不附带内置的 GraphQL Playground 集成。 相反,您可以使用 GraphiQL(设置 graphiql: true)。

多个端点

@nestjs/graphql 模块的另一个有用的功能是能够同时服务多个端点。 这使您可以决定哪些模块应该包含在哪个端点中。 默认情况下,GraphQL 会在整个应用程序中搜索解析器。 要将此扫描限制为仅某些模块,请使用 include 属性。

GraphQLModule.forRoot({
include: [CatsModule],
})
注意

如果您在单个应用程序中使用 @apollo/server 与 @as-integrations/fastify 包处理多个 GraphQL 端点, 请确保在 GraphQLModule 配置中启用 disableHealthCheck 设置。

代码优先

在代码优先方法中,您使用装饰器和 TypeScript 类来生成相应的 GraphQL 模式。

要使用代码优先方法,请首先在选项对象中添加 autoSchemaFile 属性:

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
})

autoSchemaFile 属性值是自动生成的模式将被创建的路径。或者,模式可以在内存中即时生成。 要启用此选项,请将 autoSchemaFile 属性设置为 true

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
})

默认情况下,生成的模式中的类型将按照它们在包含的模块中定义的顺序排列。 要按词典顺序对模式进行排序,请将 sortSchema 属性设置为 true

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
})

示例

一个完整的代码优先示例在这里

模式优先

要使用模式优先方法,请首先在选项对象中添加 typePaths 属性。 typePaths 属性指示 GraphQLModule 应在其中查找您将编写的 GraphQL SDL 模式定义文件的位置。 这些文件将在内存中合并;这允许您将模式拆分为几个文件并将其放置在其解析器附近。

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
})

通常,您还需要具有与 GraphQL SDL 类型相对应的 TypeScript 定义(类和接口)。 手动创建相应的 TypeScript 定义是多余且繁琐的。 这样做会使我们没有单一的真相来源 - 在 SDL 中所做的每个更改都会迫使我们调整 TypeScript 定义。 为了解决这个问题,@nestjs/graphql 包可以从抽象语法树(AST)中 自动生成 TypeScript定义。 要启用此功能,请在配置 GraphQLModule 时添加 definitions 选项属性。

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
},
})

definitions 对象的 path 属性指示要保存生成的 TypeScript 输出的位置。 默认情况下,所有生成的 TypeScript 类型都创建为接口。 要生成类,指定 outputAs 属性的值为 'class'

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
},
})

上述方法在每次应用程序启动时动态生成 TypeScript 定义。 或者,也可以构建一个简单的脚本根据需要生成这些定义。 例如,假设我们创建以下脚本 generate-typings.ts

import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';

const definitionsFactory = new GraphQLDefinitionsFactory();
definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
});

然后,您可以按需运行此脚本:

ts-node generate-typings
备注

您可以预先编译脚本(例如,使用 tsc)并使用 node 执行它。

要为脚本启用 watch 模式(在任何 .graphql 文件更改时自动生成 typings), 请将 watch 选项传递给 generate() 方法。

definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
watch: true,
});

要为每个对象类型自动生成额外的 __typename 字段,请启用 emitTypenameField 选项。

definitionsFactory.generate({
// ...,
emitTypenameField: true,
});

要生成纯字段而无需参数的解析器(查询、变异、订阅),请启用 skipResolverArgs 选项。

definitionsFactory.generate({
// ...,
skipResolverArgs: true,
});

Apollo Sandbox

要在本地开发的 GraphQL IDE 中使用 Apollo Sandbox 而不是 graphql-playground,请使用以下配置:

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';

@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: false,
plugins: [ApolloServerPluginLandingPageLocalDefault()],
}),
],
})
export class AppModule {}

实例

一个完整的模式优先示例在这里

访问生成的模式

在某些情况下(例如端到端测试),您可能希望获取对生成的模式对象的引用。 在端到端测试中,然后您可以使用 graphql 对象运行查询,而无需使用任何 HTTP 监听器。

您可以访问生成的模式(无论是代码优先还是模式优先方法),使用 GraphQLSchemaHost 类:

const { schema } = app.get(GraphQLSchemaHost);
备注

必须在应用程序初始化后调用 GraphQLSchemaHost#schema 获取器(在通过 app.listen()app.init() 方法触发 onModuleInit 钩子后)。

异步配置

当需要异步传递模块选项而不是静态传递时,请使用 forRootAsync() 方法。 与大多数动态模块一样,Nest 提供了处理异步配置的几种技术。

一种技术是使用工厂函数:

GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useFactory: () => ({
typePaths: ['./**/*.graphql'],
}),
})

与其他工厂提供程序一样,我们的工厂函数可以是async的,并且可以通过 inject 注入依赖项。

GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
typePaths: configService.get<string>('GRAPHQL_TYPE_PATHS'),
}),
inject: [ConfigService],
})

或者,您可以使用类而不是工厂配置 GraphQLModule,如下所示:

GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useClass: GqlConfigService,
})

上述构造在 GraphQLModule 内部实例化 GqlConfigService,使用它创建选项对象。 请注意,在这个例子中,GqlConfigService 必须实现 GqlOptionsFactory 接口,如下所示。 GraphQLModule 将在提供的类的实例上调用 createGqlOptions() 方法。

@Injectable()
class GqlConfigService implements GqlOptionsFactory {
createGqlOptions(): ApolloDriverConfig {
return {
typePaths: ['./**/*.graphql'],
};
}
}

如果要重用现有的选项提供程序而不是在 GraphQLModule 内部创建私有副本,请使用 useExisting 语法。

GraphQLModule.forRootAsync<ApolloDriverConfig>({
imports: [ConfigModule],
useExisting: ConfigService,
})

Mercurius 集成

Fastify 用户可以选择使用 @nestjs/mercurius 驱动而不是 Apollo。

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius';

@Module({
imports: [
GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
graphiql: true,
}),
],
})
export class AppModule {}
备注

应用程序运行后,打开浏览器并导航至 http://localhost:3000/graphiql。 您应该会看到 GraphQL IDE

forRoot() 方法采用选项对象作为参数。这些选项将传递到底层驱动程序实例。 在此处阅读有关可用设置的更多信息

第三方集成

GraphQL Yoga

解析器

解析器提供将 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 的等效部分如下:

authors/models/author.model.ts
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 标量类型是解析为单个值的原始类型(如 IDStringBooleanInt)。

备注

除了 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 中,默认情况下,每个字段都是非空的);boolean
  • description: 用于设置字段说明;string
  • deprecationReason: 用于将字段标记为已弃用;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 对象类型。

posts/models/post.model.ts
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类型

authors/authors.resolver.ts
@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 将在运行时将它们合并。有关代码组织的更多信息, 请参见下面的模块部分

备注

AuthorsServicePostsService 类中的逻辑可以是简单或复杂的,取决于需要。 此示例的主要目的是展示如何构建解析器以及它们如何与其他提供者交互。

在上面的示例中,我们创建了 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() 装饰器的参数传递来轻松实现这一点, 如下所示:

authors/authors.resolver.ts
@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: 查询的名称;string
  • description: 将用于生成 GraphQL 模式文档的说明(例如,在 GraphQL playground 中);string
  • deprecationReason: 将查询标记为已弃用的元数据(例如,在 GraphQL playground 中);string
  • nullable: 查询是否可以返回空数据响应;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: 默认值;any
  • description: 描述元数据;string
  • deprecationReason: 使字段过时并提供描述的元数据;string
  • nullable: 字段是否可为空

查询处理程序方法可以接受多个参数。假设我们想根据其 firstNamelastName 获取作者。 在这种情况下,我们可以调用 @Args 两次:

getAuthor(
@Args('firstName', { nullable: true }) firstName?: string,
@Args('lastName', { defaultValue: '' }) lastName?: string,
) {}

专用参数类

对于内联的 @Args() 调用,像上面的例子那样,代码会变得臃肿。 相反,您可以创建一个专用的 GetAuthorArgs 参数类,并在处理程序方法中访问它, 如下所示:

@Args() args: GetAuthorArgs

使用 @ArgsType() 创建 GetAuthorArgs 类,如下所示:

authors/dto/get-author.args.ts
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 类来解析作者查询:

authors/authors.resolver.ts
@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() 这样的名称。 我们可以通过将映射名称作为参数传递给装饰器来轻松实现这一点,如下所示:

authors/authors.resolver.ts
@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 中:

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()rootparent
@Context(param?: string)contextcontext[param]
@Info(param?: string)infoinfo[param]
@Args(param?: string)argsargs[param]

这些参数具有以下含义:

  • root:包含从父字段解析器返回的结果的对象,或者在顶级 Query 字段的情况下,从服务器配置传递的 rootValue
  • context:在特定查询中所有解析器共享的对象;通常用于包含每个请求的状态。
  • info:包含有关查询执行状态的信息的对象。
  • args:包含在查询中传递到字段的参数的对象。

模块

完成上述步骤后,我们已经以声明方式指定了 GraphQLModule 生成解析器映射所需的所有信息。 GraphQLModule 使用反射来检查通过装饰器提供的元数据,并自动将类转换为正确的解析器映射。

唯一需要注意的另一件事是提供者(即在某个模块中列出为提供者)解析器类(AuthorsResolver), 并在某处导入该模块(AuthorsModule),以便 Nest 能够利用它。

例如,我们可以在 AuthorsModule 中完成这个操作,该模块还可以提供在此上下文中需要的其他服务。 确保在某个地方导入 AuthorsModule(例如,在根模块中,或者在根模块导入的某个其他模块中)。

authors/authors.module.ts
@Module({
imports: [PostsModule],
providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}
备注

按照所谓的领域模型组织代码是有帮助的(类似于在 REST API 中组织入口点的方式)。 在这种方法中,将您的模型(ObjectType 类)、解析器和服务组合在一起,形成一个表示领域模型的 Nest 模块。 将所有这些组件放在每个模块的单个文件夹中。 当您这样做,并使用 Nest CLI 生成每个元素时, Nest 将自动为您将所有这些部分连接在一起(在适当的文件夹中查找文件,为providerimports数组生成条目等)。

Mutations

大多数关于 GraphQL 的讨论都侧重于数据获取,但任何完整的数据平台都需要一种修改服务器端数据的方式。 在 REST 中,任何请求都可能导致对服务器的副作用,但最佳实践建议我们不应该在 GET 请求中修改数据。 GraphQL 类似 - 从技术上讲,任何查询都可以被实现为导致数据写入。 然而,与 REST 一样,建议遵循约定,即任何导致写入的操作都应该通过显式发送变更来完成(在此处阅读更多)。

官方的 Apollo 文档使用了一个 upvotePost() 变更的例子。该变更实现了一个增加帖子投票属性值的方法。 为了在 Nest 中创建一个等效的变更,我们将利用 @Mutation() 装饰器。

代码优先

让我们在上一节中使用的 AuthorResolver 中添加另一个方法(请参阅解析器)。

@Mutation(returns => Post)
async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
return this.postsService.upvoteById({ id: postId });
}
提示

所有装饰器(例如 @Resolver@ResolveField@Args 等)都是从 @nestjs/graphql 包中导出的。

这将导致在 GraphQL 架构中生成以下部分:

type Mutation {
upvotePost(postId: Int!): Post
}

upvotePost() 方法将 postId(Int)作为参数,并返回更新后的 Post 实体。 出于解析器部分解释的原因,我们必须显式设置预期的类型。

如果变更需要将对象作为参数传递,我们可以创建一个输入类型。 输入类型是一种特殊的对象类型, 可以作为参数传递(在此处阅读更多)。 要声明输入类型,请使用 @InputType() 装饰器。

import { InputType, Field } from '@nestjs/graphql';

@InputType()
export class UpvotePostInput {
@Field()
postId: number;
}
备注

@InputType() 装饰器接受一个选项对象作为参数,因此您可以例如指定输入类型的描述。 请注意,由于 TypeScript 的元数据反射系统的限制,您必须使用 @Field 装饰器手动指定类型, 或者使用一个 CLI 插件

然后我们可以在解析器类中使用此类型:

@Mutation(returns => Post)
async upvotePost(
@Args('upvotePostData') upvotePostData: UpvotePostInput,
) {}

模式优先

让我们扩展上一节中使用的 AuthorResolver(请参阅解析器)。

@Mutation()
async upvotePost(@Args('postId') postId: number) {
return this.postsService.upvoteById({ id: postId });
}

请注意,上面我们假设业务逻辑已经移到了 PostsService(查询帖子并递增其votes属性)。 PostsService 类中的逻辑可以简单或复杂,这个示例的主要目的是展示解析器如何与其他提供程序进行交互。

最后一步是将我们的变更添加到现有类型定义中。

type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}

type Post {
id: Int!
title: String
votes: Int
}

type Query {
author(id: Int!): Author
}

type Mutation {
upvotePost(postId: Int!): Post
}

现在,upvotePost(postId: Int!): Post 变更现在可以作为我们应用程序的 GraphQL API 的一部分调用。

订阅(Subscriptions)

除了使用查询获取数据和使用变更修改数据之外,GraphQL 规范还支持第三种操作类型,称为订阅(subscription)。 GraphQL 订阅是一种从服务器推送数据到选择监听服务器实时消息的客户端的方式。 订阅类似于查询,因为它们指定要传递给客户端的一组字段,但与立即返回单个响应不同, 它会打开一个通道,并在服务器上发生特定事件时将结果发送到客户端。

订阅的一个常见用例是通知客户端特定事件, 例如创建新对象、更新字段等(在此处阅读更多)。

使用 Apollo driver 启用订阅

要启用订阅,请将 installSubscriptionHandlers 属性设置为 true

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
installSubscriptionHandlers: true,
}),
注意

installSubscriptionHandlers 配置选项已从最新版本的 Apollo server 中删除,并将很快在此包中废弃。 默认情况下,installSubscriptionHandlers 将回退到使用 subscriptions-transport-ws(在此处阅读更多), 但我们强烈建议改用 graphql-ws(在此处阅读更多)库。

要切换到使用 graphql-ws 包,请使用以下配置:

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': true
},
}),
备注

您还可以同时使用这两个包(subscriptions-transport-wsgraphql-ws),例如,为了向后兼容性。

代码优先

使用代码优先方法创建订阅时,我们使用 @Subscription() 装饰器(从 @nestjs/graphql 包中导出)和 graphql-subscriptions 包中提供的 PubSub 类,该类提供了一个简单的发布/订阅 API

以下订阅处理程序通过调用 PubSub#asyncIterator订阅事件。 此方法接受一个参数,即 triggerName,对应于事件主题名称。

const pubSub = new PubSub();

@Resolver((of) => Author)
export class AuthorResolver {
// ...
@Subscription((returns) => Comment)
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
提示

所有装饰器都是从 @nestjs/graphql 包中导出的, 而 PubSub 类是从 graphql-subscriptions 包中导出的。

备注

PubSub 是一个公开简单发布和订阅 API 的类。在此处阅读更多。 请注意,Apollo 文档警告称默认实现不适用于生产环境(在此处阅读更多)。 生产应用程序应使用由外部存储支持的 PubSub 实现(在此处阅读更多)。

这将导致在 GraphQL 架构中生成以下部分:

type Subscription {
commentAdded(): Comment!
}

请注意,订阅根据定义返回一个具有单个顶级属性的对象,该属性的键是订阅的name。 此名称要么继承自订阅处理程序方法的名称(即上面的 commentAdded), 要么通过在 @Subscription() 装饰器的第二个参数中传递带有键名的选项来显式提供,如下所示。

@Subscription(returns => Comment, {
name: 'commentAdded',
})
subscribeToCommentAdded() {
return pubSub.asyncIterator('commentAdded');
}

此结构产生与上述代码示例相同的 SDL,但允许我们将方法名称与订阅解耦

发布

现在,要发布事件,我们使用 PubSub#publish 方法。 这通常在变更内部使用,以在对象图的一部分发生更改时触发客户端更新。例如:

posts/posts.resolver.ts
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
pubSub.publish('commentAdded', { commentAdded: newComment });
return newComment;
}

PubSub#publish 方法接受 triggerName(再次,将其视为事件主题名称) 作为第一个参数,并将事件载荷作为第二个参数。 如上所述,订阅根据定义返回一个值,并且该值具有形状。再次查看我们的 commentAdded 订阅生成的 SDL:

type Subscription {
commentAdded(): Comment!
}

这告诉我们,订阅必须返回具有名为 commentAdded 的顶级属性的对象,该属性的值是 Comment 对象。 重要的一点是,由 PubSub#publish 方法发出的事件载荷的形状必须与订阅期望返回的值的形状相对应。 因此,在我们的上面的示例中,pubSub.publish('commentAdded', { commentAdded: newComment }) 语句发布具有适当形状的 commentAdded 事件载荷。 如果这些形状不匹配,您的订阅将在 GraphQL 验证阶段失败。

过滤订阅

要过滤特定事件,请将 filter 属性设置为过滤函数。 此函数的作用类似于传递给数组 filter 的函数。 它接受两个参数:包含事件载荷的 payload(由事件发布者发送), 以及包含在订阅请求期间传递的任何参数的 variables。 它返回一个布尔值,用于确定是否应将此事件发布给客户端侦听器。

@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string) {
return pubSub.asyncIterator('commentAdded');
}

突变订阅负载

要突变发布的事件载荷,请将 resolve 属性设置为一个函数。 该函数接收事件载荷(由事件发布者发送)并返回适当的值。

@Subscription(returns => Comment, {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
备注

如果使用 resolve 选项,应返回未包装的载荷 (例如,使用我们的示例,直接返回一个 newComment 对象,而不是一个 { commentAdded: newComment } 对象)。

如果需要访问注入的提供程序(例如,使用外部服务验证数据),请使用以下结构。

@Subscription(returns => Comment, {
resolve(this: AuthorResolver, value) {
// "this" 指的是 "AuthorResolver" 的实例
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}

与过滤器一样,相同的构造方式适用于解析:

@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" 指的是 "AuthorResolver" 的实例
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}

模式优先

要在 Nest 中创建等效的订阅,我们将使用 @Subscription() 装饰器。

const pubSub = new PubSub();

@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}

要根据上下文和参数筛选特定事件,请设置 filter 属性。

@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}

要变异已发布的有效负载,我们可以使用解析(resolve)函数。

@Subscription('commentAdded', {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}

如果需要访问注入的提供程序(例如,使用外部服务验证数据),请使用以下结构。

@Subscription('commentAdded', {
resolve(this: AuthorResolver, value) {
// "this" 指的是 "AuthorResolver" 的实例
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}

与过滤器一样,相同的构造方式适用于解析:

@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" 指的是 "AuthorResolver" 的实例
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}

最后一步是更新类型定义文件。

type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}

type Post {
id: Int!
title: String
votes: Int
}

type Query {
author(id: Int!): Author
}

type Comment {
id: String
content: String
}

type Subscription {
commentAdded(title: String!): Comment
}

通过这样做,我们创建了一个单一的 commentAdded(title: String!): Comment 订阅。 您可以在此处找到完整的示例实现。

PubSub

我们在上面实例化了一个本地 PubSub 实例。 首选的方法是将 PubSub 定义为提供者程序, 并通过构造函数(使用 @Inject() 装饰器)注入它。 这样我们就可以在整个应用程序中重复使用该实例。例如,定义一个提供程序如下,然后在需要的地方注入 'PUB_SUB'

{
provide: 'PUB_SUB',
useValue: new PubSub(),
}

自定义订阅服务器

要自定义订阅服务器(例如,更改路径),请使用 subscriptions 选项属性。

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql'
},
}
}),

如果您正在使用 graphql-ws 包进行订阅,请将 subscriptions-transport-ws 键替换为 graphql-ws,如下所示:

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': {
path: '/graphql'
},
}
}),

通过 WebSockets 进行身份验证

检查用户是否经过身份验证可以在 onConnect 回调函数中完成,您可以在subscriptions选项中指定该回调函数。

onConnect 将接收作为第一个参数传递给 SubscriptionClientconnectionParams(阅读更多)。

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'subscriptions-transport-ws': {
onConnect: (connectionParams) => {
const authToken = connectionParams.authToken;
if (!isValid(authToken)) {
throw new Error('Token is not valid');
}
// 从令牌中提取用户信息
const user = parseToken(authToken);
// 将用户信息返回,以便稍后将其添加到上下文中
return { user };
},
}
},
context: ({ connection }) => {
// connection.context 将等于 "onConnect" 回调返回的内容
},
}),

在此示例中,authToken 仅在客户端首次建立连接时由客户端发送。 使用此连接进行的所有订阅都将具有相同的 authToken,因此也具有相同的用户信息。

备注

subscriptions-transport-ws 中存在一个允许连接跳过 onConnect 阶段的错误(阅读更多)。 您不应假设用户在用户启动订阅时调用了 onConnect,并始终检查context是否被填充。

如果您使用 graphql-ws 包,则 onConnect 回调的签名将略有不同:

GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
// 用户验证将保持与上面示例中相同
// 使用 graphql-ws 时,应在 extra 字段中存储额外的上下文值
extra.user = { user: {} };
},
},
},
context: ({ extra }) => {
// 现在,您可以通过 extra 字段访问额外的上下文值
},
});

使用 Mercurius 驱动程序启用订阅

要启用订阅,请将 subscription 属性设置为 true

GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: true,
}),
备注

您还可以传递选项对象来设置自定义发射器、验证传入连接等。 在此处了解更多信息(请参阅subscription)。

代码优先方式

要使用代码优先方法创建订阅,我们使用 @Subscription() 装饰器(从 @nestjs/graphql 包中导出)和来自 mercurius 包的 PubSub 类, 该类提供了一个简单的发布/订阅 API

以下订阅处理程序负责通过调用 PubSub#asyncIterator 订阅事件。 此方法接受一个参数,triggerName,对应于事件主题名称。

@Resolver((of) => Author)
export class AuthorResolver {
// ...
@Subscription((returns) => Comment)
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
}
提示

上面示例中使用的所有装饰器都是从 @nestjs/graphql 包导出的, 而 PubSub 类是从 mercurius 包导出的。

备注

PubSub 是一个公开简单publishsubscribe API 的类。 查看有关如何注册自定义 PubSub 类的此部分

这将导致在 SDL 中生成以下 GraphQL 模式的部分:

type Subscription {
commentAdded(): Comment!
}

请注意,订阅根据定义返回一个对象,该对象具有单个顶级属性,其键是订阅的名称。 此名称可以从订阅处理程序方法的名称继承(即上面的 commentAdded), 或者通过将键name作为第二个参数传递给 @Subscription() 装饰器来显式提供,如下所示。

@Subscription(returns => Comment, {
name: 'commentAdded',
})
subscribeToCommentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}

这个结构产生了与前面代码示例相同的 SDL,但允许我们将方法名称与订阅解耦。

发布

现在,要发布事件,我们使用 PubSub#publish 方法。 通常,这在变异中使用,以触发客户端更新当对象图的一部分发生更改时。例如:

posts/posts.resolve.ts
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
@Context('pubsub') pubSub: PubSub,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
await pubSub.publish({
topic: 'commentAdded',
payload: {
commentAdded: newComment
}
});
return newComment;
}

如上所述,订阅根据定义返回一个值,并且该值具有形状。再次查看我们的 commentAdded 订阅生成的 SDL:

type Subscription {
commentAdded(): Comment!
}

这告诉我们,订阅必须返回一个具有名为 commentAdded 的顶级属性的对象,该属性具有 Comment 对象的值。 要注意的重要一点是,由 PubSub#publish 方法发出的事件有效负载的形状必须对应于从订阅中返回的值的形状。 因此,在我们上面的示例中,pubSub.publish({ topic: 'commentAdded', payload: { commentAdded: newComment } }) 语句发布了一个具有相应形状的 commentAdded 事件有效负载。 如果这些形状不匹配,您的订阅将在 GraphQL 验证阶段失败。

过滤订阅

要过滤特定事件,请将 filter 属性设置为筛选函数。此函数类似于传递给数组过滤器的函数。 它接受两个参数:包含事件有效负载的 payload(由事件发布者发送), 以及在订阅请求期间传递的任何参数的 variables。 它返回一个布尔值,确定是否应将此事件发布给客户端侦听器。

@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string, @Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}

如果需要访问已注入的提供程序(例如,使用外部服务验证数据),请使用以下结构。

@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" 指的是 "AuthorResolver" 的实例
return payload.commentAdded.title === variables.title;
}
})
commentAdded(@Args('title') title: string, @Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}

模式优先方式

要在 Nest 中创建等效的订阅,我们将使用 @Subscription() 装饰器。

const pubSub = new PubSub();

@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
}

要根据上下文和参数筛选特定事件,请设置 filter 属性。

@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}

如果需要访问已注入的提供程序(例如,使用外部服务验证数据),请使用以下结构。

@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" 指的是 "AuthorResolver" 的实例
return payload.commentAdded.title === variables.title;
}
})
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}

最后一步是更新类型定义文件。

type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}

type Post {


id: Int!
title: String
votes: Int
}

type Query {
author(id: Int!): Author
}

type Comment {
id: String
content: String
}

type Subscription {
commentAdded(title: String!): Comment
}

通过这样做,我们创建了一个单一的 commentAdded(title: String!): Comment 订阅。

PubSub

在上面的示例中,我们使用了默认的 PubSub 发射器(mqemitter)。 首选的方法(用于生产环境)是使用 mqemitter-redis。 或者,可以提供自定义的 PubSub 实现(阅读更多内容)。

GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: {
emitter: require('mqemitter-redis')({
port: 6579,
host: '127.0.0.1',
}),
},
});

通过 WebSocket 进行身份验证

检查用户是否经过身份验证可以在subscription选项中指定的 verifyClient 回调函数内完成。

verifyClient 将接收 info 对象作为第一个参数,您可以使用该对象检索请求的标头。

GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: {
verifyClient: (info, next) => {
const authorization = info.req.headers?.authorization as string;
if (!authorization?.startsWith('Bearer ')) {
return next(false);
}
next(true);
},
}
});

标量类型

GraphQL 对象类型有一个名称和字段,但这些字段最终必须解析为某些具体的数据。 这就是标量类型发挥作用的地方:它们表示查询的叶子节点(阅读更多)。 GraphQL 包括以下默认类型:IntFloatStringBooleanID。 除了这些内置类型外,您可能需要支持自定义原子数据类型(例如 Date)。

首选代码方式

代码优先方法提供了五个标量,其中三个是现有 GraphQL 类型的简单别名。

  • IDGraphQLID 的别名) - 表示唯一标识符,通常用于重新获取对象或作为缓存的键
  • IntGraphQLInt 的别名) - 32 位有符号整数
  • FloatGraphQLFloat 的别名) - 64 位双精度浮点值
  • GraphQLISODateTime - UTC 时间的日期时间字符串(默认用于表示 Date 类型)
  • GraphQLTimestamp - 表示日期和时间的有符号整数,作为从 UNIX 纪元开始的毫秒数

默认情况下,GraphQLISODateTime(例如 2019-12-03T09:54:33Z)用于表示 Date 类型。 要改用 GraphQLTimestamp,请将 buildSchemaOptions 对象的 dateScalarMode 设置为 'timestamp', 如下所示:

GraphQLModule.forRoot({
buildSchemaOptions: {
dateScalarMode: 'timestamp',
}
}),

同样,默认情况下,GraphQLFloat 用于表示number类型。 要改用 GraphQLInt,请将 buildSchemaOptions 对象的 numberScalarMode 设置为 'integer', 如下所示:

GraphQLModule.forRoot({
buildSchemaOptions: {
numberScalarMode: 'integer',
}
}),

此外,您还可以创建自定义标量。

覆盖默认标量

要为 Date 标量创建自定义实现,只需创建一个新类。

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('Date', (type) => Date)
export class DateScalar implements CustomScalar<number, Date> {
description = 'Date custom scalar type';

parseValue(value: number): Date {
return new Date(value); // 从客户端获取的值
}

serialize(value: Date): number {
return value.getTime(); // 发送到客户端的值
}

parseLiteral(ast: ValueNode): Date {
if (ast.kind === Kind.INT) {
return new Date(ast.value);
}
return null;
}
}

有了这个,将 DateScalar 注册为提供程序。

@Module({
providers: [DateScalar],
})
export class CommonModule {}

现在我们可以在我们的类中使用 Date 类型。

@Field()
creationDate: Date;

导入自定义标量

要使用自定义标量,导入并将其注册为解析器。 我们将使用 graphql-type-json 包作为演示目的。此 npm 包定义了 JSON GraphQL 标量类型。

首先安装该包:

npm i --save graphql-type-json

安装包后,我们将一个自定义解析器传递给 forRoot() 方法:

import GraphQLJSON from 'graphql-type-json';

@Module({
imports: [
GraphQLModule.forRoot({
resolvers: { JSON: GraphQLJSON },
}),
],
})
export class AppModule {}

现在我们可以在我们的类中使用 JSON 类型。

@Field((type) => GraphQLJSON)
info: JSON;

对于一套有用的标量,可以查看 graphql-scalars 包。

创建自定义标量

要定义自定义标量,请创建一个新的 GraphQLScalarType 实例。我们将创建一个自定义的 UUID标量。

const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function validate(uuid: unknown): string | never {
if (typeof uuid !== "string" || !regex.test(uuid)) {
throw new Error("invalid uuid");
}
return uuid;
}

export const CustomUuidScalar = new GraphQLScalarType({
name: 'UUID',
description: 'A simple UUID parser',
serialize: (value) => validate(value),
parseValue: (value) => validate(value),
parseLiteral: (ast) => validate(ast.value)
})

我们传递一个自定义解析器给 forRoot() 方法:

@Module({
imports: [
GraphQLModule.forRoot({
resolvers: { UUID: CustomUuidScalar },
}),
],
})
export class AppModule {}

现在我们可以在我们的类中使用 UUID 类型。

@Field((type) => CustomUuidScalar)
uuid: string;

架构优先

要定义自定义标量(了解更多关于标量的信息),创建一个类型定义和一个专用的解析器。 这里(与官方文档一样),我们将使用 graphql-type-json 包作为演示目的。 此 npm 包定义了 JSON GraphQL 标量类型。

首先安装该包:

npm i --save graphql-type-json

安装包后,我们传递一个自定义解析器给 forRoot() 方法:

import GraphQLJSON from 'graphql-type-json';

@Module({
imports: [
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
resolvers: { JSON: GraphQLJSON },
}),
],
})
export class AppModule {}

现在我们可以在我们的类型定义中使用 JSON 标量:

scalar JSON

type Foo {
field: JSON
}

另一种定义标量类型的方法是创建一个简单的类。假设我们想要使用 Date 类型增强我们的架构。

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('Date')
export class DateScalar implements CustomScalar<number, Date> {
description = 'Date custom scalar type';

parseValue(value: number): Date {
return new Date(value); // 从客户端获取的值
}

serialize(value: Date): number {
return value.getTime(); // 发送到客户端的值
}

parseLiteral(ast: ValueNode): Date {
if (ast.kind === Kind.INT) {
return new Date(ast.value);
}
return null;
}
}

有了这个,将 DateScalar 注册为提供程序。

@Module({
providers: [DateScalar],
})
export class CommonModule {}

现在我们可以在类型定义中使用 Date 标量。

scalar Date

默认情况下,所有标量的生成 TypeScript 定义都是 any,这并不特别类型安全。 但是,您可以在指定如何生成类型时配置 Nest 为自定义标量生成 typings

import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';

const definitionsFactory = new GraphQLDefinitionsFactory();

definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
defaultScalarType: 'unknown',
customScalarTypeMapping: {
DateTime: 'Date',
BigNumber: '_BigNumber',
},
additionalHeader: "import _BigNumber from 'bignumber.js'",
});
备注

或者,您可以使用类型引用,例如:DateTime: Date。 在这种情况下,GraphQLDefinitionsFactory 将提取指定类型(Date.name)的 name 属性以生成 TS 定义。 注意:对于非内置类型(自定义类型),需要添加导入语句。

现在,鉴于以下 GraphQL 自定义标量类型:

scalar DateTime
scalar BigNumber
scalar Payload

我们现在将在 src/graphql.ts 中看到以下生成的 TypeScript 定义:

import _BigNumber from 'bignumber.js';

export type DateTime = Date;
export type BigNumber = _BigNumber;
export type Payload = unknown;

在这里,我们使用了 customScalarTypeMapping 属性提供我们希望为自定义标量声明的类型的映射。 我们还提供了 additionalHeader 属性,以便我们可以添加这些类型定义所需的任何导入。 最后,我们添加了一个 defaultScalarType, 将未在 customScalarTypeMapping 中指定的任何自定义标量别名为 unknown, 而不是 any(TypeScript 从 3.0 版本开始推荐使用 unknown 以增加类型安全性)。

备注

请注意,我们从 bignumber.js 导入了 _BigNumber; 这是为了避免循环类型引用

指令

指令可以附加到字段或片段包含,并可以以服务器期望的任何方式影响查询的执行(阅读更多)。 GraphQL 规范提供了几个默认指令:

  • @include(if: Boolean) - 仅在参数为 true 时将此字段包含在结果中
  • @skip(if: Boolean) - 如果参数为 true,则跳过此字段
  • @deprecated(reason: String) - 使用消息标记字段已弃用

指令是一个由@字符引导的标识符,可选择地在命名参数列表之后出现, 它可以出现在 GraphQL 查询和模式语言的几乎任何元素之后。

自定义指令

为了指导当 Apollo/Mercurius 遇到您的指令时应该发生什么,您可以创建一个转换器函数。 此函数使用 mapSchema 函数遍历模式中的位置(字段定义、类型定义等)并执行相应的转换。

import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';

export function upperDirectiveTransformer(
schema: GraphQLSchema,
directiveName: string,
) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const upperDirective = getDirective(
schema,
fieldConfig,
directiveName,
)?.[0];

if (upperDirective) {
const { resolve = defaultFieldResolver } = fieldConfig;

// Replace the original resolver with a function that *first* calls
// the original resolver, then converts its result to upper case
fieldConfig.resolve = async function (source, args, context, info) {
const result = await resolve(source, args, context, info);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
return fieldConfig;
}
},
});
}

现在,在 GraphQLModule#forRoot 方法中使用 transformSchema 函数应用 upperDirectiveTransformer 转换函数:

GraphQLModule.forRoot({
// ...
transformSchema: (schema) => upperDirectiveTransformer(schema, 'upper'),
});

注册后,@upper 指令可以在我们的模式中使用。 然而,应用指令的方式将根据您使用的方法(首选代码方式或模式方式)而有所不同。

代码优先

在首选代码方式中,使用 @Directive() 装饰器应用指令。

@Directive('@upper')
@Field()
title: string;
提示

@Directive() 装饰器是从 @nestjs/graphql 包中导出的。

指令可以应用在字段、字段解析器、输入和对象类型,以及查询、变更和订阅上。 以下是在查询处理程序级别应用指令的示例:

@Directive('@deprecated(reason: "This query will be removed in the next version")')
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
注意

通过 @Directive() 装饰器应用的指令不会反映在生成的模式定义文件中。

最后,请确保在 GraphQLModule 中声明指令,如下所示:

GraphQLModule.forRoot({
// ...,
transformSchema: schema => upperDirectiveTransformer(schema, 'upper'),
buildSchemaOptions: {
directives: [
new GraphQLDirective({
name: 'upper',
locations: [DirectiveLocation.FIELD_DEFINITION],
}),
],
},
}),
提示

GraphQLDirectiveDirectiveLocation 都是从 graphql 包中导出的。

模式优先

在模式优先方式中,直接在 SDL 中应用指令。

directive @upper on FIELD_DEFINITION

type Post {
id: Int!
title: String! @upper
votes: Int
}

接口

与许多类型系统一样,GraphQL 支持接口。 接口是一个抽象类型,它包含一组字段,类型必须包含这些字段以实现接口(阅读更多)。

代码优先方式

在使用首选代码方式时,通过创建一个由 @nestjs/graphql 包导出的 @InterfaceType() 装饰器注释的抽象类来定义 GraphQL 接口。

import { Field, ID, InterfaceType } from '@nestjs/graphql';

@InterfaceType()
export abstract class Character {
@Field((type) => ID)
id: string;

@Field()
name: string;
}
注意

无法使用 TypeScript 接口定义 GraphQL 接口。

这将导致生成 SDL 的 GraphQL 模式的以下部分:

interface Character {
id: ID!
name: String!
}

现在,要实现 Character 接口,请使用 implements 键:

@ObjectType({
implements: () => [Character],
})
export class Human implements Character {
id: string;
name: string;
}
提示

@ObjectType() 装饰器是从 @nestjs/graphql 包导出的。

库生成的默认 resolveType() 函数根据从解析器方法返回的值提取类型。 这意味着您必须返回类实例(不能返回字面的 JavaScript 对象)。

要提供自定义 resolveType() 函数,请将 resolveType 属性传递给传递给 @InterfaceType() 装饰器的选项对象, 如下所示:

@InterfaceType({
resolveType(book) {
if (book.colors) {
return ColoringBook;
}
return TextBook;
},
})
export abstract class Book {
@Field((type) => ID)
id: string;

@Field()
title: string;
}

接口解析器

到目前为止,使用接口,您只能与对象共享字段定义。 如果您还想共享实际的字段解析器实现,可以创建一个专用的接口解析器, 如下所示:

import { Resolver, ResolveField, Parent, Info } from '@nestjs/graphql';

@Resolver(type => Character) // 提醒:Character 是一个接口
export class CharacterInterfaceResolver {
@ResolveField(() => [Character])
friends(
@Parent() character, // 实现 Character 接口的已解析对象
@Info() { parentType }, // 实现 Character 接口的对象类型
@Args('search', { type: () => String }) searchTerm: string,
) {
// 获取角色的朋友
return [];
}
}

现在,friends字段解析器会自动注册到所有实现 Character 接口的对象类型中。

模式优先方式

在模式优先方式中,只需创建一个带有 SDL 的 GraphQL 接口。

interface Character {
id: ID!
name: String!
}

然后,您可以使用生成类型的功能(如快速入门章节所示)生成相应的 TypeScript 定义:

export interface Character {
id: string;
name: string;
}

接口在解析器映射中需要一个额外的 __resolveType 字段,以确定接口应解析为哪种类型。 让我们创建一个 CharactersResolver 类并定义 __resolveType 方法:

@Resolver('Character')
export class CharactersResolver {
@ResolveField()
__resolveType(value) {
if ('age' in value) {
return Person;
}
return null;
}
}
提示

所有装饰器都是从 @nestjs/graphql 包导出的。

联合类型

联合类型与接口非常相似,但它们不能指定类型之间的任何公共字段(阅读更多)。 联合对于从单个字段返回不同的数据类型很有用。

代码优先方式

要定义 GraphQL 联合类型,我们必须定义将组成此联合的类。 按照 Apollo 文档的示例, 我们将创建两个类。首先是 Book

import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Book {
@Field()
title: string;
}

然后是 Author

import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Author {
@Field()
name: string;
}

有了这个,使用从 @nestjs/graphql 包导出的 createUnionType 函数注册 ResultUnion 联合:

export const ResultUnion = createUnionType({
name: 'ResultUnion',
types: () => [Author, Book] as const,
});
注意

createUnionType 函数的 types 属性返回的数组应该给出一个 const 断言。 如果不提供 const 断言,将在编译时生成错误,而在从另一个项目使用时将会出现错误。

现在,我们可以在查询中引用 ResultUnion

@Query(returns => [ResultUnion])
search(): Array<typeof ResultUnion> {
return [new Author(), new Book()];
}

这将导致生成 GraphQL 模式的以下部分:

type Author {
name: String!
}

type Book {
title: String!
}

union ResultUnion = Author | Book

type Query {
search: [ResultUnion!]!
}

库生成的默认 resolveType() 函数将根据从解析器方法返回的值提取类型。 这意味着必须返回类实例而不是字面的 JavaScript 对象。

要提供自定义 resolveType() 函数, 请将 resolveType 属性传递给传递给 createUnionType() 函数的选项对象, 如下所示:

export const ResultUnion = createUnionType({
name: 'ResultUnion',
types: () => [Author, Book] as const,
resolveType(value) {
if (value.name) {
return Author;
}
if (value.title) {
return Book;
}
return null;
},
});

模式优先方式

在模式优先方式中,只需创建带有 SDL 的 GraphQL 联合。

type Author {
name: String!
}

type Book {
title: String!
}

union ResultUnion = Author | Book

然后,您可以使用生成类型的功能(如快速入门章节所示)生成相应的 TypeScript 定义:

export class Author {
name: string;
}

export class Book {
title: string;
}

export type ResultUnion = Author | Book;

联合在解析器映射中需要一个额外的 __resolveType 字段,以确定联合应解析为哪种类型。 另外,请注意必须在任何模块中将 ResultUnionResolver 类注册为提供者。 让我们创建一个 ResultUnionResolver 类并定义 __resolveType 方法。

@Resolver('ResultUnion')
export class ResultUnionResolver {
@ResolveField()
__resolveType(value) {
if (value.name) {
return 'Author';
}
if (value.title) {
return 'Book';
}
return null;
}
}
提示

所有装饰器都是从 @nestjs/graphql 包导出的。

枚举

枚举类型是一种特殊类型的标量,其限制为特定一组允许的值(阅读更多)。这允许您:

  • 验证此类型的任何参数是否是允许的值之一
  • 通过类型系统传达字段将始终是有限值集之一

首选代码方式

在使用首选代码方式时,只需通过简单地创建 TypeScript 枚举来定义 GraphQL 枚举类型。

export enum AllowedColor {
RED,
GREEN,
BLUE,
}

有了这个,使用从 @nestjs/graphql 包导出的 registerEnumType 函数注册 AllowedColor 枚举:

registerEnumType(AllowedColor, {
name: 'AllowedColor',
});

现在,您可以在我们的类型中引用 AllowedColor

@Field(type => AllowedColor)
favoriteColor: AllowedColor;

这将导致生成以下部分的 GraphQL 模式:

enum AllowedColor {
RED
GREEN
BLUE
}

要为枚举提供描述,请将描述属性传递给 registerEnumType() 函数。

registerEnumType(AllowedColor, {
name: 'AllowedColor',
description: 'The supported colors.',
});

要为枚举值提供描述,或将值标记为弃用,请传递 valuesMap 属性,如下所示:

registerEnumType(AllowedColor, {
name: 'AllowedColor',
description: 'The supported colors.',
valuesMap: {
RED: {
description: 'The default color.',
},
BLUE: {
deprecationReason: 'Too blue.',
},
},
});

这将在 GraphQL 模式中生成以下内容:

"""
The supported colors.
"""
enum AllowedColor {
"""
The default color.
"""
RED
GREEN
BLUE @deprecated(reason: "Too blue.")
}

模式优先方式

在模式优先方式中,只需通过 SDL 创建 GraphQL 枚举。

enum AllowedColor {
RED
GREEN
BLUE
}

然后,您可以使用生成类型的功能(如快速入门章节所示)生成相应的 TypeScript 定义:

export enum AllowedColor {
RED
GREEN
BLUE
}

有时后端会强制枚举在内部使用与公共 API 中不同的值。 在这个例子中,API 包含 RED,但在解析器中我们可能使用 #f00 代替(阅读更多)。 为此,请为 AllowedColor 枚举声明一个解析器对象:

export const allowedColorResolver: Record<keyof typeof AllowedColor, any> = {
RED: '#f00',
};
提示

所有装饰器都是从 @nestjs/graphql 包导出的。

然后,与 GraphQLModule#forRoot() 方法的 resolvers 属性一起使用此解析器对象, 如下所示:

GraphQLModule.forRoot({
resolvers: {
AllowedColor: allowedColorResolver,
},
});

字段中间件

注意

本章仅适用于代码优先方式。

字段中间件允许您在字段解析之前或之后运行任意代码。 字段中间件可用于转换字段的结果、验证字段的参数, 甚至检查字段级别的角色(例如,执行中间件函数所需的目标字段的访问权限)。

您可以连接多个中间件函数到一个字段。在这种情况下,它们将按顺序沿着链调用, 其中上一个中间件决定是否调用下一个中间件。middleware数组中中间件函数的顺序很重要。 第一个解析器是“最外层”层,因此它首先执行, 也是最后一个执行的(类似于 graphql-middleware 包)。 第二个解析器是“第二外层”层,因此它第二执行,倒数第二执行。

入门

让我们首先创建一个简单的中间件,它将在将字段值发送回客户端之前将其记录到日志中:

import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql';

const loggerMiddleware: FieldMiddleware = async (
ctx: MiddlewareContext,
next: NextFn,
) => {
const value = await next();
console.log(value);
return value;
};
备注

MiddlewareContext 是一个对象, 由 GraphQL 解析器函数正常接收的相同参数组成({ source,args,context,info }), 而 NextFn 是一个函数,让您执行堆栈中的下一个中间件(绑定到此字段)或实际字段解析器。

注意

字段中间件函数无法注入依赖项,也无法访问 Nest 的 DI 容器,因为它们被设计为非常轻量级, 不应执行任何可能耗时的操作(如从数据库检索数据)。 如果需要调用外部服务/查询数据源的数据,请在绑定到根查询/变异处理程序的守卫/拦截器中执行, 并将其分配给您可以从字段中间件中访问的context对象(具体来说,是 MiddlewareContext 对象)。

请注意,字段中间件必须匹配 FieldMiddleware 接口。 在上面的示例中,我们首先运行 next() 函数(它执行实际的字段解析器并返回字段值), 然后我们将这个值记录到终端。此外,从中间件函数返回的值完全覆盖以前的值, 由于我们不想执行任何更改,因此我们简单地返回原始值。

有了这个,我们可以直接在 @Field() 装饰器中注册我们的中间件,如下所示:

@ObjectType()
export class Recipe {
@Field({ middleware: [loggerMiddleware] })
title: string;
}

现在,每当我们请求 Recipe 对象类型的 title 字段时,原始字段值将被记录到控制台中。

备注

要了解如何使用扩展功能实现字段级别的权限系统, 请查看本节

注意

字段中间件仅可应用于 ObjectType 类。有关更多详细信息, 请查看此问题

另外,正如上面提到的,我们可以在中间件函数中从中控制字段的值。 为了演示目的,让我们将食谱的标题大写(如果存在):

const value = await next();
return value?.toUpperCase();

在这种情况下,每个标题在请求时都将自动大写。

同样,您可以将字段中间件绑定到自定义字段解析器(使用 @ResolveField() 装饰器注释的方法), 如下所示:

@ResolveField(() => String, { middleware: [loggerMiddleware] })
title() {
return 'Placeholder';
}
注意

如果在字段解析器级别启用了增强器(阅读更多), 则字段中间件函数将在绑定到该方法的任何拦截器、守卫等之前运行。

全局字段中间件

除了直接将中间件绑定到特定字段外,您还可以全局注册一个或多个中间件函数。 在这种情况下,它们将自动连接到对象类型的所有字段。

GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
buildSchemaOptions: {
fieldMiddleware: [loggerMiddleware],
},
}),
备注

全局注册的字段中间件函数将在本地注册的中间件函数之前执行(直接绑定到特定字段)。

映射类型

注意

本章仅适用于代码优先方式。

在构建 CRUD(创建/读取/更新/删除)等功能时,通常会构造基础实体类型的变体。 Nest 提供了几个实用函数,执行类型转换以使此任务更加方便。

Partial(部分)

在构建输入验证类型(也称为数据传输对象或 DTO)时,通常会构建相同类型的创建和更新变体。 例如,创建变体可能要求所有字段,而更新变体可能会使所有字段都是可选的。

Nest 提供了 PartialType() 实用函数,以使此任务更加轻松并减少样板代码。

PartialType() 函数返回一个类型(类),该类型中输入类型的所有属性均设置为可选。 例如,假设我们有一个如下所示的创建类型:

@InputType()
class CreateUserInput {
@Field()
email: string;

@Field()
password: string;

@Field()
firstName: string;
}

默认情况下,所有这些字段都是必需的。 要创建一个具有相同字段但每个字段都是可选的类型, 请使用 PartialType() 将类引用(CreateUserInput)作为参数传递:

@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {}
提示

PartialType() 函数是从 @nestjs/graphql 包导入的。

PartialType() 函数接受一个可选的第二个参数,该参数是对装饰器工厂的引用。 此参数可用于更改应用于生成的(子)类的装饰器函数。 如果未指定,子类将有效地使用与父类相同的装饰器(第一个参数中引用的类)。 在上面的示例中,我们正在扩展使用 @InputType() 装饰器注释的 CreateUserInput。 由于我们希望 UpdateUserInput 也被视为已使用 @InputType() 装饰器注释, 因此我们不需要将 InputType 作为第二个参数传递。 如果父类型和子类型不同(例如,父类型用 @ObjectType 装饰器注释), 我们将 InputType 作为第二个参数传递。例如:

@InputType()
export class UpdateUserInput extends PartialType(User, InputType) {}

Pick(选择)

PickType() 函数通过从输入类型中选择一组属性来构造一个新类型(类)。 例如,假设我们从一个如下的类型开始:

@InputType()
class CreateUserInput {
@Field()
email: string;

@Field()
password: string;

@Field()
firstName: string;
}

我们可以使用 PickType() 实用函数从这个类中选择一组属性:

@InputType()
export class UpdateEmailInput extends PickType(CreateUserInput, [
'email',
] as const) {}
提示

PickType() 函数是从 @nestjs/graphql 包导入的。

Omit(省略)

OmitType() 函数通过从输入类型中选择所有属性,然后删除特定的键,构造一个类型。 例如,假设我们从一个如下的类型开始:

@InputType()
class CreateUserInput {
@Field()
email: string;

@Field()
password: string;

@Field()
firstName: string;
}

我们可以生成一个派生类型,该类型除了 email 之外的每个属性都有, 如下所示。在此结构中,OmitType 的第二个参数是属性名称的数组。

@InputType()
export class UpdateUserInput extends OmitType(CreateUserInput, [
'email',
] as const) {}
提示

OmitType() 函数是从 @nestjs/graphql 包导入的。

Intersection(交集)

IntersectionType() 函数将两个类型组合成一个新类型(类)。 例如,假设我们从两个类型开始:

@InputType()
class CreateUserInput {
@Field()
email: string;

@Field()
password: string;
}

@ObjectType()
export class AdditionalUserInfo {
@Field()
firstName: string;

@Field()
lastName: string;
}

我们可以生成一个新类型,该类型合并了两种类型的所有属性。

@InputType()
export class UpdateUserInput extends IntersectionType(
CreateUserInput,
AdditionalUserInfo,
) {}
提示

IntersectionType() 函数是从 @nestjs/graphql 包导入的。

组合

类型映射实用函数是可组合的。例如,以下内容将产生一个类型(类), 该类型具有 CreateUserInput 类的所有属性,但 email 属性除外,这些属性将设置为可选:

@InputType()
export class UpdateUserInput extends PartialType(
OmitType(CreateUserInput, ['email'] as const),
) {}

使用 Apollo 的插件

插件使您能够通过在响应某些事件时执行自定义操作来扩展 Apollo Server 的核心功能。 目前,这些事件对应于 GraphQL 请求生命周期的各个阶段, 以及 Apollo Server 本身的启动(详细信息请参阅此处)。 例如,一个基本的日志记录插件可能会记录与发送到 Apollo Server 的每个请求相关联的 GraphQL 查询字符串。

自定义插件

要创建一个插件,请声明一个使用 @nestjs/apollo 包导出的 @Plugin 装饰器注释的类。 此外,为了更好的代码自动完成,实现 @apollo/server 包的 ApolloServerPlugin 接口。

import { ApolloServerPlugin, GraphQLRequestListener } from '@apollo/server';
import { Plugin } from '@nestjs/apollo';

@Plugin()
export class LoggingPlugin implements ApolloServerPlugin {
async requestDidStart(): Promise<GraphQLRequestListener<any>> {
console.log('Request started');
return {
async willSendResponse() {
console.log('Will send response');
},
};
}
}

有了这个插件,我们可以将 LoggingPlugin 注册为提供者。

@Module({
providers: [LoggingPlugin],
})
export class CommonModule {}

Nest 将自动实例化插件并将其应用于 Apollo Server。

使用外部插件

有几个现成的插件可供使用。要使用现有插件,只需导入它并将其添加到plugins数组中:

GraphQLModule.forRoot({
// ...
plugins: [ApolloServerOperationRegistry({ /* options */})]
}),
提示

ApolloServerOperationRegistry 插件是从 @apollo/server-plugin-operation-registry 包导出的。

在 Mercurius 中使用插件

一些现有的 mercurius-specific Fastify 插件必须在插件树上的 mercurius 插件之后加载(详细信息请参阅此处)。

注意

mercurius-upload 是一个例外,应在主文件中注册。

为此,MercuriusDriver 公开了一个可选的 plugins 配置选项。 它表示由两个属性组成的对象数组:plugin 和其options。 因此,注册缓存插件将如下所示:

GraphQLModule.forRoot({
driver: MercuriusDriver,
// ...
plugins: [
{
plugin: cache,
options: {
ttl: 10,
policy: {
Query: {
add: true
}
}
},
}
]
}),

复杂度

备注

本章仅适用于代码优先方法。

查询复杂度允许您定义某些字段的复杂程度,并限制具有最大复杂程度的查询。 其思想是通过使用一个简单的数字来定义每个字段的复杂程度。 一个常见的默认值是为每个字段分配复杂度为1。 此外,可以使用所谓的复杂度估算器定制 GraphQL 查询的复杂度计算。 复杂度估算器是一个简单的函数,用于计算字段的复杂度。 您可以将任意数量的复杂度估算器添加到规则中,然后依次执行它们。 第一个返回数值复杂度值的估算器确定该字段的复杂程度。

@nestjs/graphql 包与 graphql-query-complexity 这样的工具非常好地集成在一起,提供了一种基于成本分析的解决方案。 使用该库,您可以拒绝认为执行成本过高的 GraphQL 服务器的查询。

安装

要开始使用它,首先安装所需的依赖。

npm install --save graphql-query-complexity

入门

安装过程完成后,我们可以定义 ComplexityPlugin 类。

import { GraphQLSchemaHost } from "@nestjs/graphql";
import { Plugin } from "@nestjs/apollo";
import {
ApolloServerPlugin,
GraphQLRequestListener,
} from 'apollo-server-plugin-base';
import { GraphQLError } from 'graphql';
import {
fieldExtensionsEstimator,
getComplexity,
simpleEstimator,
} from 'graphql-query-complexity';

@Plugin()
export class ComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}

async requestDidStart(): Promise<GraphQLRequestListener> {
const maxComplexity = 20;
const { schema } = this.gqlSchemaHost;

return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > maxComplexity) {
throw new GraphQLError(
`Query is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}`,
);
}
console.log('Query Complexity:', complexity);
},
};
}
}

出于演示目的,我们指定了最大允许的复杂度为20。 在上面的示例中,我们使用了2个估算器,即 simpleEstimatorfieldExtensionsEstimator

  • simpleEstimator:简单估算器为每个字段返回一个固定的复杂度。
  • fieldExtensionsEstimator:字段扩展估算器提取架构中每个字段的复杂度值。
备注

记得将这个类添加到任何模块中的提供者数组中。

字段级别的复杂度

有了这个插件,我们现在可以通过在传递给 @Field() 装饰器的选项对象中指定 complexity 属性来定义任何字段的复杂度, 如下所示:

@Field({ complexity: 3 })
title: string;

或者,您可以定义估算器函数:

@Field({ complexity: (options: ComplexityEstimatorArgs) => ... })
title: string;

查询/变更级别的复杂度

此外,@Query()@Mutation() 装饰器可以指定 complexity 属性,如下所示:

@Query({ complexity: (options: ComplexityEstimatorArgs) => options.args.count * options.childComplexity })
items(@Args('count') count: number) {
return this.itemsService.getItems({ count });
}

扩展

注意

本章仅适用于代码优先方法。

Extensions 是一种高级的低级特性,允许您在类型配置中定义任意数据。 附加自定义元数据到某些字段允许您创建更复杂、通用的解决方案。 例如,使用扩展,您可以定义访问特定字段所需的字段级角色。 这些角色可以在运行时反映,以确定调用者是否具有检索特定字段的足够权限。

添加自定义元数据

要为字段附加自定义元数据,请使用 @nestjs/graphql 包中导出的 @Extensions() 装饰器。

@Field()
@Extensions({ role: Role.ADMIN })
password: string;

在上面的示例中,我们将 role 元数据属性分配为 Role.ADMIN 的值。 Role 是一个简单的 TypeScript 枚举,将我们系统中所有可用的用户角色分组在一起。

请注意,除了在字段上设置元数据之外, 还可以在类级别和方法级别(例如,在查询处理程序上)使用 @Extensions() 装饰器。

使用自定义元数据

利用自定义元数据的逻辑可以尽可能复杂。 例如,您可以创建一个简单的拦截器,它会存储/记录每个方法调用的事件, 或者一个字段中间件, 它将检索字段所需的角色与调用者权限进行匹配(字段级权限系统)。

出于说明目的,让我们定义一个 checkRoleMiddleware, 它比较用户角色(在此硬编码)与访问目标字段所需的角色:

export const checkRoleMiddleware: FieldMiddleware = async (
ctx: MiddlewareContext,
next: NextFn,
) => {
const { info } = ctx;
const { extensions } = info.parentType.getFields()[info.fieldName];

/**
* 在实际应用程序中,"userRole" 变量
* 应该代表调用者(用户)的角色(例如,"ctx.user.role")。
*/
const userRole = Role.USER;
if (userRole === extensions.role) {
// 或者只需返回 null 以忽略
throw new ForbiddenException(
`User does not have sufficient permissions to access "${info.fieldName}" field.`,
);
}
return next();
};

有了这个,我们可以为 password 字段注册一个中间件,如下所示:

@Field({ middleware: [checkRoleMiddleware] })
@Extensions({ role: Role.ADMIN })
password: string;

CLI 插件

注意

本章仅适用于代码优先方法。

TypeScript 的元数据反射系统有一些限制,例如不可能确定类包含哪些属性或识别给定属性是可选的还是必需的。 但是,某些约束可以在编译时解决。 Nest 提供了一个插件,通过增强 TypeScript 编译过程来减少所需的样板代码量。

备注

此插件是可选的。如果您愿意,可以手动声明所有装饰器,或者只在需要时声明特定的装饰器。

概述

GraphQL 插件将自动:

  • 对所有输入对象、对象类型和参数类属性进行注释,除非使用 @HideField
  • 根据问号设置可为空属性(例如,name?: string 将设置 nullable: true
  • 根据类型设置类型属性(还支持数组)
  • 根据注释生成属性的描述(如果 introspectComments 设置为 true

请注意,为了能够通过插件分析,您的文件名必须具有以下后缀之一: ['.input.ts', '.args.ts', '.entity.ts', '.model.ts'](例如,author.entity.ts)。 如果您使用不同的后缀,可以通过指定 typeFileNameSuffix 选项调整插件的行为(见下文)。

根据我们到目前为止学到的知识,您必须复制很多代码以让包知道如何在 GraphQL 中声明您的类型。 例如,您可以定义一个简单的 Author 类如下:

authors/models/author.model.ts

@ObjectType()
export class Author {
@Field(type => ID)
id: number;

@Field({ nullable: true })
firstName?: string;

@Field({ nullable: true })
lastName?: string;

@Field(type => [Post])
posts: Post[];
}

在中等规模的项目中,这可能不是一个重大问题,但是一旦有了大量的类,就会变得冗长且难以维护。

通过启用 GraphQL 插件,上面的类定义可以简化为:

authors/models/author.model.ts

@ObjectType()
export class Author {
@Field(type => ID)
id: number;
firstName?: string;
lastName?: string;
posts: Post[];
}

该插件根据抽象语法树动态添加适当的装饰器。因此,您不必在整个代码中挣扎着使用 @Field 装饰器。

备注

插件将自动生成任何缺失的 GraphQL 属性,但是如果需要覆盖它们, 只需通过 @Field() 明确设置即可。

注释内省

启用注释内省功能后,CLI 插件将根据注释为字段生成描述。

例如,给定一个示例 roles 属性:

/**
* A list of user's roles
*/
@Field(() => [String], {
description: `A list of user's roles`
})
roles: string[];

您必须复制描述值。启用 introspectComments 后, CLI 插件可以提取这些注释并根据其自动为属性提供描述。 现在,上述字段可以简单地声明如下:

/**
* A list of user's roles
*/
roles: string[];

使用 CLI 插件

要启用插件,请打开 nest-cli.json(如果使用 Nest CLI), 并添加以下插件配置:

{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/graphql"]
}
}

您可以使用 options 属性自定义插件的行为。

"plugins": [
{
"name": "@nestjs/graphql",
"options": {
"typeFileNameSuffix": [".input.ts", ".args.ts"],
"introspectComments": true
}
}
]

options 属性必须满足以下接口:

export interface PluginOptions {
typeFileNameSuffix?: string[];
introspectComments?: boolean;
}
选项默认值描述
typeFileNameSuffix['.input.ts', '.args.ts', '.entity.ts', '.model.ts']GraphQL types files suffix
introspectCommentsfalseIf set to true, plugin will generate descriptions for properties based on comments

如果您不使用 CLI 而是有自定义的 webpack 配置,您可以将此插件与 ts-loader 结合使用:

getCustomTransformers: (program: any) => ({
before: [require('@nestjs/graphql/plugin').before({}, program)]
}),

SWC 构建器

对于标准设置(非单体库),要使用 SWC 构建器与 CLI 插件, 您需要启用类型检查,如此处所述。

nest start -b swc --type-check

对于monorepo设置, 请按照这里的说明进行操作。

npx ts-node src/generate-metadata.ts
# 或 npx ts-node apps/{YOUR_APP}/src/generate-metadata.ts

现在,元数据文件必须由 GraphQLModule 方法加载,如下所示:

import metadata from './metadata'; // <-- 文件由 "PluginMetadataGenerator" 自动生成

GraphQLModule.forRoot<...>({
..., // 其他选项
metadata,
})

ts-jest 的集成(端到端测试)

在启用此插件的情况下运行端到端测试时,可能会遇到编译模式的模式问题。例如,最常见的错误之一是:

Object type <name> must define one or more fields.

这是因为 jest 配置未在任何地方导入 @nestjs/graphql/plugin 插件。

要解决此问题,请在您的端到端测试目录中创建以下文件:

const transformer = require('@nestjs/graphql/plugin');

module.exports.name = 'nestjs-graphql-transformer';
// 每次更改下面配置的时候,您都应该更改版本号 - 否则 jest 将无法检测到更改
module.exports.version = 1;

module.exports.factory = (cs) => {
return transformer.before(
{
// @nestjs/graphql/plugin 选项(可以为空)
},
cs.program, // 旧版本的 Jest(<= v27)的 "cs.tsCompiler.program"
);
};

有了这个,从 jest 配置文件中导入 AST 转换器。 默认情况下(在启动应用程序中),端到端测试配置文件位于 test 文件夹下, 并命名为 jest-e2e.json

{
... // 其他配置
"globals": {
"ts-jest": {
"astTransformers": {
"before": ["<path to the file created above>"]
}
}
}
}

如果使用 jest@^29,请使用下面的片段,因为先前的方法已被弃用。

{
... // 其他配置
"transform": {
"^.+\\.(t|j)s$": [
"ts-jest",
{
"astTransformers": {
"before": ["<path to the file created above>"]
}
}
]
}
}

生成 SDL

注意

本章仅适用于代码优先方法。

要手动生成 GraphQL SDL 模式(即,不运行应用程序、连接到数据库、连接解析器等), 请使用 GraphQLSchemaBuilderModule

async function generateSchema() {
const app = await NestFactory.create(GraphQLSchemaBuilderModule);
await app.init();

const gqlSchemaFactory = app.get(GraphQLSchemaFactory);
const schema = await gqlSchemaFactory.create([RecipesResolver]);
console.log(printSchema(schema));
}
提示

GraphQLSchemaBuilderModuleGraphQLSchemaFactory@nestjs/graphql 包导入。 printSchema 函数从 graphql 包导入。

用法

gqlSchemaFactory.create()方法接受一个解析器类引用数组。例如:

const schema = await gqlSchemaFactory.create([
RecipesResolver,
AuthorsResolver,
PostsResolvers,
]);

它还接受一个第二个可选参数,其中包含标量类的数组:

const schema = await gqlSchemaFactory.create(
[RecipesResolver, AuthorsResolver, PostsResolvers],
[DurationScalar, DateScalar],
);

最后,您可以传递一个选项对象:

const schema = await gqlSchemaFactory.create([RecipesResolver], {
skipCheck: true,
orphanedTypes: [],
});
  • skipCheck:忽略模式验证;布尔值,默认为 false
  • orphanedTypes:未显式引用(不是对象图的一部分)的类的列表,以生成它们。 通常,如果声明了一个类但在图中没有其他引用,则会被省略。属性值是一个类引用的数组。

共享模型

注意

本章仅适用于代码优先方法。

在项目的后端使用 TypeScript 的最大优势之一是可以通过使用一个通用的 TypeScript 包, 在基于 TypeScript 的前端应用程序中重用相同的模型。

但是存在一个问题:使用代码优先方法创建的模型使用了与 GraphQL 相关的装饰器。 这些装饰器在前端是无关紧要的,会对性能产生负面影响。

使用模型 shim

为了解决这个问题,NestJS 提供了一个“shim”(模拟器),它允许您通过使用 webpack(或类似工具)配置, 将原始装饰器替换为惰性代码。要使用此 shim,请在 @nestjs/graphql 包和 shim 之间配置别名。

例如,对于 webpack,可以通过以下方式解决此问题:

resolve: { // 参见:https://webpack.js.org/configuration/resolve/
alias: {
"@nestjs/graphql": path.resolve(__dirname, "../node_modules/@nestjs/graphql/dist/extra/graphql-model-shim")
}
}
备注

TypeORM 包有一个类似的 shim, 可以在找到。

其他特性

在GraphQL领域,有很多关于处理认证问题或操作副作用的讨论。 我们应该在业务逻辑内处理这些问题吗? 应该使用高阶函数通过授权逻辑增强查询和突变吗? 还是应该使用模式指令? 对于这些问题,没有一种适用于所有情况的单一解决方案。

Nest通过其跨平台功能(如守卫拦截器)来解决这些问题。 其哲学是减少冗余并提供工具,以帮助创建结构良好、可读且一致的应用程序。

概述

您可以像在任何RESTful应用程序中一样,使用标准的 守卫拦截器过滤器管道。 此外,您可以通过利用自定义装饰器功能轻松创建 自己的装饰器。 让我们看一个样本GraphQL查询处理程序。

@Query('author')
@UseGuards(AuthGuard)
async getAuthor(@Args('id', ParseIntPipe) id: number) {
return this.authorsService.findOneById(id);
}

正如您所看到的,GraphQL与守卫和管道的工作方式与HTTP REST处理程序相同。 因此,您可以将身份验证逻辑移至守卫;甚至可以在REST和GraphQL API界面之间重复使用相同的守卫类。 同样,拦截器在两种类型的应用程序中以相同的方式工作。

@Mutation()
@UseInterceptors(EventsInterceptor)
async upvotePost(@Args('postId') postId: number) {
return this.postsService.upvoteById({ id: postId });
}

执行上下文

由于GraphQL在传入请求中接收了一种不同类型的数据, 因此守卫和拦截器接收的执行上下文在GraphQL与REST之间略有不同。 GraphQL解析器具有一组不同的参数:root、args、context和info。 因此,守卫和拦截器必须将通用的ExecutionContext转换为GqlExecutionContext。 这很简单:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const ctx = GqlExecutionContext.create(context);
return true;
}
}

GqlExecutionContext.create()返回的GraphQL上下文对象为每个GraphQL解析器参数 公开了一个get方法(例如,getArgs()getContext()等)。 转换后,我们可以轻松地提取当前请求的任何GraphQL参数。

异常过滤器

Nest标准的异常过滤器 在GraphQL应用程序中同样适用。与ExecutionContext一样, GraphQL应用程序应将ArgumentsHost对象转换为GqlArgumentsHost对象。

@Catch(HttpException)
export class HttpExceptionFilter implements GqlExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const gqlHost = GqlArgumentsHost.create(host);
return exception;
}
}
提示

GqlExceptionFilterGqlArgumentsHost都从@nestjs/graphql包中导入。

请注意,与REST情况不同,您不使用本机response对象生成响应。

自定义装饰器

如前所述,自定义装饰器 功能在GraphQL解析器中正常工作。

export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) =>
GqlExecutionContext.create(ctx).getContext().user,
);

使用@User()自定义装饰器如下:

@Mutation()
async upvotePost(
@User() user: UserEntity,
@Args('postId') postId: number,
) {}
备注

在上面的示例中,我们假设user对象分配给了您的GraphQL应用程序的上下文。

在字段解析器级别执行增强器

在GraphQL上下文中,Nest不会在字段级别运行增强器(拦截器、守卫和过滤器的通用名称) 请参见此问题: 它们仅对顶级@Query()/@Mutation()方法运行。 您可以告诉Nest为带有@ResolveField()注解的方法执行拦截器、守卫或过滤器, 方法是在GqlModuleOptions中设置fieldResolverEnhancers选项。 将其传递给'interceptors''guards'和/或'filters'的列表:

GraphQLModule.forRoot({
fieldResolverEnhancers: ['interceptors']
}),
注意

启用字段解析器的增强器可能在返回大量记录并且字段解析器被执行数千次时导致性能问题。 因此,当启用fieldResolverEnhancers时, 我们建议您跳过对字段解析器不是严格必要的增强器的执行。 您可以使用以下辅助函数来实现:

export function isResolvingGraphQLField(context: ExecutionContext): boolean {
if (context.getType<GqlContextType>() === 'graphql') {
const gqlContext = GqlExecutionContext.create(context);
const info = gqlContext.getInfo();
const parentType = info.parentType.name;
return parentType !== 'Query' && parentType !== 'Mutation';
}
return false;
}

创建自定义驱动程序

Nest提供了两个内置的驱动程序:@nestjs/apollo@nestjs/mercurius, 以及一个API,允许开发人员构建新的自定义驱动程序。使用自定义驱动程序, 您可以集成任何GraphQL库或扩展现有的集成,添加额外的功能。

例如,要集成express-graphql包,您可以创建以下驱动程序类:

import { AbstractGraphQLDriver, GqlModuleOptions } from '@nestjs/graphql';
import { graphqlHTTP } from 'express-graphql';

class ExpressGraphQLDriver extends AbstractGraphQLDriver {
async start(options: GqlModuleOptions<any>): Promise<void> {
options = await this.graphQlFactory.mergeWithSchema(options);

const { httpAdapter } = this

.httpAdapterHost;
httpAdapter.use(
'/graphql',
graphqlHTTP({
schema: options.schema,
graphiql: true,
}),
);
}

async stop() {}
}

然后像这样使用它:

GraphQLModule.forRoot({
driver: ExpressGraphQLDriver,
});

联合(Federation)

联合提供了一种将您的单体GraphQL服务器拆分为独立的微服务的方式。 它包含两个组件一个网关一个或多个联合的微服务。 每个微服务都持有部分模式,而网关将这些模式合并成一个可以由客户端消耗的单一模式。

引用Apollo文档的话, 联合设计有以下核心原则:

  1. 构建图形应该是声明性的。使用联合,您可以从模式内部声明性地组合图形,而不是编写命令式的模式拼接代码。
  2. 代码应该按关注点分离,而不是按类型分离。通常没有单个团队控制像User或Product这样重要类型的每个方面,因此这些类型的定义应该分布在团队和代码库之间,而不是集中在一处。
  3. 图应该对客户端来说是简单的。联合服务可以共同形成一个完整、以产品为中心的图形,准确地反映了客户端上的使用方式。
  4. 它只是GraphQL,仅使用语言的规范特性。任何语言,不仅仅是JavaScript,都可以实现联合。
注意

联合当前不支持订阅。

在以下部分中,我们将设置一个演示应用程序, 该应用程序包含一个网关和两个联合的端点:用户服务帖子服务

使用Apollo进行联合

首先安装所需的依赖:

npm install --save @apollo/federation @apollo/subgraph

模式优先

"用户服务"提供了一个简单的模式。 请注意@key指令:它指示Apollo查询规划器,如果指定了其id, 则可以获取User的特定实例。还请注意我们extendQuery类型。

type User @key(fields: "id") {
id: ID!
name: String!
}

extend type Query {
getUser(id: ID!): User
}

解析器提供了一个名为resolveReference()的额外方法。 每当Apollo Gateway需要User实例时,该方法将由它触发。 稍后我们将在帖子服务中看到这个方法的示例。 请注意,该方法必须用@ResolveReference()装饰器进行注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';

@Resolver('User')
export class UsersResolver {
constructor(private usersService: UsersService) {}

@Query()
getUser(@Args('id') id: string) {
return this.usersService.findById(id);
}

@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findById(reference.id);
}
}

最后,通过在配置对象中传递ApolloFederationDriver, 将所有内容连接起来,注册GraphQLModule

import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';

@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
typePaths: ['**/*.graphql'],
}),
],
providers: [UsersResolver],
})
export class AppModule {}

代码优先

首先为User实体添加一些额外的装饰器。

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;

@Field()
name: string;
}

解析器提供了一个名为resolveReference()的额外方法。 每当Apollo Gateway需要User实例时,该方法将由它触发。 稍后我们将在帖子服务中看到这个方法的示例。 请注意,该方法必须用@ResolveReference()装饰器进行注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';

@Resolver((of) => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}

@Query((returns) => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}

@ResolveReference()
resolveReference(reference: { __typename: string; id: number }): User {
return this.usersService.findById(reference.id);
}
}

最后,通过在配置对象中传递ApolloFederationDriver, 将所有内容连接起来,注册GraphQLModule

import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example

@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}

在代码优先模式中有一个可用的示例, 也有一个模式优先模式中的示例

联合示例:帖子

帖子服务旨在通过getPosts查询提供聚合的帖子, 同时通过扩展我们的User类型的user.posts字段。

首先模式

"帖子服务"在其模式中通过使用extend关键字标记User类型来引用它。 它还在User类型上声明了一个额外的属性(posts)。 请注意使用@key指令匹配User实例,并使用@external指令指示id字段在其他地方进行管理。

type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}

extend type User @key(fields: "id") {
id: ID! @external
posts: [Post]
}

extend type Query {
getPosts: [Post]
}

在下面的示例中,PostsResolver提供了getUser()方法, 该方法返回一个包含__typename和一些可能需要解析引用的其他属性的引用, 例如id.__typename由GraphQL网关用于定位负责User类型的微服务并检索相应实例。 上述描述的"用户服务"将在执行resolveReference()方法时被请求。

import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';

@Resolver('Post')
export class PostsResolver {
constructor(private postsService: PostsService) {}

@Query('getPosts')
getPosts() {
return this.postsService.findAll();
}

@ResolveField('user')
getUser(@Parent() post: Post) {
return { __typename: 'User', id: post.userId };
}
}

最后,我们必须注册GraphQLModule,类似于我们在“用户服务”部分所做的操作。

import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';

@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}

代码优先

首先,我们必须声明一个表示User实体的类。 尽管实体本身位于另一个服务中,但我们将在这里使用它(扩展其定义)。 请注意@extends@external指令。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';

@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
@Directive('@external')
id: number;

@Field((type) => [Post])
posts?: Post[];
}

现在让我们为User实体的扩展创建相应的解析器。

import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => User)
export class UsersResolver {
constructor(private readonly postsService: PostsService) {}

@ResolveField((of) => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}

我们还必须定义Post实体类。

import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field((type) => ID)
id: number;

@Field()
title: string;

@Field((type) => Int)
authorId: number;

@Field((type) => User)
user?: User;
}

以及它的解析器。

import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}

@Query((returns) => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}

@Query((returns) => [Post])
getPosts(): Post[] {
return this.postsService.all();
}

@ResolveField((of) => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}

最后,在一个模块中将它们组合在一起。 请注意模式构建选项,其中我们指定User是一个孤立的(external)类型。

import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example

@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver:

ApolloFederationDriver,
autoSchemaFile: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

此处提供了代码优先模式的工作示例, 此处提供了模式优先模式的工作示例

联合示例:网关

首先,安装必需的依赖项:

npm install --save @apollo/gateway

网关需要指定端点的列表,并且它将自动发现相应的模式。 因此,网关服务的实现在代码和模式优先方法中保持不变。

import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
imports: [
GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
driver: ApolloGatewayDriver,
server: {
// ... Apollo server options
cors: true,
},
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
}),
},
}),
],
})
export class AppModule {}

此处和 提供了代码优先模式的 此处。 提供了模式优先模式的工作示例。

使用 Mercurius 的联合示例

首先,安装所需的依赖项:

npm install --save @apollo/subgraph @nestjs/mercurius
提示

@apollo/subgraph包是构建子图模式(buildSubgraphSchemaprintSubgraphSchema函数)所必需的。

模式优先方式

"用户服务"提供了一个简单的模式。 请注意@key指令:它指示Mercurius查询规划器, 如果指定其id,则可以获取User的特定实例。还要注意,我们extendQuery类型。

type User @key(fields: "id") {
id: ID!
name: String!
}

extend type Query {
getUser(id: ID!): User
}

解析器提供了一个名为resolveReference()的附加方法。 每当Mercurius Gateway需要User实例时,Apollo Gateway将触发此方法。 我们稍后在帖子服务中将看到这个方法的示例。 请注意,该方法必须用@ResolveReference()装饰器进行注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';

@Resolver('User')
export class UsersResolver {
constructor(private usersService: UsersService) {}

@Query()
getUser(@Args('id') id: string) {
return this.usersService.findById(id);
}

@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findById(reference.id);
}
}

最后,我们通过在配置对象中传递MercuriusFederationDriver驱动程序来注册GraphQLModule

import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';

@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
typePaths: ['**/*.graphql'],
federationMetadata: true,
}),
],
providers: [UsersResolver],
})
export class AppModule {}

代码优先模式

首先,为User实体添加一些额外的装饰器。

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;

@Field()
name: string;
}

解析器提供了一个名为resolveReference()的附加方法。 每当Mercurius Gateway需要User实例时,Apollo Gateway将触发此方法。 我们稍后在帖子服务中将看到这个方法的示例。 请注意,该方法必须用@ResolveReference()装饰器进行注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';

@Resolver((of) => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}

@Query((returns) => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}

@ResolveReference()
resolveReference(reference: { __typename: string; id: number }): User {
return this.usersService.findById(reference.id);
}
}

最后,我们通过在配置对象中传递MercuriusFederationDriver驱动程序来注册GraphQLModule

import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // 未在此示例中包含

@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}

联合示例:帖子

帖子服务旨在通过getPosts查询提供聚合的帖子,同时通过扩展我们的User类型的user.posts字段。

模式优先模式

"帖子服务"在其模式中通过使用extend关键字标记User类型来引用它。 它还在User类型上声明了一个额外的属性(posts)。 请注意使用@key指令匹配User实例,并使用@external指令指示id字段在其他地方进行管理。

type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}

extend type User @key(fields: "id") {
id: ID! @external
posts: [Post]
}

extend type Query {
getPosts: [Post]
}

在下面的示例中,PostsResolver提供了getUser()方法, 该方法返回一个引用,其中包含__typename和一些可能需要

解析引用的应用程序的其他属性,这里是id.__typename由GraphQL Gateway使用, 以定位负责User类型的微服务并检索相应实例。 上面描述的"用户服务"将在执行resolveReference()方法时被请求。

import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';

@Resolver('Post')
export class PostsResolver {
constructor(private postsService: PostsService) {}

@Query('getPosts')
getPosts() {
return this.postsService.findAll();
}

@ResolveField('user')
getUser(@Parent() post: Post) {
return { __typename: 'User', id: post.userId };
}
}

最后,我们必须注册GraphQLModule,类似于我们在“用户服务”部分中所做的操作。

import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';

@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
federationMetadata: true,
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}

代码优先模式

首先,我们必须声明一个表示User实体的类。 尽管实体本身位于另一个服务中,但我们将在这里使用它(扩展其定义)。 请注意@extends@external指令。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';

@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
@Directive('@external')
id: number;

@Field((type) => [Post])
posts?: Post[];
}

现在让我们为我们在User实体上的扩展创建相应的解析器。

import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => User)
export class UsersResolver {
constructor(private readonly postsService: PostsService) {}

@ResolveField((of) => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}

我们还必须定义Post实体类。

import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field((type) => ID)
id: number;

@Field()
title: string;

@Field((type) => Int)
authorId: number;

@Field((type) => User)
user?: User;
}

以及它的解析器。

import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}

@Query((returns) => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}

@Query((returns) => [Post])
getPosts(): Post[] {
return this.postsService.all();
}

@ResolveField((of) => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}

最后,在一个模块中将它们绑定在一起。 请注意模式构建选项,我们在那里指定User是一个孤立的(external)类型。

import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // 未在此示例中包含

@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

联合示例:网关

网关需要指定端点的列表,并且它将自动发现相应的模式。 因此,网关服务的实现对于代码和模式优先方法都将保持相同。

import {
MercuriusGatewayDriver,
MercuriusGatewayDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
imports: [
GraphQLModule.forRoot<MercuriusGatewayDriverConfig>({
driver: MercuriusGatewayDriver,
gateway: {
services: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
},
}),
],
})
export class AppModule {}

联合 2

Federation 2改进了开发人员在原始Apollo联合(在本文档中称为Federation 1)中的体验, 它与大多数原始超图兼容。

注意

Mercurius不完全支持Federation 2。您可以在这里查看支持Federation 2的库的列表。

在接下来的几节中,我们将升级前面的示例以适应Federation 2。

联合示例:用户

在Federation 2中的一个变化是实体不再有起源子图,因此我们不再需要扩展Query。 有关更多详细信息,请参阅Apollo Federation 2文档中的实体主题

模式优先方式

我们可以简单地从模式中删除extend关键字。

type User @key(fields: "id") {
id: ID!
name: String!
}

type Query {
getUser(id: ID!): User
}

代码优先方式

为了使用Federation 2,我们需要在autoSchemaFile选项中指定联合版本。

import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example

@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}

联合示例:帖子

出于同样的原因,我们不再需要扩展UserQuery

模式优先方式

我们可以简单地从模式中删除extendexternal指令。

type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}

type User @key(fields: "id") {
id: ID!
posts: [Post]
}

type Query {
getPosts: [Post]
}

代码优先方式

由于我们不再扩展User实体,我们可以简单地从User中删除extendsexternal指令。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;

@Field((type) => [Post])
posts?: Post[];
}

同样,类似于用户服务,我们需要在GraphQLModule中指定使用Federation 2。

import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example

@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

从v10迁移到v11

本章提供了从@nestjs/graphql版本10迁移到版本11的一组指南。 作为这个主要版本的一部分,我们将Apollo驱动程序更新为与Apollo Server v4兼容(而不是v3)。 注意:Apollo Server v4中存在一些重大变化(特别是在插件和生态系统包方面), 因此您将不得不相应地更新代码库。 有关更多信息,请参见Apollo Server v4迁移指南。

Apollo包

不再安装apollo-server-express包,您将需要安装@apollo/server

npm uninstall apollo-server-express
npm install @apollo/server

如果使用Fastify适配器,您将需要安装@as-integrations/fastify包:

npm uninstall apollo-server-fastify
npm install @apollo/server @as-integrations/fastify

Mercurius包

Mercurius网关不再是mercurius包的一部分。相反,您将需要单独安装@mercuriusjs/gateway包:

npm install @mercuriusjs/gateway

同样,为了创建联合模式,您将需要安装@mercuriusjs/federation包:

npm install @mercuriusjs/federation

从v10迁移到v9

本章提供了从@nestjs/graphql版本9迁移到版本10的一组指南。 此主要版本发布的重点是提供一个更轻量级、与平台无关的核心库。

引入“driver”包

在最新版本中,我们决定将@nestjs/graphql包拆分成几个单独的库, 让您可以选择在项目中使用Apollo(@nestjs/apollo)、 Mercurius(@nestjs/mercurius)或其他GraphQL库。

这意味着现在您必须明确指定应用程序将使用的驱动程序。

之前

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
}),
],
})
export class AppModule {}

// 之后
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: 'schema.gql',
}),
],
})
export class AppModule {}

插件

Apollo Server插件允许您对某些事件做出自定义操作。由于这是Apollo的一个独有功能,我们将其从@nestjs/graphql移动到新创建的@nestjs/apollo包中,因此您必须更新应用程序中的导入。

// 之前
import { Plugin } from '@nestjs/graphql';

// 之后
import { Plugin } from '@nestjs/apollo';

指令

schemaDirectives功能已被v8版本的@graphql-tools/schema包中的新Schema指令API替换。

// 之前
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLField } from 'graphql';

export class UpperCaseDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const result = await resolve.apply(this, args);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
}
}

// 之后
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';

export function upperDirectiveTransformer(
schema: GraphQLSchema,
directiveName: string,
) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const upperDirective = getDirective(
schema,
fieldConfig,
directiveName,
)?.[0];

if (upperDirective) {
const { resolve = defaultFieldResolver } = fieldConfig;

// Replace the original resolver with a function that *first* calls
// the original resolver, then converts its result to upper case
fieldConfig.resolve = async function (source, args, context, info) {
const result = await resolve(source, args, context, info);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
return fieldConfig;
}
},
});
}

要将此指令实现应用于包含@upper指令的模式,请使用transformSchema函数:

GraphQLModule.forRoot<ApolloDriverConfig>({
...
transformSchema: schema => upperDirectiveTransformer(schema, 'upper'),
})

联合

GraphQLFederationModule已被删除,并替换为相应的驱动程序类。

// 之前
GraphQLFederationModule.forRoot({
autoSchemaFile: true,
});

// 之后
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
});
提示

ApolloFederationDriver类和ApolloFederationDriverConfig都从@nestjs/apollo包中导出。

同样,不再使用专用的GraphQLGatewayModule,只需将适当的驱动程序类传递给您的GraphQLModule设置:

// 之前
GraphQLGatewayModule.forRoot({
gateway: {
supergraphSdl: new Intros

pectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:3000/graphql' },
{ name: 'posts', url: 'http://localhost:3001/graphql' },
],
}),
},
});

// 之后
GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
driver: ApolloGatewayDriver,
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:3000/graphql' },
{ name: 'posts', url: 'http://localhost:3001/graphql' },
],
}),
},
});
提示

ApolloGatewayDriver类和ApolloGatewayDriverConfig都从@nestjs/apollo包中导出。