GraphQL

快速开始

GraphQL 是一种用于 API 的查询语言,是使用现有数据来完成这些查询的运行时。这是一种优雅的方法,可以解决我们在典型REST apis 中遇到的许多问题 。这里是 GraphQL 和 REST 之间一个很好的比较 。在这组文章中, 我们将不解释什么是 GraphQL, 而是演示如何使用 @nestjs/graphql 模块。本章假定你已经熟练GraphQL。

GraphQLModule 仅仅是 Apollo Server 的包装器。我们没有造轮子, 而是提供一个现成的模块, 这让 GraphQL 和 Nest 有了比较简洁的融合方式。

安装

首先,我们需要安装以下依赖包:

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

概述

Nest 提供了两种构建 GraphQL 应用程序的方式,模式优先和代码优先。

模式优先的方式,本质是 GraphQL SDL(模式定义语言)。它以一种与语言无关的方式,基本允许您在不同平台之间共享模式文件。此外,Nest 将根据GraphQL 模式(通过类或接口)自动生成 TypeScript 定义,以减少冗余。

另一方面,在代码优先的方法中,您将仅使用装饰器和 TypeScript 类来生成相应的 GraphQL 架构。如果您更喜欢使用 TypeScript 来工作并避免语言语法之间的上下文切换,那么它变得非常方便。

入门

依赖包安装完成后,我们就可以注册 GraphQLModule

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { GraphQLModule } from '@nestjs/graphql';
  3. @Module({
  4. imports: [
  5. GraphQLModule.forRoot({}),
  6. ],
  7. })
  8. export class ApplicationModule {}

.forRoot() 函数将选项对象作为参数。这些选项将传递给底层的 Apollo 实例(请在此处阅读有关可用设置的更多信息)。例如,如果要禁用playground并关闭debug模式,只需传递以下选项:

  1. import { Module } from '@nestjs/common';
  2. import { GraphQLModule } from '@nestjs/graphql';
  3. @Module({
  4. imports: [
  5. GraphQLModule.forRoot({
  6. debug: false,
  7. playground: false,
  8. }),
  9. ],
  10. })
  11. export class ApplicationModule {}

如上所述,所有这些设置都将传递给ApolloServer构造函数。

Playground

Playground 是一个图形化的,交互式的浏览器内 GraphQL IDE,默认情况下可与 GraphQL 服务器本身 URL 相同。当您的应用程序在后台运行时,打开 Web 浏览器并访问: http://localhost:3000/graphql (主机和端口可能因您的配置而异)。

GraphQL - 图1

多个端点

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

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

模式优先

当使用模式优先的方式,最简单的方法是为 typePaths 数组中添加对象即可。

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

该 typePaths 属性指示 GraphQLModule 应该查找 GraphQL 文件的位置。所有这些文件最终将合并到内存中,这意味着您可以将模式拆分为多个文件并将它们放在靠近解析器的位置。

同时创建 GraphQL 类型和相应的 TypeScript 定义会产生不必要的冗余。导致我们没有单一的实体来源,SDL 内部的每个变化都促使我们调整接口。因此,该@nestjs/graphql 包提供了另一个有趣的功能,使用抽象语法树(AST)自动生成TS定义。要启用它,只需添加 definitions 属性即可。

  1. GraphQLModule.forRoot({
  2. typePaths: ['./**/*.graphql'],
  3. definitions: {
  4. path: join(process.cwd(), 'src/graphql.ts'),
  5. },
  6. }),

src/graphql.ts 为TypeScript输出文件。默认情况下,所有类型都转换为接口。您也可以通过将 outputAs 属性改为切换到 class

  1. GraphQLModule.forRoot({
  2. typePaths: ['./**/*.graphql'],
  3. definitions: {
  4. path: join(process.cwd(), 'src/graphql.ts'),
  5. outputAs: 'class',
  6. },
  7. }),

事实上,每个应用程序启动时都生成类型定义并不是必须的。我们可能更喜欢完全控制,只在执行专用命令时才生成类型定义文件。在这种情况下,我们可以通过创建自己的脚本来实现,比如说 generate-typings.ts:

  1. import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
  2. import { join } from 'path';
  3. const definitionsFactory = new GraphQLDefinitionsFactory();
  4. definitionsFactory.generate({
  5. typePaths: ['./src/**/*.graphql'],
  6. path: join(process.cwd(), 'src/graphql.ts'),
  7. outputAs: 'class',
  8. });

然后,只需运行:

  1. ts-node generate-typings

您也可以预先编译脚本并使用 node 可执行文件。

当需要切换到文件监听模式(在任何 .graphql 文件更改时自动生成 Typescript),请将 watch 选项传递给 generate() 函数。

  1. definitionsFactory.generate({
  2. typePaths: ['./src/**/*.graphql'],
  3. path: join(process.cwd(), 'src/graphql.ts'),
  4. outputAs: 'class',
  5. watch: true,
  6. });

这里 提供完整的例子。

代码优先

在代码优先方法中,您将只使用装饰器和 TypeScript 类来生成相应的 GraphQL 架构。

Nest 通过使用一个惊艳的type-graphql 库,来提供此功能。为此,在我们继续之前,您必须安装此软件包。

  1. $ npm i type-graphql

安装过程完成后,我们可以使用 autoSchemaFile 向 options 对象添加属性。

  1. GraphQLModule.forRoot({
  2. typePaths: ['./**/*.graphql'],
  3. autoSchemaFile: 'schema.gql',
  4. }),

这里 autoSchemaFile 是您自动生成的gql文件将被创建的路径。您一样可以传递 buildSchemaOptions 属性 - 用于传递给 buildSchema() 函数的选项(从type-graphql包中)。

这里 提供完整的例子。

Async 配置

大多数情况下, 您可能希望异步传递模块选项, 而不是预先传递它们。在这种情况下, 请使用 forRootAsync() 函数, 它提供了处理异步数据的几种不同方法。

第一种方法是使用工厂功能:

  1. GraphQLModule.forRootAsync({
  2. useFactory: () => ({
  3. typePaths: ['./**/*.graphql'],
  4. }),
  5. }),

我们的 factory 的行为和其他人一样 (可能是异步的, 并且能够通过 inject 注入依赖关系)。

  1. GraphQLModule.forRootAsync({
  2. imports: [ConfigModule],
  3. useFactory: async (configService: ConfigService) => ({
  4. typePaths: configService.getString('GRAPHQL_TYPE_PATHS'),
  5. }),
  6. inject: [ConfigService],
  7. }),

当然除了factory, 您也可以使用类。

  1. GraphQLModule.forRootAsync({
  2. useClass: GqlConfigService,
  3. }),

上面的构造将实例化 GqlConfigService 内部 GraphQLModule, 并将利用它来创建选项对象。GqlConfigService 必须实现 GqlOptionsFactory 接口。

  1. @Injectable()
  2. class GqlConfigService implements GqlOptionsFactory {
  3. createGqlOptions(): GqlModuleOptions {
  4. return {
  5. typePaths: ['./**/*.graphql'],
  6. };
  7. }
  8. }

为了防止 GqlConfigService内部创建 GraphQLModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。

  1. GraphQLModule.forRootAsync({
  2. imports: [ConfigModule],
  3. useExisting: ConfigService,
  4. }),

它的工作原理与 useClass 有一个关键的区别—— GraphQLModule 将查找导入的模块可重用的已经创建的 ConfigService, 而不是单独实例化它。

例子

这里 提供完整的案例。

解析图

通常,您必须手动创建解析图。 @nestjs/graphql 包也产生解析器映射,可以自动使用由装饰器提供的元数据。为了学习库基础知识,我们将创建一个简单的用户 API。

模式优先

正如提到以前的章节,让我们在 SDL 中定义我们的类型(阅读更多):

  1. type Author {
  2. id: Int!
  3. firstName: String
  4. lastName: String
  5. posts: [Post]
  6. }
  7. type Post {
  8. id: Int!
  9. title: String!
  10. votes: Int
  11. }
  12. type Query {
  13. author(id: Int!): Author
  14. }

我们的 GraphQL 架构包含公开的单个查询 author(id: Int!): Author 。现在,让我们创建一个 AuthorResolver

  1. @Resolver('Author')
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query()
  8. async author(@Args('id') id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty()
  12. async posts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

提示:使用 @Resolver() 装饰器则不必将类标记为 @Injectable() ,否则必须这么做。

@Resolver() 装饰器不影响查询和对象变动 (@Query()@Mutation() 装饰器)。这只会通知 Nest, 每个 @ResolveProperty() 有一个父节点, Author 在这种情况下是父节点, Author在这种情况下是一个类型(Author.posts 关系)。基本上,不是为类设置 @Resolver() ,而是为函数:

  1. @Resolver('Author')
  2. @ResolveProperty()
  3. async posts(@Parent() author) {
  4. const { id } = author;
  5. return await this.postsService.findAll({ authorId: id });
  6. }

但当 @ResolveProperty() 在一个类中有多个,则必须为所有的都添加 @Resolver(),这不是一个好习惯(额外的开销)。

通常, 我们会使用像 getAuthor()getPosts() 之类的函数来命名。通过将真实名称放在装饰器里很容易地做到这一点。

  1. @Resolver('Author')
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query('author')
  8. async getAuthor(@Args('id') id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty('posts')
  12. async getPosts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

这个 @Resolver() 装饰器可以在函数级别被使用。

Typings

假设我们已经启用了分型生成功能(带outputAs: ‘class’)在前面的章节,一旦你运行应用程序,应该生成以下文件:

  1. export class Author {
  2. id: number;
  3. firstName?: string;
  4. lastName?: string;
  5. posts?: Post[];
  6. }
  7. export class Post {
  8. id: number;
  9. title: string;
  10. votes?: number;
  11. }
  12. export abstract class IQuery {
  13. abstract author(id: number): Author | Promise<Author>;
  14. }

类允许您使用装饰器,这使得它们在验证方面非常有用(阅读更多)。例如:

  1. import { MinLength, MaxLength } from 'class-validator';
  2. export class CreatePostInput {
  3. @MinLength(3)
  4. @MaxLength(50)
  5. title: string;
  6. }

要启用输入(和参数)的自动验证,必须使用 ValidationPipe 。了解更多有关验证或者更具体

尽管如此,如果将装饰器直接添加到自动生成的文件中,它们将在每次连续更改时被丢弃。因此,您应该创建一个单独的文件,并简单地扩展生成的类。

  1. import { MinLength, MaxLength } from 'class-validator';
  2. import { Post } from '../../graphql.ts';
  3. export class CreatePostInput extends Post {
  4. @MinLength(3)
  5. @MaxLength(50)
  6. title: string;
  7. }

代码优先

在代码优先方法中,我们不必手动编写SDL。相反,我们只需使用装饰器。

  1. import { Field, Int, ObjectType } from 'type-graphql';
  2. import { Post } from './post';
  3. @ObjectType()
  4. export class Author {
  5. @Field(type => Int)
  6. id: number;
  7. @Field({ nullable: true })
  8. firstName?: string;
  9. @Field({ nullable: true })
  10. lastName?: string;
  11. @Field(type => [Post])
  12. posts: Post[];
  13. }

Author 模型已创建。现在,让我们创建缺少的 Post 类。

  1. import { Field, Int, ObjectType } from 'type-graphql';
  2. @ObjectType()
  3. export class Post {
  4. @Field(type => Int)
  5. id: number;
  6. @Field()
  7. title: string;
  8. @Field(type => Int, { nullable: true })
  9. votes?: number;
  10. }

由于我们的模型准备就绪,我们可以转到解析器类。

  1. @Resolver(of => Author)
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query(returns => Author)
  8. async author(@Args({ name: 'id', type: () => Int }) id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty()
  12. async posts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

通常,我们会使用类似 getAuthor() 或 getPosts() 函数名称。我们可以通过将真实名称移动到装饰器里来轻松完成此操作。

  1. @Resolver(of => Author)
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query(returns => Author, { name: 'author' })
  8. async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty('posts')
  12. async getPosts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

通常,您不必将此类对象传递给 @Args() 装饰器。例如,如果您的标识符的类型是字符串,则以下结构就足够了:

  1. @Args('id') id: string

但是,该 number. 类型没有提供 type-graphql 有关预期的 GraphQL 表示( Int vs Float )的足够信息,因此,我们必须显式传递类型引用。

而且,您可以创建一个专用 AuthorArgs 类:

  1. @Args() id: AuthorArgs

用以下结构:

  1. @ArgsType()
  2. class AuthorArgs {
  3. @Field(type => Int)
  4. @Min(1)
  5. id: number;
  6. }

@Field()@ArgsType() 装饰器都是从 type-graphql 包中导入的,而 @Min() 来自 class-validator

您可能还会注意到这些类与 ValidationPipe 相关(更多内容)。

装饰

在上面的示例中,您可能会注意到我们使用专用装饰器来引用以下参数。下面是提供的装饰器和它们代表的普通 Apollo 参数的比较。

@Root()@Parent() root/parent
@Context(param?:string) context/context[param]
@Info(param?:string) info/info[param]
@Args(param?:string) args/args[param]

Module

一旦我们在这里完成,我们必须将 AuthorResolver 注册,例如在新创建的 AuthorsModule 内部注册。

  1. @Module({
  2. imports: [PostsModule],
  3. providers: [AuthorsService, AuthorResolver],
  4. })
  5. export class AuthorsModule {}

GraphQLModule 会考虑反映了元数据和转化类到正确的解析器的自动映射。您应该注意的是您需要在某处 import 此模块,Nest 才会知道 AuthorsModule 确实存在。

提示:在此处了解有关 GraphQL 查询的更多信息。

变更(Mutations)

在 GraphQL 中,为了变更服务器端数据,我们使用了变更(在这里阅读更多) 。官方 Apollo 文档共享一个 upvotePost() 变更示例。该变更允许增加 votes 属性值。为了在 Nest 中创建等效变更,我们将使用 @Mutation() 装饰器。

模式优先

让我们扩展我们在上一节中AuthorResolver的用法(见解析图)。

  1. @Resolver('Author')
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query('author')
  8. async getAuthor(@Args('id') id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @Mutation()
  12. async upvotePost(@Args('postId') postId: number) {
  13. return await this.postsService.upvoteById({ id: postId });
  14. }
  15. @ResolveProperty('posts')
  16. async getPosts(@Parent() { id }) {
  17. return await this.postsService.findAll({ authorId: id });
  18. }
  19. }

请注意,我们假设业务逻辑已移至 PostsService(分别查询 post 和 incrementing votes 属性)。

类型定义

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

  1. type Author {
  2. id: Int!
  3. firstName: String
  4. lastName: String
  5. posts: [Post]
  6. }
  7. type Post {
  8. id: Int!
  9. title: String
  10. votes: Int
  11. }
  12. type Query {
  13. author(id: Int!): Author
  14. }
  15. type Mutation {
  16. upvotePost(postId: Int!): Post
  17. }

upvotePost(postId: Int!): Post 变更现在可用!

代码优先

让我们使用 在上一节中AuthorResolver另一种方法(参见解析图)。

  1. @Resolver(of => Author)
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query(returns => Author, { name: 'author' })
  8. async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @Mutation(returns => Post)
  12. async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
  13. return await this.postsService.upvoteById({ id: postId });
  14. }
  15. @ResolveProperty('posts')
  16. async getPosts(@Parent() author) {
  17. const { id } = author;
  18. return await this.postsService.findAll({ authorId: id });
  19. }
  20. }

upvotePost()postIdInt)作为输入参数,并返回更新的 Post 实体。出于与解析器部分相同的原因,我们必须明确设置预期类型。

如果变异必须将对象作为参数,我们可以创建一个输入类型。

  1. @InputType()
  2. export class UpvotePostInput {
  3. @Field() postId: number;
  4. }

@InputType()@Field() 需要 import type-graphql 包。

然后在解析图类中使用它:

  1. @Mutation(returns => Post)
  2. async upvotePost(
  3. @Args('upvotePostData') upvotePostData: UpvotePostInput,
  4. ) {}

订阅(Subscriptions)

订阅只是查询和变更的另一种 GraphQL 操作类型。它允许通过双向传输层创建实时订阅,主要通过 websockets 实现。在这里阅读更多关于订阅的内容。

以下是 commentAdded 订阅示例,可直接从官方 Apollo 文档复制和粘贴:

  1. Subscription: {
  2. commentAdded: {
  3. subscribe: () => pubSub.asyncIterator('commentAdded');
  4. }
  5. }

pubsub 是一个 PubSub 类的实例。在这里阅读更多。

模式优先

为了以 Nest 方式创建等效订阅,我们将使用 @Subscription() 装饰器。

  1. const pubSub = new PubSub();
  2. @Resolver('Author')
  3. export class AuthorResolver {
  4. constructor(
  5. private readonly authorsService: AuthorsService,
  6. private readonly postsService: PostsService,
  7. ) {}
  8. @Query('author')
  9. async getAuthor(@Args('id') id: number) {
  10. return await this.authorsService.findOneById(id);
  11. }
  12. @ResolveProperty('posts')
  13. async getPosts(@Parent() author) {
  14. const { id } = author;
  15. return await this.postsService.findAll({ authorId: id });
  16. }
  17. @Subscription()
  18. commentAdded() {
  19. return pubSub.asyncIterator('commentAdded');
  20. }
  21. }

为了根据上下文和参数过滤掉特定事件,我们可以设置一个 filter 属性。

  1. @Subscription('commentAdded', {
  2. filter: (payload, variables) =>
  3. payload.commentAdded.repositoryName === variables.repoFullName,
  4. })
  5. commentAdded() {
  6. return pubSub.asyncIterator('commentAdded');
  7. }

为了改变已发布的有效负载,我们可以使用 resolve 函数。

  1. @Subscription('commentAdded', {
  2. resolve: value => value,
  3. })
  4. commentAdded() {
  5. return pubSub.asyncIterator('commentAdded');
  6. }

类型定义

最后一步是更新类型定义文件。

  1. type Author {
  2. id: Int!
  3. firstName: String
  4. lastName: String
  5. posts: [Post]
  6. }
  7. type Post {
  8. id: Int!
  9. title: String
  10. votes: Int
  11. }
  12. type Query {
  13. author(id: Int!): Author
  14. }
  15. type Comment {
  16. id: String
  17. content: String
  18. }
  19. type Subscription {
  20. commentAdded(repoFullName: String!): Comment
  21. }

做得好。我们创建了一个 commentAdded(repoFullName: String!): Comment 订阅。您可以在此处找到完整的示例实现。

使用 Typescript

要使用 class-first 方法创建订阅,我们将使用 @Subscription() 装饰器。

  1. const pubSub = new PubSub();
  2. @Resolver('Author')
  3. export class AuthorResolver {
  4. constructor(
  5. private readonly authorsService: AuthorsService,
  6. private readonly postsService: PostsService,
  7. ) {}
  8. @Query(returns => Author, { name: 'author' })
  9. async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
  10. return await this.authorsService.findOneById(id);
  11. }
  12. @ResolveProperty('posts')
  13. async getPosts(@Parent() author) {
  14. const { id } = author;
  15. return await this.postsService.findAll({ authorId: id });
  16. }
  17. @Subscription(returns => Comment)
  18. commentAdded() {
  19. return pubSub.asyncIterator('commentAdded');
  20. }
  21. }

为了根据上下文和参数过滤掉特定事件,我们可以设置 filter 属性。

  1. @Subscription(returns => Comment, {
  2. filter: (payload, variables) =>
  3. payload.commentAdded.repositoryName === variables.repoFullName,
  4. })
  5. commentAdded() {
  6. return pubSub.asyncIterator('commentAdded');
  7. }

为了改变已发布的有效负载,我们可以使用 resolve 函数。

  1. @Subscription(returns => Comment, {
  2. resolve: value => value,
  3. })
  4. commentAdded() {
  5. return pubSub.asyncIterator('commentAdded');
  6. }

Pubsub

我们在这里使用了一个本地 PubSub 实例。相反, 我们应该将 PubSub 定义为一个组件, 通过构造函数 (使用 @Inject () 装饰器) 注入它, 并在整个应用程序中重用它。您可以在此了解有关嵌套自定义组件的更多信息

  1. {
  2. provide: 'PUB_SUB',
  3. useValue: new PubSub(),
  4. }

Module

为了启用订阅,我们必须将 installSubscriptionHandlers 属性设置为 true

  1. GraphQLModule.forRoot({
  2. typePaths: ['./**/*.graphql'],
  3. installSubscriptionHandlers: true,
  4. }),

要自定义订阅服务器(例如,更改端口),您可以使用 subscriptions 属性(阅读更多)。

标量

该GraphQL包括以下默认类型:IntFloatStringBooleanID。但是,有时您可能需要支持自定义原子数据类型(例如 Date )。

模式优先

为了定义一个自定义标量(在这里阅读更多关于标量的信息),我们必须创建一个类型定义和一个专用的解析器。在这里(如在官方文档中),我们将采取 graphql-type-json 包用于演示目的。这个npm包定义了一个JSONGraphQL标量类型。首先,让我们安装包:

  1. $ npm i --save graphql-type-json

然后,我们必须将自定义解析器传递给 forRoot() 函数:

  1. import * as GraphQLJSON from 'graphql-type-json';
  2. @Module({
  3. imports: [
  4. GraphQLModule.forRoot({
  5. typePaths: ['./**/*.graphql'],
  6. resolvers: { JSON: GraphQLJSON },
  7. }),
  8. ],
  9. })
  10. export class ApplicationModule {}

现在, 我们可以在类型定义中使用 JSON 标量:

  1. scalar JSON
  2. type Foo {
  3. field: JSON
  4. }

定义标量类型的另一种形式是创建一个简单的类。假设我们想用 Date 类型增强我们的模式。

  1. import { Scalar, CustomScalar } from '@nestjs/graphql';
  2. import { Kind, ValueNode } from 'graphql';
  3. @Scalar('Date')
  4. export class DateScalar implements CustomScalar<number, Date> {
  5. description = 'Date custom scalar type';
  6. parseValue(value: number): Date {
  7. return new Date(value); // value from the client
  8. }
  9. serialize(value: Date): number {
  10. return value.getTime(); // value sent to the client
  11. }
  12. parseLiteral(ast: ValueNode): Date {
  13. if (ast.kind === Kind.INT) {
  14. return new Date(ast.value);
  15. }
  16. return null;
  17. }
  18. }

之后,我们需要注册 DateScalar 为提供者。

  1. @Module({
  2. providers: [DateScalar],
  3. })
  4. export class CommonModule {}

现在我们可以在 Date 类型定义中使用标量。

  1. scalar Date

使用 Typescript

要创建 Date 标量,只需创建一个新类。

  1. import { Scalar, CustomScalar } from '@nestjs/graphql';
  2. import { Kind, ValueNode } from 'graphql';
  3. @Scalar('Date', type => Date)
  4. export class DateScalar implements CustomScalar<number, Date> {
  5. description = 'Date custom scalar type';
  6. parseValue(value: number): Date {
  7. return new Date(value); // value from the client
  8. }
  9. serialize(value: Date): number {
  10. return value.getTime(); // value sent to the client
  11. }
  12. parseLiteral(ast: ValueNode): Date {
  13. if (ast.kind === Kind.INT) {
  14. return new Date(ast.value);
  15. }
  16. return null;
  17. }
  18. }

准备好后,注册 DateScalar 为provider。

  1. @Module({
  2. providers: [DateScalar],
  3. })
  4. export class CommonModule {}

现在可以在类中使用 Date 类型。

  1. @Field()
  2. creationDate: Date;

工具

在GraphQL世界中,很多文章抱怨如何处理诸如身份验证或操作的副作用之类的东西。我们应该把它放在业务逻辑中吗?我们是否应该使用更高阶的函数来增强查询和变更,例如,使用授权逻辑?或者也许使用模式指令。无论如何,没有一个答案。

Nest生态系统正试图利用守卫拦截器等现有功能帮助解决这个问题。它们背后的想法是减少冗余,并为您提供有助于创建结构良好,可读且一致的应用程序的工具。

概述

您可以以与简单的 REST 应用程序相同的方式使用守卫拦截器过滤器管道。此外,您还可以通过利用自定义装饰器 特性轻松地创建自己的 decorator。他们都一样。让我们看看下面的代码:

  1. @Query('author')
  2. @UseGuards(AuthGuard)
  3. async getAuthor(@Args('id', ParseIntPipe) id: number) {
  4. return await this.authorsService.findOneById(id);
  5. }

正如您所看到的,GraphQL在看守器和管道方面都能很好地工作。因此,您可以将身份验证逻辑移至守卫,甚至可以复用与 REST 应用程序相同的守卫。拦截器的工作方式完全相同:

  1. @Mutation()
  2. @UseInterceptors(EventsInterceptor)
  3. async upvotePost(@Args('postId') postId: number) {
  4. return await this.postsService.upvoteById({ id: postId });
  5. }

执行上下文

但是,ExecutionContext 看守器和拦截器所接收的情况有所不同。GraphQL 解析器有一个单独的参数集,分别为 root,args,context,和 info。因此,我们需要将 ExecutionContext 转换为 GqlExecutionContext,这非常简单。

  1. import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
  2. import { GqlExecutionContext } from '@nestjs/graphql';
  3. @Injectable()
  4. export class AuthGuard implements CanActivate {
  5. canActivate(context: ExecutionContext): boolean {
  6. const ctx = GqlExecutionContext.create(context);
  7. return true;
  8. }
  9. }

GqlExecutionContext 为每个参数公开相应的函数,比如 getArgs(),getContext()等等。现在,我们可以毫不费力地获取特定于当前处理的请求的每个参数。

异常过滤器

异常过滤器与 GraphQL 应用程序兼容。

  1. @Catch(HttpException)
  2. export class HttpExceptionFilter implements GqlExceptionFilter {
  3. catch(exception: HttpException, host: ArgumentsHost) {
  4. const gqlHost = GqlArgumentsHost.create(host);
  5. return exception;
  6. }
  7. }

GqlExceptionFilter 和 GqlArgumentsHost 需要import @nestjs/graphql 包。

但是,response 在这种情况下,您无法访问本机对象(如在HTTP应用程序中)。

自定义装饰器

如前所述,自定义装饰器功能也可以像 GraphQL 解析器一样工作。但是,Factory 函数采用一组参数而不是 request 对象。

  1. export const User = createParamDecorator(
  2. (data, [root, args, ctx, info]) => ctx.user,
  3. );

然后:

  1. @Mutation()
  2. async upvotePost(
  3. @User() user: UserEntity,
  4. @Args('postId') postId: number,
  5. ) {}

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

译者署名

用户名 头像 职能 签名
@zuohuadong GraphQL - 图2 翻译 专注于 caddy 和 nest,@zuohuadong at Github
@Drixn GraphQL - 图3 翻译 专注于 nginx 和 C++,@Drixn