Graphql

解析器映射

学习如何在NestJS中创建和配置GraphQL解析器,包括代码优先和模式优先两种方法

解析器映射

解析器提供了将GraphQL操作(查询、变更或订阅)转换为数据的指令。它们返回与模式中描述的形状相同的数据,可以是同步的,也可以是异步的。通常,解析器返回一个Promise(例如,从数据库获取数据)。

代码优先

代码优先方法中,我们不手动编写GraphQL模式。相反,我们使用装饰器从TypeScript类定义中自动生成模式。要创建解析器,我们将创建一个带有解析器方法的类,并用@Resolver()装饰器装饰该类。

在本章中,我们将使用一个简化的Recipe模型:

export class Recipe {
  id: string;
  title: string;
  description?: string;
  creationDate: Date;
  ingredients: string[];
}

现在,让我们创建一个解析器类:

@@filename(recipes.resolver)
import { Args, Query, Resolver } from '@nestjs/graphql';
import { Recipe } from './models/recipe.model';
import { RecipeService } from './recipe.service';

@Resolver()
export class RecipeResolver {
  constructor(private recipeService: RecipeService) {}

  @Query(returns => [Recipe])
  async recipes(): Promise<Recipe[]> {
    return this.recipeService.findAll();
  }

  @Query(returns => Recipe)
  async recipe(@Args('id') id: string): Promise<Recipe> {
    return this.recipeService.findOneById(id);
  }
}

提示 我们假设您已经创建了一个RecipeService(一个简单的CRUD服务,我们在这里不会详细介绍)。

在上面的示例中,我们定义了一个RecipeResolver,它提供了两个查询:recipes()recipe()。要指定方法是查询处理程序,我们使用@Query()装饰器。类似地,要创建变更,我们将使用@Mutation()装饰器。要创建订阅,我们将使用@Subscription()装饰器。

装饰器的第一个参数是返回类型。请注意,由于TypeScript的元数据反射系统的限制,我们必须显式指定返回类型。例如,如果我们返回一个数组,我们必须手动指示数组项类型,如上面的recipes()方法所示。

要指定返回类型,我们使用类型引用(例如,Recipe)。

现在我们需要将Recipe模型转换为GraphQL模式。为此,我们用@ObjectType()装饰器注释模型类,并用@Field()装饰器装饰属性:

@@filename(models/recipe.model)
import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Recipe {
  @Field()
  id: string;

  @Field()
  title: string;

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

  @Field()
  creationDate: Date;

  @Field(type => [String])
  ingredients: string[];
}

提示 TypeScript的元数据反射系统只能确定类型是否为数组。因此,对于数组,我们必须手动指示数组项类型(如上面的ingredients字段所示)或使用显式类型引用。

现在Recipe模型已经注释,让我们重新访问RecipeResolver类。

@@filename(recipes.resolver)
import { Args, Query, Resolver } from '@nestjs/graphql';
import { Recipe } from './models/recipe.model';
import { RecipeService } from './recipe.service';

@Resolver(of => Recipe)
export class RecipeResolver {
  constructor(private recipeService: RecipeService) {}

  @Query(returns => [Recipe])
  async recipes(): Promise<Recipe[]> {
    return this.recipeService.findAll();
  }

  @Query(returns => Recipe)
  async recipe(@Args('id') id: string): Promise<Recipe> {
    return this.recipeService.findOneById(id);
  }
}

请注意,我们将类引用(Recipe)传递给@Resolver()装饰器。这是可选的,但提供此信息允许库访问有用的元数据。

通常,我们使用专用的参数装饰器,如@Args()来提取GraphQL请求中的参数。

@Args('id') id: string
@Args({ name: 'id', type: () => Int }) id: number

在第一种情况下,我们依赖TypeScript的类型反射来推断参数类型。这适用于stringboolean等基本类型,但对于复杂类型(如数组或自定义GraphQL标量),我们必须显式传递类型引用。

或者,我们可以传递一个选项对象而不是参数名称:

@Query(returns => Recipe)
async recipe(@Args({ name: 'id', type: () => Int }) id: number): Promise<Recipe> {
  return this.recipeService.findOneById(id);
}

@Args()装饰器的选项对象允许以下可选参数:

  • nullable: boolean - 参数是否可选
  • defaultValue: any - 默认值
  • description: string - 描述数据
  • deprecationReason: string - 将字段标记为已弃用,并提供原因
  • type: () => GraphQLScalarType - 显式GraphQL类型

最后,我们需要在模块中注册RecipeResolver

@@filename(recipe.module)
import { Module } from '@nestjs/common';
import { RecipeResolver } from './recipe.resolver';
import { RecipeService } from './recipe.service';

@Module({
  providers: [RecipeResolver, RecipeService],
})
export class RecipeModule {}

将所有内容放在一起,GraphQLModule将查找所有用@Resolver()装饰器注释的类,并将为每个方法处理程序自动生成查询映射。

对象类型

我们在上面定义的大多数装饰器都有一个可选的type函数。此类型函数返回GraphQL类型。我们已经看到了基本类型(StringNumberBoolean)和数组([String])的示例。但我们也可以指定复杂的对象类型。

例如,如果我们有一个Author对象类型:

@@filename(models/author.model)
import { Field, Int, ObjectType } from '@nestjs/graphql';

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

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

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

  @Field(type => [Recipe])
  recipes: Recipe[];
}

然后我们可以在我们的解析器中使用它:

@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number): Promise<Author> {
  return this.authorsService.findOneById(id);
}

参数

我们的解析器可以接受参数来访问来自客户端的数据。在上面的示例中,我们使用了@Args('id') id: string参数。这里我们使用@Args()装饰器从GraphQL请求中提取单个参数。

GraphQL请求可以接受多个参数。例如:

{
  author(firstName: "John", lastName: "Doe") {
    id
    firstName
    lastName
  }
}

在这种情况下,我们可以使用以下方法签名:

@Query(returns => Author)
async author(
  @Args('firstName') firstName: string,
  @Args('lastName') lastName: string,
): Promise<Author> {
  return this.authorsService.findOneByName(firstName, lastName);
}

专用参数类

使用内联@Args()调用,代码往往会变得臃肿。相反,您可以创建一个专用的GetAuthorArgs参数类:

@@filename(dto/get-author.args)
import { ArgsType, Field } from '@nestjs/graphql';

@ArgsType()
export class GetAuthorArgs {
  @Field({ nullable: true })
  firstName?: string;

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

提示 同样,由于TypeScript的元数据反射系统的限制,您必须使用@Field()装饰器手动指示类型和可选性,或使用CLI插件

现在我们可以在解析器中使用以下方法签名:

@Query(returns => Author)
async author(@Args() args: GetAuthorArgs): Promise<Author> {
  return this.authorsService.findOneByName(args.firstName, args.lastName);
}

类验证器集成

Nest与类验证器库很好地集成。这种强大的组合允许您对传入的GraphQL查询使用基于装饰器的验证。例如:

@@filename(dto/get-author.args)
import { IsOptional, Length } from 'class-validator';
import { ArgsType, Field } from '@nestjs/graphql';

@ArgsType()
export class GetAuthorArgs {
  @Field({ nullable: true })
  @IsOptional()
  @Length(3, 50)
  firstName?: string;

  @Field({ nullable: true })
  @IsOptional()
  @Length(3, 50)
  lastName?: string;
}

注意 要启用GraphQL参数的自动验证,您必须设置ValidationPipe。在这里这里阅读更多关于验证的信息。

模式优先

模式优先方法中,我们首先手动定义GraphQL模式,然后实现解析器。

假设我们为我们的GraphQL API定义了以下模式(在.graphql文件中):

type Query {
  recipes: [Recipe!]!
  recipe(id: ID!): Recipe
}

type Recipe {
  id: ID!
  title: String!
  description: String
  creationDate: Date!
  ingredients: [String!]!
}

现在我们可以创建一个解析器类:

@@filename(recipes.resolver)
@Resolver('Recipe')
export class RecipesResolver {
  constructor(private readonly recipesService: RecipesService) {}

  @Query()
  async recipes() {
    return this.recipesService.findAll();
  }

  @Query('recipe')
  async getRecipe(@Args('id') id: string) {
    return this.recipesService.findOneById(id);
  }
}

在上面的示例中,我们创建了一个RecipesResolver类,它定义了一个查询解析器函数和一个字段解析器函数。由于我们使用的是模式优先方法,解析器方法的名称是任意的。

或者,您可以使用以下语法:

@@filename(recipes.resolver)
@Resolver('Recipe')
export class RecipesResolver {
  constructor(private readonly recipesService: RecipesService) {}

  @Query('recipes')
  async getRecipes() {
    return this.recipesService.findAll();
  }

  @Query('recipe')
  async getRecipe(@Args('id') id: string) {
    return this.recipesService.findOneById(id);
  }
}

如果您想要将解析器函数与模式中的名称匹配,您可以使用以下方法:

@@filename(recipes.resolver)
@Resolver('Recipe')
export class RecipesResolver {
  constructor(private readonly recipesService: RecipesService) {}

  @Query()
  recipes() {
    return this.recipesService.findAll();
  }

  @Query()
  recipe(@Args('id') id: string) {
    return this.recipesService.findOneById(id);
  }
}

生成类型

假设我们使用模式优先方法并启用了TypeScript定义生成功能(如前一章所示),一旦您运行应用程序,将生成以下文件:

@@filename(graphql.ts)
export class Recipe {
  id: string;
  title: string;
  description?: string;
  creationDate: Date;
  ingredients: string[];
}

export interface IQuery {
  recipes(): Recipe[] | Promise<Recipe[]>;
  recipe(id: string): Recipe | Promise<Recipe>;
}

现在,我们可以使用接口来键入我们的解析器类:

@@filename(recipes.resolver)
@Resolver('Recipe')
export class RecipesResolver implements IQuery {
  constructor(private readonly recipesService: RecipesService) {}

  recipes(): Recipe[] {
    return this.recipesService.findAll();
  }

  recipe(id: string): Recipe {
    return this.recipesService.findOneById(id);
  }
}

提示 要了解更多关于生成TypeScript定义的信息,请参阅此章节

装饰器

您可能会注意到,我们在上面的示例中引用了相同的装饰器,但没有任何类型函数(例如,@Query(returns => [Recipe])变成了@Query())。这是因为在模式优先方法中,类型定义在SDL文件中,而不是通过装饰器。

模块

一旦我们创建了解析器,我们需要在模块中注册RecipesResolver

@@filename(recipes.module)
@Module({
  imports: [RecipesService],
  providers: [RecipesResolver],
})
export class RecipesModule {}

访问用户载荷

在某些情况下,您可能需要访问当前经过身份验证的用户载荷。您通常通过将令牌与请求一起发送,然后在服务器端验证令牌并提取用户载荷来实现此目的。

在GraphQL应用程序中,用户载荷通过执行上下文传递。我们可以通过添加@Context()参数装饰器来访问上下文:

@Query(returns => Recipe)
async recipe(@Args('id') id: string, @Context() ctx) {
  console.log(ctx.user); // 打印经过身份验证的用户
  return this.recipesService.findOneById(id);
}

提示 要了解更多关于GraphQL上下文的信息,请访问此链接

信息对象

解析器的另一个可能有用的参数是info对象,它包含有关执行状态的信息,包括字段名称、从根到字段的路径等。您可以使用@Info()装饰器访问此对象:

@Query(returns => Recipe)
async recipe(@Args('id') id: string, @Info() info) {
  console.log(info.fieldName); // 打印 "recipe"
  return this.recipesService.findOneById(id);
}

工作示例

完整的工作示例可在这里获得。