在 GraphQL 世界中,关于如何处理诸如身份验证或操作的副作用等问题,存在许多讨论。我们应该在业务逻辑中处理这些问题吗?是否应该使用高阶函数为查询和变更操作增强授权逻辑?还是应该使用 schema 指令?对于这些问题,并没有一种放之四海而皆准的答案。
Nest 通过其跨平台特性(如守卫和拦截器)帮助开发者应对这些挑战。其理念是减少冗余,并提供有助于创建结构良好、可读性强且一致性高的应用程序的工具。
你可以像在 RESTful 应用中一样,在 GraphQL 中使用标准的守卫、拦截器、异常过滤器和管道。此外,你还可以通过自定义装饰器功能,轻松创建自己的装饰器。下面让我们看一个 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 在接收请求时的数据类型与 REST 不同,守卫和拦截器所接收的执行上下文在 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
}
}GqlExceptionFilter 和 GqlArgumentsHost 都需要从 @nestjs/graphql 包中导入。
需要注意的是,与 REST 场景不同,这里不需要使用原生的 响应对象 来生成响应。
如前所述,自定义装饰器功能在 GraphQL 解析器(resolver)中同样可以正常使用。
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 并不会在字段级别运行增强器(enhancer,增强器是拦截器、守卫和过滤器的统称),它们只会在顶层的 @Query()/@Mutation() 方法上运行。详见此 issue。你可以通过在 GqlModuleOptions 中设置 fieldResolverEnhancers 选项,让 Nest 在带有 @ResolveField() 注解的方法上执行拦截器、守卫或过滤器。只需传入包含 '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 允许开发者构建新的自定义驱动(Custom Driver)。通过自定义驱动,你可以集成任意 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,
})