NestJS 9 GraphQL 中文文档(十七) - Federation

213 阅读8分钟

Nest 最新的版本为V9,较之前的V7/8有不小改动,GraphQL 部分官方文档之前的V8也是本人翻译的,有很多不完善的地方,这次打算重新精细翻译一遍,会持续更新这块内容,并最后贡献给中文文档仓库,为中文社区贡献一份力量。有兴趣的小伙伴记得要关注收藏。

Federation(笔者认为可以翻译为联邦,暂时存疑)

Federation 提供了一种将单体 GraphQL 服务器拆分为独立微服务的方法。它包含两个组件:一个网关,和一个或多个联合微服务。每个微服务拥有部分 schema,网关则将这些 schema 合并为一个 schema,以便可以被客户端消费。

引用Apollo官方文档,Federation 的设计遵循以下核心原则:

  • 构建图表应该是声明式的。通过 Federation,你可以从 schema 中以声明方式组合图表,而不是编写命令式schema拼接代码。
  • 代码应该按关注点拆分,而不是按类型。通常没有一个团队可以单独掌控重要类型(如用户或产品)的每个方面,因此,这些类型的定义应当跨团队和代码库分布,而不是集中在一起。
  • 该图表应该很容易让客户使用。联合服务可以形成一个完整的、以产品为中心的图表,准确反映客户如何使用它。
  • 它只是GraphQL,仅使用符合该语言规范的功能。任何语言,不仅仅是 JavaScript,都可以实现 Federation。

警告 ⚠️
Federation 目前还不支持订阅。

在后续的章节中,我们将设置一个演示应用,它包含一个网关和两个联合端点:用户服务和文章服务。

使用Apollo federation

首先要安装所需的依赖包:

$ npm install --save @apollo/federation @apollo/subgraph

模式优先

“用户服务“ 提供了一个简单的schema。注意@key指令:它指示Apollo查询规划器,如果你指定其id,则可以获取特定的User 实例。另外,注意我们在Query类型前加了extend

type User @key(fields: "id") {
  id: ID!
  name: String!
}

extend type Query {
  getUser(id: ID!): User
}

解析器提供了一个额外的方法叫resolveReference()。每当相关资源需要User实例时,Apollo网关就会触发此方法。我们将在后面的Posts服务中看到这个例子。请注意该方法必须使用@ResolveReference()装饰器来注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';

@Resolver('User')
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query()
  getUser(@Args('id') id: string) {
    return this.usersService.findById(id);
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return this.usersService.findById(reference.id);
  }
}

最后,我们通过在配置对象中注册传递ApolloFederationDriver驱动程序的GraphQLModule来连接所有内容:

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      typePaths: ['**/*.graphql'],
    }),
  ],
  providers: [UsersResolver],
})
export class AppModule {}

代码优先

首先给User实体添加额外的装饰器。

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field((type) => ID)
  id: number;

  @Field()
  name: string;
}

解析器提供了一个额外的方法叫resolveReference()。每当相关资源需要User实例时,Apollo网关就会触发此方法。我们将在后面的Posts服务中看到这个例子。请注意该方法必须使用@ResolveReference()装饰器来注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';

@Resolver((of) => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query((returns) => User)
  getUser(@Args('id') id: number): User {
    return this.usersService.findById(id);
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: number }): User {
    return this.usersService.findById(reference.id);
  }
}

最后,我们通过在配置对象中注册传递ApolloFederationDriver驱动程序的GraphQLModule来连接所有内容:

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: true,
    }),
  ],
  providers: [UsersResolver, UsersService],
})
export class AppModule {}

此处查看代码优先示例代码,而在此处查看模式优先。

联合示例:文章

文章服务应该是通过getPosts查询来提供聚合文章的,还可以使用user.posts字段扩展我们的User类型。

模式优先

“文章服务“ 通过使用extend关键字标记来引用其schema中的 User 类型。它同样在User类型声明了一个额外的属性(posts)。注意用来匹配User实例的@key指令,以及指示id字段在别处管理的@extendal指令。

type Post @key(fields: "id") {
  id: ID!
  title: String!
  body: String!
  user: User
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post]
}

extend type Query {
  getPosts: [Post]
}

在下面的示例中,PostsResolver提供了getUser() 方法,该方法返回一个包含 __typename的引用和你的应用程序可能需要解析该引用的一些其他属性,在本例中为id__typename 被GraphQL 网关用来查明负责User类型的微服务并检索相应的实例。执行resolveReference() 方法时将请求上述的“用户服务“。

import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';

@Resolver('Post')
export class PostsResolver {
  constructor(private postsService: PostsService) {}

  @Query('getPosts')
  getPosts() {
    return this.postsService.findAll();
  }

  @ResolveField('user')
  getUser(@Parent() post: Post) {
    return { __typename: 'User', id: post.userId };
  }
}

最后,我们必须注册GraphQLModule,与“用户服务”一节中的做法相似。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      typePaths: ['**/*.graphql'],
    }),
  ],
  providers: [PostsResolvers],
})
export class AppModule {}

代码优先

首先,我们必须声明一个代表User实体的类。尽管该实体本身存在于另一个服务中,但我们将在这里使用它(扩展其定义)。注意@extends@external指令。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';

@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
  @Field((type) => ID)
  @Directive('@external')
  id: number;

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

现在让我们在User实体上为我们的扩展创建对应的解析器,如下所示:

import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => User)
export class UsersResolver {
  constructor(private readonly postsService: PostsService) {}

  @ResolveField((of) => [Post])
  public posts(@Parent() user: User): Post[] {
    return this.postsService.forAuthor(user.id);
  }
}

我们还必须定义Post实体类:

import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
  @Field((type) => ID)
  id: number;

  @Field()
  title: string;

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

  @Field((type) => User)
  user?: User;
}

还有其解析器:

import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => Post)
export class PostsResolver {
  constructor(private readonly postsService: PostsService) {}

  @Query((returns) => Post)
  findPost(@Args('id') id: number): Post {
    return this.postsService.findOne(id);
  }

  @Query((returns) => [Post])
  getPosts(): Post[] {
    return this.postsService.all();
  }

  @ResolveField((of) => User)
  user(@Parent() post: Post): any {
    return { __typename: 'User', id: post.authorId };
  }
}

最后,把它们绑定到一个模块中。注意schema构建选项,在这里我们指定User是一个孤岛(外部)类型。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: true,
      buildSchemaOptions: {
        orphanedTypes: [User],
      },
    }),
  ],
  providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

这里有一个代码优先模式的工作示例,这里有模式优先模式的工作示例。

联合示例:网关

首先安装所需的依赖包:

$ npm install --save @apollo/gateway

网关需要指定端点列表,它将自动发现相应的schema。因此,对于代码优先和模式优先方法,网关服务的实现将保持不变。

import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
      driver: ApolloGatewayDriver,
      server: {
        // ... Apollo server options
        cors: true,
      },
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [
            { name: 'users', url: 'http://user-service/graphql' },
            { name: 'posts', url: 'http://post-service/graphql' },
          ],
        }),
      },
    }),
  ],
})
export class AppModule {}

这里有一个代码优先模式的工作示例,这里有模式优先模式的工作示例。

使用Mercurius federation

首先安装所需的依赖包:

$ npm install --save @apollo/subgraph @nestjs/mercurius

提示 @apollo/subgraph包被用来构建子图表schema(buildSubgraphSchemaprintSubgraphSchema函数)。

模式优先

“用户服务“ 提供了一个简单的schema。注意@key指令:它指示Mercurius查询规划器,如果你指定其id,则可以获取特定的User 实例。另外,注意我们在Query类型前加了extend

type User @key(fields: "id") {
  id: ID!
  name: String!
}

extend type Query {
  getUser(id: ID!): User
}

解析器提供了一个额外的方法叫resolveReference()。每当相关资源需要User实例时,Mercurius网关就会触发此方法。我们将在后面的Posts服务中看到这个例子。请注意该方法必须使用@ResolveReference()装饰器来注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';

@Resolver('User')
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query()
  getUser(@Args('id') id: string) {
    return this.usersService.findById(id);
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return this.usersService.findById(reference.id);
  }
}

最后,我们通过在配置对象中注册传递MercuriusFederationDriver驱动程序的GraphQLModule来连接所有内容:

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      typePaths: ['**/*.graphql'],
      federationMetadata: true,
    }),
  ],
  providers: [UsersResolver],
})
export class AppModule {}

代码优先

首先给User实体添加额外的装饰器。

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field((type) => ID)
  id: number;

  @Field()
  name: string;
}

解析器提供了一个额外的方法叫resolveReference()。每当相关资源需要User实例时,Mercurius网关就会触发此方法。我们将在后面的Posts服务中看到这个例子。请注意该方法必须使用@ResolveReference()装饰器来注释。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';

@Resolver((of) => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query((returns) => User)
  getUser(@Args('id') id: number): User {
    return this.usersService.findById(id);
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: number }): User {
    return this.usersService.findById(reference.id);
  }
}

最后,我们通过在配置对象中注册传递MercuriusFederationDriver驱动程序的GraphQLModule来连接所有内容:

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      autoSchemaFile: true,
      federationMetadata: true,
    }),
  ],
  providers: [UsersResolver, UsersService],
})
export class AppModule {}

联合示例:文章

文章服务应该是通过getPosts查询来提供聚合文章的,还可以使用user.posts字段扩展我们的User类型。

模式优先

“文章服务“ 通过使用extend关键字标记来引用其schema中的 User 类型。它同样在User类型声明了一个额外的属性(posts)。注意用来匹配User实例的@key指令,以及指示id字段在别处管理的@extendal指令。

type Post @key(fields: "id") {
  id: ID!
  title: String!
  body: String!
  user: User
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post]
}

extend type Query {
  getPosts: [Post]
}

在下面的示例中,PostsResolver提供了getUser() 方法,该方法返回一个包含 __typename的引用和你的应用程序可能需要解析该引用的一些其他属性,在本例中为id__typename 被GraphQL 网关用来查明负责User类型的微服务并检索相应的实例。执行resolveReference() 方法时将请求上述的“用户服务“。

import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';

@Resolver('Post')
export class PostsResolver {
  constructor(private postsService: PostsService) {}

  @Query('getPosts')
  getPosts() {
    return this.postsService.findAll();
  }

  @ResolveField('user')
  getUser(@Parent() post: Post) {
    return { __typename: 'User', id: post.userId };
  }
}

最后,我们必须注册GraphQLModule,与“用户服务”一节中的做法相似。

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      federationMetadata: true,
      typePaths: ['**/*.graphql'],
    }),
  ],
  providers: [PostsResolvers],
})
export class AppModule {}

代码优先

首先,我们必须声明一个代表User实体的类。尽管该实体本身存在于另一个服务中,但我们将在这里使用它(扩展其定义)。注意@extends@external指令。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';

@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
  @Field((type) => ID)
  @Directive('@external')
  id: number;

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

现在让我们在User实体上为我们的扩展创建对应的解析器,如下所示:

import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => User)
export class UsersResolver {
  constructor(private readonly postsService: PostsService) {}

  @ResolveField((of) => [Post])
  public posts(@Parent() user: User): Post[] {
    return this.postsService.forAuthor(user.id);
  }
}

我们还必须定义Post实体类:

import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
  @Field((type) => ID)
  id: number;

  @Field()
  title: string;

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

  @Field((type) => User)
  user?: User;
}

还有其解析器:

import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';

@Resolver((of) => Post)
export class PostsResolver {
  constructor(private readonly postsService: PostsService) {}

  @Query((returns) => Post)
  findPost(@Args('id') id: number): Post {
    return this.postsService.findOne(id);
  }

  @Query((returns) => [Post])
  getPosts(): Post[] {
    return this.postsService.all();
  }

  @ResolveField((of) => User)
  user(@Parent() post: Post): any {
    return { __typename: 'User', id: post.authorId };
  }
}

最后,把它们绑定到一个模块中。注意schema构建选项,在这里我们指定User为一个孤岛(外部)类型。

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      autoSchemaFile: true,
      federationMetadata: true,
      buildSchemaOptions: {
        orphanedTypes: [User],
      },
    }),
  ],
  providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

联合示例:网关

网关需要指定端点列表,它将自动发现相应的schema。因此,对于代码优先和模式优先方法,网关服务的实现将保持不变。

import {
  MercuriusGatewayDriver,
  MercuriusGatewayDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusGatewayDriverConfig>({
      driver: MercuriusGatewayDriver,
      gateway: {
        services: [
          { name: 'users', url: 'http://user-service/graphql' },
          { name: 'posts', url: 'http://post-service/graphql' },
        ],
      },
    }),
  ],
})
export class AppModule {}

Federation 2

引述Apollo官方文档,Federation 2相比原始Apollo Federation(在此文档中也叫作 Federation 1)提升了开发者的体验,同时向后兼容大部分原始超图。

警告 ⚠️
Mercurius不完全支持Federation 2。你可以在此处查看支持Federation 2 的库列表。

在接下来的部分中,我们会将之前的示例升级到Federation 2。

联合示例:用户

Federation 2中的一大变化是实体没有了原始子图,所以我们无需再扩展Query。要了解更多细节请前往Apollo Federation 2 文档中的实体话题

模式优先

我们可以从贵schema中简单移除extend关键字。

type User @key(fields: "id") {
  id: ID!
  name: String!
}

type Query {
  getUser(id: ID!): User
}

代码优先

要使用 Federation 2,我们需要在autoSchemaFile选项中指定federation的版本。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: {
        federation: 2,
      },
    }),
  ],
  providers: [UsersResolver, UsersService],
})
export class AppModule {}

联合示例:文章

原因同上,我们无需再扩展UserQuery了。

模式优先

我们可以简单地从schema中移除extendexternal指令。

type Post @key(fields: "id") {
  id: ID!
  title: String!
  body: String!
  user: User
}

type User @key(fields: "id") {
  id: ID!
  posts: [Post]
}

type Query {
  getPosts: [Post]
}

代码优先

由于我们不再扩展User实体,我们可以简单地从User中移除extendsexternal指令了。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field((type) => ID)
  id: number;

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

同样的,与用户服务相似,我们需要在GraphQLModule中指定使用Federation 2。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: {
        federation: 2,
      },
      buildSchemaOptions: {
        orphanedTypes: [User],
      },
    }),
  ],
  providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}