GraphQL

优质
小牛编辑
131浏览
2023-12-01

快速开始

GraphQL 是一种用于 API 的查询语言。这是 GraphQL 和 REST 之间一个很好的比较 (译者注: GraphQL 替代 REST 是必然趋势)。在这组文章中, 我们不会解释什幺是 GraphQL, 而是演示如何使用 @nestjs/GraphQL 模块。

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

安装

首先,我们需要安装所需的软件包:

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

译者注: fastify 请参考:
https://github.com/coopnd/fastify-apollo

Apollo 中间件

安装软件包后,我们可以应用 apollo-server-express 软件包提供的 GraphQL 中间件 :

app.module.ts

  1. import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
  2. import { graphqlExpress } from 'apollo-server-express';
  3. import { GraphQLModule } from '@nestjs/graphql';
  4. @Module({
  5. imports: [GraphQLModule],
  6. })
  7. export class ApplicationModule implements NestModule {
  8. configure(consumer: MiddlewareConsumer) {
  9. consumer
  10. .apply(graphqlExpress(req => ({ schema: {}, rootValue: req })))
  11. .forRoutes('/graphql');
  12. }
  13. }

就这样。我们传递一个空对象作为 GraphQL schemareq(请求对象)rootValue。此外,还有其他一些可用的 graphqlExpress 选项,你可以在这里阅读它们。

Schema

为了创建 schema, 我们使用的是 GraphQLFactory 它是 @nestjs/graphql 包的一部分。此组件提供了一个createSchema ( )方法,该方法接受与makeexecumbleschema ( )函数相同的对象,这里详细描述。

schema 选项对象至少需要 resolverstypeDefs 属性。您可以手动传递类型定义, 或者使用 GraphQLFactory 的实用程序 mergeTypesByPaths() 方法。让我们看一下下面的示例:

app.module.ts

  1. import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
  2. import { graphqlExpress } from 'apollo-server-express';
  3. import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql';
  4. @Module({
  5. imports: [GraphQLModule],
  6. })
  7. export class ApplicationModule implements NestModule {
  8. constructor(private readonly graphQLFactory: GraphQLFactory) {}
  9. configure(consumer: MiddlewareConsumer) {
  10. const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql');
  11. const schema = this.graphQLFactory.createSchema({ typeDefs });
  12. consumer
  13. .apply(graphqlExpress(req => ({ schema, rootValue: req })))
  14. .forRoutes('/graphql');
  15. }
  16. }

?> 在此处了解关于GraphQL Schema 的更多信息。

在这种情况下,GraphQLFactory 将遍历每个目录,并合并具有 .graphql 扩展名的文件。之后,我们可以使用这些特定的类型定义来创建一个 schemaresolvers 将自动反映出来。

这里, 您可以更多地了解解析器映射的实际内容。

解析器映射

当使用 graphql-tools 时,您必须手动创建解析器映射。以下示例是从Apollo文档复制并粘贴的,您可以在其中阅读更多内容:

  1. import { find, filter } from 'lodash';
  2. // example data
  3. const authors = [
  4. { id: 1, firstName: 'Tom', lastName: 'Coleman' },
  5. { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
  6. { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
  7. ];
  8. const posts = [
  9. { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
  10. { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
  11. { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
  12. { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
  13. ];
  14. const resolverMap = {
  15. Query: {
  16. author(obj, args, context, info) {
  17. return find(authors, { id: args.id });
  18. },
  19. },
  20. Author: {
  21. posts(author, args, context, info) {
  22. return filter(posts, { authorId: author.id });
  23. },
  24. },
  25. };

使用该 @nestjs/graphql 包,解析器映射是使用元数据自动生成的。我们用等效的 Nest-Way 代码重写上面的例子。

  1. import { Query, Resolver, ResolveProperty } from '@nestjs/graphql';
  2. import { find, filter } from 'lodash';
  3. // example data
  4. const authors = [
  5. { id: 1, firstName: 'Tom', lastName: 'Coleman' },
  6. { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
  7. { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
  8. ];
  9. const posts = [
  10. { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
  11. { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
  12. { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
  13. { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
  14. ];
  15. @Resolver('Author')
  16. export class AuthorResolver {
  17. @Query()
  18. author(obj, args, context, info) {
  19. return find(authors, { id: args.id });
  20. }
  21. @ResolveProperty()
  22. posts(author, args, context, info) {
  23. return filter(posts, { authorId: author.id });
  24. }
  25. }

@Resolver() 装饰并不影响查询和变更。它只告诉 Nest 每个 @ResolveProperty() 都有一个父级,在本例中是 Author

?> 如果您使用的是 @Resolver() 修饰器, 则不必将类标记为 @Injectable(), 否则, 它将是必需的。

通常, 我们会使用类似 getAuthor()getPosts() 作为方法名。我们也可以轻松地做到这一点:

  1. import { Query, Resolver, ResolveProperty } from '@nestjs/graphql';
  2. import { find, filter } from 'lodash';
  3. // example data
  4. const authors = [
  5. { id: 1, firstName: 'Tom', lastName: 'Coleman' },
  6. { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
  7. { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
  8. ];
  9. const posts = [
  10. { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
  11. { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
  12. { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
  13. { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
  14. ];
  15. @Resolver('Author')
  16. export class AuthorResolver {
  17. @Query('author')
  18. getAuthor(obj, args, context, info) {
  19. return find(authors, { id: args.id });
  20. }
  21. @ResolveProperty('posts')
  22. getPosts(author, args, context, info) {
  23. return filter(posts, { authorId: author.id });
  24. }
  25. }

?> @Resolver() 装饰可以在方法级别被使用。

重构

上述代码背后的想法是为了展示 Apollo 和 Nest-way 之间的区别,以便简单地转换代码。现在,我们要做一个小的重构来利用 Nest 架构的优势,使其成为一个真实的例子。

  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(obj, args, context, info) {
  9. const { id } = args;
  10. return await this.authorsService.findOneById(id);
  11. }
  12. @ResolveProperty('posts')
  13. async getPosts(author, args, context, info) {
  14. const { id } = author;
  15. return await this.postsService.findAll({ authorId: id });
  16. }
  17. }

现在我们必须在某个地方注册 AuthorResolver, 例如在新创建的 AuthorsModule 中。

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

GraphQLModule 将负责反映「元数据」, 并自动将类转换为正确的解析器映射。你必须做的唯一一件事就是在某个地方导入此模块, 因此 Nest 将知道 AuthorsModule 存在。

类型定义

最后一个缺失的部分是类型定义(阅读更多)文件。让我们在解析器类附近创建它。

author-types.graphql

  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. }

就这样。我们创建了一个 author(id: Int!) 查询。

?> 此处了解有关 GraphQL 查询的更多信息。

变更(Mutations)

在 GraphQL 中,为了修改服务器端数据,我们使用了变更(Mutations)。了解更多

Apollo 官方文献中有一个 upvotePost () 变更的例子。这种变更允许增加后 votes 属性值。

  1. Mutation: {
  2. upvotePost: (_, { postId }) => {
  3. const post = find(posts, { id: postId });
  4. if (!post) {
  5. throw new Error(`Couldn't find post with id ${postId}`);
  6. }
  7. post.votes += 1;
  8. return post;
  9. },
  10. }

为了在 Nest-way 中创建等价的变更,我们将使用 @Mutation() 装饰器。让我们扩展在上一节 (解析器映射) 中使用的 AuthorResolver

  1. import { Query, Mutation, Resolver, ResolveProperty } from '@nestjs/graphql';
  2. import { find, filter } from 'lodash';
  3. // example data
  4. const authors = [
  5. { id: 1, firstName: 'Tom', lastName: 'Coleman' },
  6. { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
  7. { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
  8. ];
  9. const posts = [
  10. { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
  11. { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
  12. { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
  13. { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
  14. ];
  15. @Resolver('Author')
  16. export class AuthorResolver {
  17. @Query('author')
  18. getAuthor(obj, args, context, info) {
  19. return find(authors, { id: args.id });
  20. }
  21. @Mutation()
  22. upvotePost(_, { postId }) {
  23. const post = find(posts, { id: postId });
  24. if (!post) {
  25. throw new Error(`Couldn't find post with id ${postId}`);
  26. }
  27. post.votes += 1;
  28. return post;
  29. }
  30. @ResolveProperty('posts')
  31. getPosts(author) {
  32. return filter(posts, { authorId: author.id });
  33. }
  34. }

重构

我们要做一个小的重构来利用Nest架构的优势,将其变为一个 真实的例子.

  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(obj, args, context, info) {
  9. const { id } = args;
  10. return await this.authorsService.findOneById(id);
  11. }
  12. @Mutation()
  13. async upvotePost(_, { postId }) {
  14. return await this.postsService.upvoteById({ id: postId });
  15. }
  16. @ResolveProperty('posts')
  17. async getPosts(author) {
  18. const { id } = author;
  19. return await this.postsService.findAll({ authorId: id });
  20. }
  21. }

就这样。业务逻辑被转移到了 PostsService

类型定义

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

author-types.graphql

  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 变更后现在可用!

订阅(Subscriptions)

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

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

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

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

为了以Nest方式创建等效订阅,我们将使用 @Subscription() 装饰器。让我们扩展 AuthorResolver 在解析器映射部分中的使用。

  1. import { Query, Resolver, Subscription, ResolveProperty } from '@nestjs/graphql';
  2. import { find, filter } from 'lodash';
  3. import { PubSub } from 'graphql-subscriptions';
  4. // example data
  5. const authors = [
  6. { id: 1, firstName: 'Tom', lastName: 'Coleman' },
  7. { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
  8. { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
  9. ];
  10. const posts = [
  11. { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
  12. { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
  13. { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
  14. { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
  15. ];
  16. // example pubsub
  17. const pubSub = new PubSub();
  18. @Resolver('Author')
  19. export class AuthorResolver {
  20. @Query('author')
  21. getAuthor(obj, args, context, info) {
  22. return find(authors, { id: args.id });
  23. }
  24. @Subscription()
  25. commentAdded() {
  26. return {
  27. subscribe: () => pubSub.asyncIterator('commentAdded'),
  28. };
  29. }
  30. @ResolveProperty('posts')
  31. getPosts(author) {
  32. return filter(posts, { authorId: author.id });
  33. }
  34. }

重构

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

类型定义

最后一步是更新类型定义(阅读更多)文件。

author-types.graphql

  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 订阅。另外,我们应该创建一个发送和订阅事件的 Web sockets 服务器,但为了保持这个示例尽可能简单,我们省略了这部分。尽管如此,你可以在这里找到完整的示例实现。

标量

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

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

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

  1. const resolvers = { JSON: GraphQLJSON };
  2. const schema = this.graphQLFactory.createSchema({ typeDefs, resolvers });

?> GraphQLJSON 是从 graphql-type-json 包中导入的

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

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

看守器和拦截器

在 GraphQL 中, 许多文章抱怨如何处理诸如身份验证或操作的副作用之类的问题。我们应该把它放在业务逻辑里面吗?我们是否应该使用高阶函数来增强查询和变更, 例如使用授权逻辑?没有单一的答案。

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

使用

您可以像在简单的 REST 应用程序中一样使用看守器和拦截器。它们的行为等同于在 graphqlExpress 中间件中作为 rootValue 传递请求。让我们看一下下面的代码:

  1. @Query('author')
  2. @UseGuards(AuthGuard)
  3. async getAuthor(obj, args, context, info) {
  4. const { id } = args;
  5. return await this.authorsService.findOneById(id);
  6. }

由于这一点,您可以将您的身份验证逻辑移至看守器(guard),甚至可以复用与 REST 应用程序中相同的看守器(guard)类。拦截器的工作方式完全相同:

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

写一次,随处使用:)

Schema 拼接

Schema 拼接功能允许从多个底层GraphQL API创建单个GraphQL模式。你可以在这里阅读更多。

代理(Proxying)

要在模式之间添加代理字段的功能,您需要在它们之间创建额外的解析器。我们来看看 Apollo 文档中的例子:

  1. mergeInfo => ({
  2. User: {
  3. chirps: {
  4. fragment: `fragment UserFragment on User { id }`,
  5. resolve(parent, args, context, info) {
  6. const authorId = parent.id;
  7. return mergeInfo.delegate(
  8. 'query',
  9. 'chirpsByAuthorId',
  10. {
  11. authorId,
  12. },
  13. context,
  14. info,
  15. );
  16. },
  17. },
  18. }
  19. })

在这里,我们将 Userchirps 属性委托给另一个 GraphQL API。为了在 Nest-way 中实现相同的结果,我们使用 @DelegateProperty() 装饰器。

  1. @Resolver('User')
  2. @DelegateProperty('chirps')
  3. findChirpsByUserId() {
  4. return (mergeInfo: MergeInfo) => ({
  5. fragment: `fragment UserFragment on User { id }`,
  6. resolve(parent, args, context, info) {
  7. const authorId = parent.id;
  8. return mergeInfo.delegate(
  9. 'query',
  10. 'chirpsByAuthorId',
  11. {
  12. authorId,
  13. },
  14. context,
  15. info,
  16. );
  17. },
  18. });
  19. }

?> @Resolver() 装饰器在这里用于方法级, 但也可以在顶级 (class) 级别使用它。

然后, 让我们再回到 graphqlExpress 中间件。我们需要合并我们的架构并在它们之间添加委托。要创建委托, 我们使用 GraphQLFactory 类的 createDelegates () 方法。

app.module.ts

  1. configure(consumer) {
  2. const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql');
  3. const localSchema = this.graphQLFactory.createSchema({ typeDefs });
  4. const delegates = this.graphQLFactory.createDelegates();
  5. const schema = mergeSchemas({
  6. schemas: [localSchema, chirpSchema, linkTypeDefs],
  7. resolvers: delegates,
  8. });
  9. consumer
  10. .apply(graphqlExpress(req => ({ schema, rootValue: req })))
  11. .forRoutes('/graphql');
  12. }

为了合并 schema ,我们使用了 mergeSchemas() 函数(阅读更多)。此外,您可能会注意到 chirpsSchemalinkTypeDefs 变量。他们是直接从 Apollo 文档复制和粘贴的。

  1. import { makeExecutableSchema } from 'graphql-tools';
  2. const chirpSchema = makeExecutableSchema({
  3. typeDefs: `
  4. type Chirp {
  5. id: ID!
  6. text: String
  7. authorId: ID!
  8. }
  9. type Query {
  10. chirpById(id: ID!): Chirp
  11. chirpsByAuthorId(authorId: ID!): [Chirp]
  12. }
  13. `
  14. });
  15. const linkTypeDefs = `
  16. extend type User {
  17. chirps: [Chirp]
  18. }
  19. extend type Chirp {
  20. author: User
  21. }
  22. `;

就这样。

IDE

最受欢迎的 GraphQL 浏览器 IDE 称为 GraphiQL。要在您的应用程序中使用 GraphiQL,您需要设置一个中间件。这个特殊的中间件附带了 apollo-server-express ,我们必须安装。它的名字是 graphiqlExpress()

为了建立一个中间件,我们需要再次打开一个 app.module.ts 文件:

app.module.ts

  1. import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
  2. import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
  3. import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql';
  4. @Module({
  5. imports: [GraphQLModule],
  6. })
  7. export class ApplicationModule implements NestModule {
  8. constructor(private readonly graphQLFactory: GraphQLFactory) {}
  9. configure(consumer: MiddlewareConsumer) {
  10. const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql');
  11. const schema = this.graphQLFactory.createSchema({ typeDefs });
  12. consumer
  13. .apply(graphiqlExpress({ endpointURL: '/graphql' }))
  14. .forRoutes('/graphiql')
  15. .apply(graphqlExpress(req => ({ schema, rootValue: req })))
  16. .forRoutes('/graphql');
  17. }
  18. }

?> graphiqlExpress() 提供了一些其他选项, 请在此处阅读更多信息。

现在,当你打开 http://localhost:PORT/graphiql 你应该看到一个图形交互式 GraphiQL IDE。