Skip to content

其他特性

在 GraphQL 世界中,关于如何处理认证或操作的副作用等问题有很多讨论。我们应该在业务逻辑中处理这些事情吗?应该使用高阶函数来增强查询和变更的授权逻辑吗?还是应该使用 schema 指令?对于这些问题,没有一个放之四海而皆准的答案。

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

概述

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

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

如你所见,GraphQL 以与 HTTP REST 处理器相同的方式使用守卫和管道。因此,你可以将认证逻辑移到守卫中;你甚至可以在 REST 和 GraphQL API 接口之间重用相同的守卫类。同样,拦截器在两种类型的应用程序中的工作方式也相同:

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

执行上下文

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

typescript
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 对象。

typescript
@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 解析器一起使用。

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

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

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

提示

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

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

在 GraphQL 上下文中,Nest 不会在字段级别运行增强器(拦截器、守卫和过滤器的统称)参见此问题:它们只在顶层 @Query()/@Mutation() 方法上运行。你可以通过在 GqlModuleOptions 中设置 fieldResolverEnhancers 选项来告诉 Nest 为使用 @ResolveField() 注解的方法执行拦截器、守卫或过滤器。传入一个包含 'interceptors''guards' 和/或 'filters' 的列表:

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

警告

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

typescript
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 包,你可以创建以下驱动类:

typescript
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() {}
}

然后按如下方式使用它:

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

基于 NestJS 官方文档翻译