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 集成 ,您应该使用 MercuriusDriver 和 MercuriusDriverConfig。
两者都是从 @nestjs/mercurius 包中导出的。
forRoot() 方法接受一个选项对象作为参数。
这些选项将传递给底层的驱动程序实例(有关可用设置的更多信息,请阅读
Apollo 和
Mercurius 文档)。
例如,如果要禁用 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 操作(查询、变异或订阅)转换为数据的指令。
它们返回我们在模式中指定的相同形状的数据,可以是同步的,也可以是解析为该形状的结果的承诺。
通常,您会手动创建解析器映射。
与此相反,@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
}