在 GraphQL 世界中,关于如何处理身份验证或操作的副作用等问题存在很多争论。我们应该在业务逻辑内部处理问题吗?我们应该使用高阶函数来增强查询和授权逻辑的变更吗?还是应该使用 架构指令?这些问题没有单一的万能答案。
Nest 通过其跨平台功能(如 guards 和 interceptors)帮助解决这些问题。其理念是减少冗余并提供有助于创建结构良好、可读且一致的应用程序的工具。
概述
您可以像使用任何 RESTful 应用程序一样,以相同的方式在 GraphQL 中使用标准 guards、interceptors、filters 和 pipes。此外,您还可以利用 custom decorators 功能轻松创建自己的装饰器。让我们看一个示例 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 解析器参数(例如 getArgs()
、getContext()
等)公开一个 get 方法。转换后,我们可以轻松地为当前请求挑选出任何 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 情况不同,您不使用本机 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 不会在字段级别运行增强器(拦截器、保护器和过滤器的通用名称)请参阅此问题(https://github.com/nestjs/graphql/issues/320#issuecomment-511193229):它们仅针对顶级 @Query()
/@Mutation()
方法运行。您可以通过在 GqlModuleOptions
中设置 fieldResolverEnhancers
选项来告诉 Nest 为使用 @ResolveField()
注释的方法执行拦截器、保护器或过滤器。根据需要向其传递一个 '拦截器''、
'保护器'' 和/或 `'过滤器'' 列表:
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,
})