GraphQL

1,239 阅读8分钟

快速开始

GraphQL是一种功能强大的api查询语言,他是一种优雅的方法,可以解决REAT api中许多常见的问题。GraphQL结合 TypeScript可以帮助开发更好的GraphQL查询类型安全性,提供端到端的输入。

安装

$ npm i @nestjs/graphql graphql-tools graphql apollo-server-express

概览

Nest 提供了两种方式来构建GraphQL项目:code firstschema first.你可以选一种最适合自己的方式,下面介绍的章节主体被划分成两个部分,如果你先采用code first,你应该遵循一个,如果你先采用schema first,你应该使用另一个。

code first方法中,使用装饰器和TypeScript类来生成一个相应的GraphQL模式。如果你喜欢单独使用TypeScript,避免语言语法之间的上下文切换,那么这种方法很有用。

schema first方法中,事实的来源是GraphQL SDL文件。SDL是在不同平台之间共享模式文件的一种与语言无关的方式。Nest会根据GraphQL模式自动生成你的TypeScript定义文件。减少编写冗余样板代码的需要。

Getting started with GraphQL & TypeScript

一旦包安装好,我们就可以导入GraphQLModule并使用forRoot()静态方法配置它。

@@filename()
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({}),
  ],
})
export class AppModule {}

forRoot()方法接受一个options对象作为参数。这些option 对象被传递到底层的Apollo实例,举个例子:你想要禁用你的 playground 和关闭关闭debug 模式,可以查看下吗的例子:

@@filename()
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: false,
      playground: false,
    }),
  ],
})
export class AppModule {}要访问操场,您需要配置并运行一个基本的GraphQL服务器。

如上所述,这些选项将被转发到 ApolloServer 构造函数。

GraphQL playground

playground是一个图形化的、交互式的、浏览器内置的GraphQL IDE,默认情况下可以在与GraphQL服务器本身相同的URL上使用。要访问playground,您需要配置并运行一个基本的GraphQL服务器。

Multiple endpoints

@nestjs/graphql 模块的另一个有用特性是能够同时为多个端点提供服务。这让您可以决定哪些模块应该包含在哪个端点中。默认情况下,GraphQL在整个应用程序中搜索解析器。要将此限制为模块的一个子集,可以使用include属性。

GraphQLModule.forRoot({
  include: [CatsModule],
}),

Code first

code first的方法中,你使用装饰器和TypeScript类来生成相应的GraphQL模式。

想要使用 code first的方法,将autoSchemaFile属性添加到选项对象中:

GraphQLModule.forRoot({
  autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),

autoSchemaFile属性值是创建自动生成的模式的路径。或者,schema可以在内存中动态生成。要启用此功能,请将autoSchemaFile属性设置为true

GraphQLModule.forRoot({
  autoSchemaFile: true,
}),

默认情况下,生成的模式中的类型排序将按照它们在包含的模块中定义的顺序来进行排序。要按字典顺序对模式排序,请将sortSchema属性设置为true

GraphQLModule.forRoot({
  autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
  sortSchema: true,
}),

Schema first

schema first 的方法中,首先向option对象添加一个typePaths属性。typePaths属性指表明GraphQLModule 应该在哪里查找你将要编写的GraphQL SDLshcema定义文件。这些文件将在内存中合并;这允许你将分割shcema为几个文件,并在它们的解析器附近定位它们。

GraphQLModule.forRoot({
  typePaths: ['./**/*.graphql'],
}),

你通常还需要有TypeScript定义(类和接口)来对应GraphQLSDL类型,手动创建相应的TypeScript定义既冗余又繁琐。在SDL中做的每一个改变都迫使我们调整TypeScript的定义,为了解决这个问题,  @nestjs/graphql 包可以从(AST)抽象语法树中自动生成TypeScript定义,为了可以启用这个属性,请在GraphQLModule模块中定义definitions options 属性。

GraphQLModule.forRoot({
  typePaths: ['./**/*.graphql'],
  definitions: {
    path: join(process.cwd(), 'src/graphql.ts'),
  },
}),

definitions options对象的path属性指明了在哪里保存生成的TypeScript输出,默认情况下,所有生成的TypeScript类型都被创建为接口,要生成类,请将outputAs属性的值指定为class

GraphQLModule.forRoot({
  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

有的时候,我们需要可以按照制定的规则,当指定(满足要求的文件)的文件发生改变的时候,自动识别其内容并做对应的调整,这个时候我们需要设置监听模式。将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,
});

Accessing generated schema

在某些情况下(例如端到端测试),你可能希望引用生成的架构对象,在端到端测试中,可以使用graphql对象运行查询,而不需要使用任何HTTP listeners

你可以使用GraphQLSchemaHost类来访问生成的模式.

const { schema } = app.get(GraphQLSchemaHost);

Async configuration

当你需要异步而不是静态地传递模块选项时,使用forRootAsync() 方法,与大多数动态模块一样,Nest提供几种技术来处理异步配置。

第一种方式是使用工厂方法:

GraphQLModule.forRootAsync({
  useFactory: () => ({
    typePaths: ['./**/*.graphql'],
  }),
}),

像其他工厂providers程序一样,我们的工厂函数可以是异步的,并且可以通过inject注入依赖项。

GraphQLModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    typePaths: configService.getString('GRAPHQL_TYPE_PATHS'),
  }),
  inject: [ConfigService],
}),

或者,你不仅可以使用工厂方法还可以使用类来配置GraphQLModule,如下所示:

GraphQLModule.forRootAsync({
  useClass: GqlConfigService,
}),

上面的构造在GraphQLModule中实例化了GqlConfigService,使用它来创建选项对象。注意,在这个例子中,GqlConfigService必须实现GqlOptionsFactory接口,如下所示: GraphQLModule将在提供的类的实例化对象上调用createGqlOptions()方法。

@Injectable()
class GqlConfigService implements GqlOptionsFactory {
  createGqlOptions(): GqlModuleOptions {
    return {
      typePaths: ['./**/*.graphql'],
    };
  }
}

如果你想重用现有的option提供程序,而不是在GraphQLModule中创建私有备份,可以使用useExisting语法。

GraphQLModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
}),

Resolvers

概念:Resolvers提供将 GraphQL 操作(查询、变异或订阅)转换为数据的指令。它们返回我们在模式中指定的相同结构的数据。要么是同步的,要么是解析为该格式的结果的Promise。通常,您可以手动创建Resolvers映射。从另一方面来说,@nestjs/graphql包可以让我们使用 注释类的装饰器提供的 元数据自动生成一个解析器映射。为了演示使用包特性创建GraphQL API的过程,我们将创建一个简单的authors API

Code first

code first方法,我们没有遵循手工编写GraphQL SDL来创建GraphQL模式的典型过程。相反,我们使用TypeScript装饰器从TypeScript类定义中生成SDL@nestjs/graphql包读取通过装饰器定义的元数据,并自动生成 schema

Object types

GraphQL 模式中的大多数定义都是对象类型。每一个对象类型你的定义的的应该代表一个域对象,这个域对象就是application可能应该与其交互的对象。举个例子:api定义需要能够获取作者和他们的帖子的列表。所以我们需要定义 Author 类型以及 Post类型去支持这个功能。

如果我们使用schema first的方法,我们将用SDL定义这样一个模式:

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post]
}

在这种情况下,使用 code first方法,我们使用TypeScript类来定义模式,并使用TypeScript装饰器来注释这些类的字段,在 code first 的方法中,与上述SDL等价的是:

@@filename(authors/models/author.model)
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[];
}

我们必须在模式定义类中显式地使用@Field()装饰器来提供关于每个字段的GraphQL类型和可选性的元数据.

Author对象类型与任何类一样,由字段的集合组成,每个字段声明一个类型。字段的类型对应于GraphQL类型。字段的GraphQL类型可以是另一种对象类型,也可以是标量类型。GraphQL标量类型是解析为单个值的原语(如IDStringBooleanInt)。

上面的Author对象类型定义将导致Nest生成如上所示的SDL:

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post]
}

@Field()装饰器接受一个可选的类型函数(例如,type => Int)和一个可选的options对象。TypeScript类型体系和GraphQL类型体系之间可能存在歧义时,类型函数是必需的。具体来说:stringboolean是非必需的;number是必需的(必须映射到一个GraphQL IntFloat),type函数应该简单地返回所需的GraphQL类型。

options对象可以有以下任何键/值对:

  • nullable: 用于指定一个字段是否为空(在SDL中,每个字段默认为非空);boolean
  • description: 用于设置字段说明;string
  • deprecationReason: 用于标记一个已弃用的字段;string

举个例子:

@Field({ description: `Book title`, deprecationReason: 'Not useful in v2 schema'})
title: string;

当字段是一个数组时,我们必须在field()装饰器的type函数中手动指定数组类型,如下所示:

@Field(type => [Post])
posts: Post[];

使用数组方括号([]),我们可以表示数组的深度。例如,使用[[Int]]将表示一个整数矩阵。

要声明数组的项(不是数组本身)是可空的,请将nullable属性设置为'items'

@Field(type => [Post], { nullable: 'items' })
posts: Post[];

如果数组和它的项都是可空的,将nullable设置为'itemsAndList'

既然Author对象类型已经创建,那么让我们来定义 Post 对象类型。

@@filename(posts/models/post.model)
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
}

Code first resolver

我们已经定义了已经存在在数据图谱中的对象,但是客户端还没有办法与这些对象进行交互。为了解决这个问题,我们需要创建一个解析器类。在code first的方法中,解析器类既定义解析器函数,又生成查询类型。当我们处理下面的示例时,这一点将会很清楚:

@@filename(authors/authors.resolver)
@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将在运行时合并这些。有关代码组织的更多信息,请参阅下面的模块部分。

AuthorsServicePostsService类内部的逻辑可以根据需要简单或复杂。本例的主要目的是展示如何构造解析器,以及它们如何与其他提供程序交互。

在上面的例子中,我们创建了AuthorsResolver,它定义了一个查询解析器函数和一个字段解析器函数。要创建一个解析器,我们创建了一个带有解析器函数作为方法的类,并使用@Resolver()装饰器来注释该类。

在这个例子中,我们定义了一个查询处理程序来根据请求中发送的id获取author对象。要指定该方法为查询处理程序,请使用@Query()装饰器。

传递给 @Resolver() 装饰器的参数是可选的,但当我们的图形变得不重要时,它就会起作用。它用于提供一个父对象,供字段解析器函数在遍历对象图时使用。

在我们的例子中,因为类包含了一个字段解析器函数(用于 Author 对象类型的 posts 属性),我们必须为 @Resolver() 装饰器提供一个值来指出哪个类是这个类中定义的所有字段解析器的父类型(即对应的 ObjectType 类名)。从这个例子中可以清楚地看出,在编写一个字段解析器函数时,必须要访问父对象,在这个例子中,我们用一个字段解析器来填充 authorposts 数组,这个字段解析器调用一个以 authorid 作为参数的服务。因此需要在 @Resolver() 装饰器中标识父对象。注意 @Parent() 方法参数装饰器的相应使用,然后在字段解析器中提取对父对象的引用。

我们可以定义多个@Query()解析器函数(既在这个类中,也在任何其他解析器类中),它们将在生成的SDL中与解析器映射中的适当条目一起聚合为单个Query类型定义。这允许我们定义接近于它们使用的moduleservicce的查询,并使它们在模块中保持良好的组织。

Query type names

在上面的例子中,@Query()装饰器基于方法名生成一个GraphQL模式查询类型名。举个例子,根据上面的例子考虑下面的结构:

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

这将为我们的模式中的author查询生成以下条目:

type Query {
  author(id: Int!): Author
}

通常,我们倾向于解耦这些名称;相对于仍然使用author作为查询类型名称,我们更喜欢使用getAuthor()这样的名称作为查询处理程序方法,这同样适用于我们的域field resolvers,通过将映射名称作为@Query()@ResolveField()装饰器的参数传递,我们可以很容易地做到这一点,如下所示:

@@filename(authors/authors.resolver)
@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
}

Query decorator options

 @Query()装饰器对象接受许多键值对组合:

  • name: 参数的名称 ;String
  • description:用于生成GraphQL模式文档的描述;String
  • deprecationReason:设置查询元数据以显示已弃用的查询;String
  • nullable:查询是否可以返回空数据响应;boolean,'items','itemsAndList'

Args decorator options

@Args()装饰器从请求中提取参数,以便在方法处理程序中使用。

通常@Args()装饰器会很简单,不像上面的getAuthor()方法那样需要一个对象参数,例如,如果标识符的类型是字符串,下面的构造就足够了,只是从GraphQL请求提取命名字段作为方法参数使用。

@Args('id') id: string

getAuthor()的情况下,使用的是数字类型,这就提出了一个挑战。TypeScript类型的Number并没有给我们足够的信息来描述GraphQL的期望表示的类型(GraphQL的number 类型对应的是 IntFloat)。因此,必须显式传递类型引用。我们通过传递第二个参数给Args()装饰器。

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

options对象允许我们指定以下可选键值对:

  • type: 返回GraphQL类型的函数
  • defaultValue:默认值
  • description:描述元数据;string
  • deprecationReason:弃用一个字段并提供元数据来说明原因;string
  • nullable: 字段是否可以为空

查询处理程序方法可以接受多个参数,假设我们想根据authorfirstNamelastName获取一个author。在本例中,我们可以调用@Args两次

getAuthor(
 @Args('firstName', { nullable: true }) firstName?: string,
 @Args('lastName', { defaultValue: '' }) lastName?: string,
) {}

Dedicated arguments class

使用内联的@Args()调用,像上面的例子一样的代码会变得臃肿。相反,你可以创建一个专用的GetAuthorArgs参数类,并在handler方法中访问它,如下所示:

@Args() args: GetAuthorArgs

使用 @ArgsType()来创建GetAuthorArgs 类:

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

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

  @Field({ defaultValue: '' })
  @MinLength(3)
  lastName: string;
}

这将在SDL中生成GraphQL模式的以下部分:

type Query {
  author(firstName: String, lastName: String = ''): Author
}

Class inheritance

你可以使用标准的TypeScript类继承来创建具有通用实用程序类型特性的基类,这些特征可以被扩展。举个例子:你可能有一组和分页相关的参数,这个参数经常包括一些标准的参数offset 和 limit 字段。还有一些其他特定类型的索引字段,您可以设置如下所示的类层次结构:

基类的参数类型:

@ArgsType()
class PaginationArgs {
  @Field((type) => Int)
  offset: number = 0;

  @Field((type) => Int)
  limit: number = 10;
}

基类@ArgsType()的类型特定子类:

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

  @Field({ defaultValue: '' })
  @MinLength(3)
  lastName: string;
}

同样的方法也适用于@ObjectType()对象。在基类上定义泛型属性:

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

  @Field()
  name: string;
}

在子类上添加特定类型的属性:

@ObjectType()
class Warrior extends Character {
  @Field()
  level: number;
}

您也可以在解析器中使用继承。你可以通过结合继承和TypeScript泛型来确保类型安全。例如,要用泛型findAll查询创建基类,可以使用这样的构造:

function BaseResolver<T extends Type<unknown>>(classRef: T): any {
  @Resolver({ isAbstract: true })
  abstract class BaseResolverHost {
    @Query((type) => [classRef], { name: `findAll${classRef.name}` })
    async findAll(): Promise<T[]> {
      return [];
    }
  }
  return BaseResolverHost;
}

注意以下几点:

  • 需要显式的返回类型,否则TypeScript会报错private类定义的使用。建议:定义接口而不是使用any
  • Type@nestjs/common包中导入
  • isAbstract: true属性表示不应该为这个类生成SDL(Schema Definition Language statements),注意,您也可以为其他类型设置此属性以抑制SDL生成。

下面是如何生成BaseResolver的具体子类:

@Resolver((of) => Recipe)
export class RecipesResolver extends BaseResolver(Recipe) {
  constructor(private recipesService: RecipesService) {
    super();
  }
}

这个构造将生成以下SDL:

type Query {
  findAllRecipe: [Recipe!]!
}

Generics

我们在上面看到一次泛型的使用。这个强大的TypeScript特性可以用来创建有用的抽象。举个例子:

import { Field, ObjectType, Int } from '@nestjs/graphql';
import { Type } from '@nestjs/common';

export function Paginated<T>(classRef: Type<T>): any {
  @ObjectType(`${classRef.name}Edge`)
  abstract class EdgeType {
    @Field((type) => String)
    cursor: string;

    @Field((type) => classRef)
    node: T;
  }

  @ObjectType({ isAbstract: true })
  abstract class PaginatedType {
    @Field((type) => [EdgeType], { nullable: true })
    edges: EdgeType[];

    @Field((type) => [classRef], { nullable: true })
    nodes: T[];

    @Field((type) => Int)
    totalCount: number;

    @Field()
    hasNextPage: boolean;
  }
  return PaginatedType;
}

定义了上述基类之后,我们现在可以轻松地创建继承此行为的专门化类型。例如:

@ObjectType()
class PaginatedAuthor extends Paginated(Author) {}

Schema first

如前一章所述,在Schema first的方法中,我们首先在SDL中手动定义模式类型,考虑以下SDL类型定义。

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post]
}

type Post {
  id: Int!
  title: String!
  votes: Int
}

type Query {
  author(id: Int!): Author
}

Schema first resolver

上面的模式公开了一个查询- author(id: Int!): author

现在让我们创建一个 AuthorsResolver 类来解析作者查询:

@@filename(authors/authors.resolver)
@Resolver('Author')
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query()
  async author(@Args('id') id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField()
  async posts(@Parent() author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

@Resolver()装饰器是必需的。它接受一个带有类名的可选字符串参数。每当类包含@ResolveField()装饰器时,都需要这个类名,以通知Nest这个装饰方法与父类型(当前示例中的Author类型)相关联。或者,不要在类的顶部设置@Resolver(),可以对每个方法都这样做:

@Resolver('Author')
@ResolveField()
async posts(@Parent() author) {
  const { id } = author;
  return this.postsService.findAll({ authorId: id });
}

在这种案例下,如果你在一个类中有多个@ResolveField() 装饰器,你必须给每一个 都添加@Resolver(),这种做法一看就不是最佳实践。

在上面的例子中,@Query()@ResolveField()装饰器根据方法名与GraphQL模式类型相关联。例如,从上面的例子考虑以下结构:

@Query()
async author(@Args('id') id: number) {
  return this.authorsService.findOneById(id);
}

这将在我们的模式中为author查询生成以下内容:

type Query {
  author(id: Int!): Author
}

通常,我们更倾向于将这些解耦,为我们的解析器方法使用像getAuthor()getPosts()这样的名称。我们可以通过将映射名作为参数传递给装饰器来轻松做到这一点,如下所示:

@@filename(authors/authors.resolver)
@Resolver('Author')
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

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

  @ResolveField('posts')
  async getPosts(@Parent() author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Generating types

假设我们使用schema first的方法,并启用了类型生成特性,一旦你启动了项目就会自动的生成下面的文件,举个例子:

export class Author {
  id: number;
  firstName?: string;
  lastName?: string;
  posts?: Post[];
}

export class Post {
  id: number;
  title: string;
  votes?: number;
}

export abstract class IQuery {
  abstract author(id: number): Author | Promise<Author>;
}

通过生成类(而不是默认的生成接口的技术),可以结合schema first的方法使用声明性验证装饰器,这是一种非常有用的技术(请阅读更多内容)。例如,您可以将类验证器装饰器添加到生成的CreatePostInput类中,如下所示,以强制标题字段上的最小和最大字符串长度。

import { MinLength, MaxLength } from 'class-validator';

export class CreatePostInput {
  @MinLength(3)
  @MaxLength(50)
  title: string;
}

GraphQL argument decorators

我们可以使用专用的装饰器来访问标准的GraphQL解析器参数。下面是Nest装饰器和它们所代表的普通 Apollo 参数的比较。

image.png

  • root:一个对象,包含解析器在父字段上返回的结果,或者,在顶级查询字段的情况下,从服务器配置传递的rootValue。
  • context:在特定查询中被所有解析器共享的对象;通常用于包含每个请求的状态。
  • info: 包含有关查询执行状态的信息的对象。
  • args:一个对象,其参数被传递到查询中的字段中。

Module

完成上述步骤后,我们已经声明性地指定了GraphQLModule生成解析器映射所需的所有信息,GraphQLModule使用反射来内省通过装饰器提供的元数据,并自动将类转换为正确的解析器映射。

你需要注意的另一件事是 provide 解析器类(AuthorsResolver),并在某个地方导入模块(AuthorsModule),这样Nest就可以利用它了。

我们可以在AuthorsModule中做到这一点,它还可以提供此上下文中需要的其他服务。确保在某个地方导入AuthorsModule

@@filename(authors/authors.module.ts)
@Module({
  imports: [PostsModule],
  providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}

Mutations

概念:GraphQL 的大多数讨论都集中在数据获取上,但是任何完整的数据平台都需要一种方法来修改服务器端数据。在REST中,任何请求最终都可能对服务器造成副作用,但最佳实践建议我们不应该修改GET请求中的数据。GraphQL与此类似——从技术上讲,任何查询都可以实现数据写入。但是,与REST一样,建议遵循这样的约定:任何导致写入的操作都应该通过突变显式发送。

官方的Apollo文档使用了upvotePost() mutation突变示例。这个突变实现了一个增加帖子投票属性值的方法。为了在Nest中创建一个等价mutation,我们将使用@Mutation()装饰器。

Code first

让我们向上一节中使用的 AuthorResolver 添加另一个方法。

@Mutation(returns => Post)
async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
  return this.postsService.upvoteById({ id: postId });
}

这将导致在SDL中生成下面的GraphQL模式:

type Mutation {
  upvotePost(postId: Int!): Post
}

upvotePost()方法接受postId (Int)作为参数,并返回一个更新后的Post实体。由于解析器一节中解释的原因,我们必须显式地设置预期的类型。

如果 mutation 需要接受一个对象作为参数,我们可以创建一个输入类型。输入类型是一种特殊的对象类型,可以作为参数传入。要声明输入类型,请使用@InputType()装饰器。

import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class UpvotePostInput {
  @Field()
  postId: number;
}

然后我们可以在解析器类中使用这个类型:

@Mutation(returns => Post)
async upvotePost(
  @Args('upvotePostData') upvotePostData: UpvotePostInput,
) {}

Schema first

让我们扩展上一节中使用的 AuthorResolver

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

注意,上面我们假设业务逻辑已经转移到PostsService,PostsService类内部的逻辑可以根据需要简单或复杂。本示例的主要目的是展示解析器如何与其他提供程序交互。

最后一步是将我们的变异添加到现有的类型定义中。

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post]
}

type Post {
  id: Int!
  title: String
  votes: Int
}

type Query {
  author(id: Int!): Author
}

type Mutation {
  upvotePost(postId: Int!): Post
}

Subscriptions

概念:除了使用查询获取数据和使用mutations修改数据外,GraphQL规范还支持第三种操作类型,称为订阅(subscription)。GraphQL订阅是一种将数据从服务器推送到选择侦听来自服务器的实时消息的客户机的方法。订阅与查询类似,它们指定一组要交付给客户端的字段,但不是立即返回单个答案,而是在每次服务器上发生特定事件时打开一个通道并将结果发送给客户端。

订阅的一个常见用例是通知客户端特定的事件,例如新对象的创建、更新的字段等。

Enable subscriptions with Apollo driver

启用订阅,请将installSubscriptionHandlers属性设置为true

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  installSubscriptionHandlers: true,
}),

要切换到使用graphql-ws包,请使用以下配置:

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  subscriptions: {
    'graphql-ws': true
  },
}),

Code first

要使用代码优先方法创建订阅,我们使用@Subscription()装饰器(从@nestjs/graphql包导出)和graphql-subscriptions包中的PubSub类,后者提供了一个简单的 发布/订阅 API。

下面的订阅处理程序通过调用PubSub#asyncIterator来处理对事件的订阅。此方法接受单个参数triggerName,它对应于一个事件主题名称。

const pubSub = new PubSub();

@Resolver((of) => Author)
export class AuthorResolver {
  // ...
  @Subscription((returns) => Comment)
  commentAdded() {
    return pubSub.asyncIterator('commentAdded');
  }
}

所有的装饰器都是从@nestjs/graphql包导出的,而PubSub类是从graphql-subscriptions包导出的。

这将导致在SDL中生成下面的GraphQL模式: