• Apollo GraphQL


    介绍

    Apollo 是一个开源的 GraphQL 开发平台, 提供了符合 GraphQL 规范的服务端和客户端实现。使用 Apollo 可以帮助我们更方便快捷的开发使用 GraphQL。
    在这里插入图片描述

    基本用法

    1、准备

    # 创建项目目录
    mkdir graphql-server-example
    cd graphql-server-example
    
    # 初始化 package.json 文件
    npm init -y
    
    # 安装依赖
    npm install apollo-server graphql
    
    # 创建 index.js
    touch index.js
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2、index.js

    const { ApolloServer, gql } = require('apollo-server')
    
    const typeDefs = gql`
      # Comments in GraphQL strings (such as this one) start with the hash (#) symbol.
    
      # This "Book" type defines the queryable fields for every book in our data source.
      type Book {
        title: String
        author: String
      }
    
      # The "Query" type is special: it lists all of the available queries that
      # clients can execute, along with the return type for each. In this
      # case, the "books" query returns an array of zero or more Books (defined above).
      type Query {
        books: [Book]
      }
    `
    
    const books = [
      {
        title: 'The Awakening',
        author: 'Kate Chopin'
      },
      {
        title: 'City of Glass',
        author: 'Paul Auster'
      }
    ]
    
    const resolvers = {
      Query: {
        books: () => books
      }
    }
    
    // The ApolloServer constructor requires two parameters: your schema
    // definition and your set of resolvers.
    const server = new ApolloServer({ typeDefs, resolvers })
    
    // The `listen` method launches a web server.
    server.listen().then(({ url }) => {
      console.log(`🚀  Server ready at ${url}`)
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    关于 gql 标签模板

    • gql 标签模板将查询字符串解析为 AST,用于使用 Prettier 突出显示编辑器语法和自动格式化。
    • 将来,我们将使用它来静态分析 GraphQL 查询,因此 Apollo Server 需要使用 gql 包装您的 schema。
    • gql 也可以用作将字符串转换为AST的常规函数

    与 Node.js 中间件集成

    Apollo Server 可以轻松地与几个流行的 Node.js 中间件库集成。要集成,请首先从下表中安装适当的软件包,而不是核心 apollo-server 软件包。

    与中间件集成时,首先像往常一样初始化 Apollo Server,然后调用 applyMiddleware。

    这是为 Hello 服务的基本 Express 示例。从 /graphql 以外的每个路径访问,该路径与 Apollo Server 一起提供GraphQL端点:

    const express = require('express');
    const { ApolloServer } = require('apollo-server-express');
    const { typeDefs, resolvers } = require('./schema');
    
    const app = express();
    const server = new ApolloServer({
      typeDefs,
      resolvers,
    });
    
    server.applyMiddleware({ app });
    
    app.use((req, res) => {
      res.status(200);
      res.send('Hello!');
      res.end();
    });
    
    app.listen({ port: 4000 }, () =>
      console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
    )
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    提供给 applyMiddleware 的参数是中间件对应用程序的顶级表示。在 Express 应用程序中,此变量通常称为app。

    当您将应用程序传递给 applyMiddleware 时,Apollo Server 会自动配置各种中间件(包括请求体解析,GraphQL Playground 前端和 CORS 支持),因此您不需要通过诸如 app.use 之类的机制来应用它们。

    解析器

    基本用法

    schema:

    type Query {
      numberSix: Int! # Should always return the number 6 when queried
      numberSeven: Int! # Should always return 7
    }
    
    • 1
    • 2
    • 3
    • 4

    resolver:

    const resolvers = {
      Query: {
        numberSix() {
          return 6;
        },
        numberSeven() {
          return 7;
        }
      }
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    处理参数

    schema:

    type User {
      id: ID!
      name: String
    }
    
    type Query {
      user(id: ID!): User
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    resolver:

    const users = [
      {
        id: '1',
        name: 'Elizabeth Bennet'
      },
      {
        id: '2',
        name: 'Fitzwilliam Darcy'
      }
    ];
    
    const resolvers = {
      Query: {
        user(parent, args, context, info) {
          return users.find(user => user.id === args.id);
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    解析器链

    # A library has a branch and books
    type Library {
      branch: String!
      books: [Book!]
    }
    
    # A book has a title and author
    type Book {
      title: String!
      author: Author!
    }
    
    # An author has a name
    type Author {
      name: String!
    }
    
    type Query {
      libraries: [Library]
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    有效的查询语法:

    query GetBooksByLibrary {
      libraries {
        books {
          author {
            name
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    此查询的结果解析器链与查询本身的层次结构匹配,这些解析器按上述顺序执行,并将其返回值通过父参数传递给链中的下一个解析器。

    resolver 处理如下:

    const { ApolloServer, gql } = require('apollo-server');
    
    const libraries = [
      {
        branch: 'downtown'
      },
      {
        branch: 'riverside'
      },
    ];
    
    // The branch field of a book indicates which library has it in stock
    const books = [
      {
        title: 'The Awakening',
        author: 'Kate Chopin',
        branch: 'riverside'
      },
      {
        title: 'City of Glass',
        author: 'Paul Auster',
        branch: 'downtown'
      },
    ];
    
    // Schema definition
    const typeDefs = gql`
    
    # A library has a branch and books
      type Library {
        branch: String!
        books: [Book!]
      }
    
      # A book has a title and author
      type Book {
        title: String!
        author: Author!
      }
    
      # An author has a name
      type Author {
        name: String!
      }
    
      # Queries can fetch a list of libraries
      type Query {
        libraries: [Library]
      }
    `;
    
    // Resolver map
    const resolvers = {
      Query: {
        libraries() {
    
          // Return our hardcoded array of libraries
          return libraries;
        }
      },
      Library: {
        books(parent) {
    
          // Filter the hardcoded array of books to only include
          // books that are located at the correct branch
          return books.filter(book => book.branch === parent.branch);
        }
      },
      Book: {
    
        // The parent resolver (Library.books) returns an object with the
        // author's name in the "author" field. Return a JSON object containing
        // the name, because this field expects an object.
        author(parent) {
          return {
            name: parent.author
          };
        }
      }
    
      // Because Book.author returns an object with a "name" field,
      // Apollo Server's default resolver for Author.name will work.
      // We don't need to define one.
    };
    
    // Pass schema definition and resolvers to the
    // ApolloServer constructor
    const server = new ApolloServer({ typeDefs, resolvers });
    
    // Launch the server
    server.listen().then(({ url }) => {
      console.log(`🚀  Server ready at ${url}`);
    });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    解析器参数

    解析程序函数按顺序接受以下位置参数:
    在这里插入图片描述

    context 参数

    context 参数对于传递任何解析器可能需要的东西很有用,例如身份验证范围,数据库连接和自定义获取函数。如果您正在使用数据加载器跨解析器批处理请求,则也可以将它们附加到上下文。

    解析器决不应破坏性地修改上下文参数。这样可以确保所有解析程序的一致性,并防止意外错误。

    要为您的解析器提供上下文,请向 ApolloServer 构造函数添加上下文初始化函数。每个请求都会调用此函数,因此您可以根据请求的详细信息(例如HTTP标头)设置上下文。

    
    // Constructor
    const server = new ApolloServer({
      typeDefs,
      resolvers,
      context: (req) => ({
        authScope: getScope(req.headers.authorization)
      })
    }));
    
    // Example resolver
    (parent, args, context, info) => {
      if(context.authScope !== ADMIN) throw new AuthenticationError('not admin');
      // Proceed
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果您使用的是 Express 以外的中间件,则传递给上下文函数的对象的字段会有所不同。有关详细信息,请参见API参考。

    上下文初始化可以是异步的,从而可以完成数据库连接和其他操作:

    context: async () => ({
      db: await client.connect(),
    })
    
    // Resolver
    (parent, args, context, info) => {
      return context.db.query('SELECT * FROM table_name');
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    返回值

    Apollo Server根据其类型对解析器函数的返回值进行不同的处理:
    在这里插入图片描述

    默认解析器

    如果您没有为特定的架构字段定义解析器,则Apollo Server会为其定义默认解析器(请参阅源代码)。

    默认的解析器功能使用以下逻辑:
    在这里插入图片描述
    例如,请考虑以下 schema:

    type Book {
      title: String
    }
    
    type Author {
      books: [Book]
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果 books 字段的解析程序返回一个对象数组,每个对象包含一个 title 字段,则可以为 title 字段使用默认的解析程序。默认解析器将正确返回 parent.title。

    监控解析器性能

    与所有代码一样,解析器的性能取决于其逻辑。重要的是要了解模式中的哪些字段在计算上是昂贵的或难以解决的,以便您可以提高其性能或确保仅在必要时查询它们。

    Apollo Studio 直接与 Apollo Server 集成在一起,以提供字段级指标,以帮助您了解一段时间内数据图的性能。有关更多信息,请参阅分析性能。

    在本地进行开发时,您可以为 ApolloServer 构造函数提供 tracing: true,以启用跟踪面板:

    const server = new ApolloServer({
      typeDefs,
      resolvers,
      tracing: true,
    });
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    连接到数据源

    Apollo Server 可以从多给位置获取数据。

    Apollo提供了一个 DataSource 类,我们可以扩展该类以处理特定类型的数据源的交互逻辑。

    从解析器访问数据

    为了使解析器能够访问数据源,可以将它们作为选项传递给 ApolloServer 构造函数:

    const server = new ApolloServer({
      typeDefs,
      resolvers,
      dataSources: () => {
        return {
          moviesAPI: new MoviesAPI(),
          personalizationAPI: new PersonalizationAPI(),
        };
      },
      context: () => {
        return {
          token: 'foo',
        };
      },
    });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Apollo Server 会将每个请求的数据源放在上下文中,因此您可以从解析器访问它们。它还将使您的数据源可以访问上下文。 (之所以没有让用户直接将数据源放在上下文中,是因为这会导致循环依赖。)
    从解析器中,我们可以访问数据源并返回结果:

     Query: {
        movie: async (_source, { id }, { dataSources }) => {
          return dataSources.moviesAPI.getMovie(id);
        },
        mostViewedMovies: async (_source, _args, { dataSources }) => {
          return dataSources.moviesAPI.getMostViewedMovies();
        },
        favorites: async (_source, _args, { dataSources }) => {
          return dataSources.personalizationAPI.getFavorites();
        },
      },
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    错误处理

    Apollo 服务器提供了一组预定义的错误,包括 AuthenticationError,ForbiddenError,UserInputError 和常规 ApolloError。这些错误旨在增强 GraphQL 执行之前和执行过程中引发的错误,从而使调试 Apollo Server 集成更加容易,并使客户端能够基于错误采取特定的措施。

    当解析器内部和外部的 Apollo Server 中都发生错误时,errors 数组内部的每个错误都包含一个扩展对象,该对象包含由 Apollo Server 添加的信息。

    改善服务器可用性的第一步是默认情况下提供错误堆栈跟踪。下面的示例演示了从Apollo Server返回的响应,该响应带有引发节点 SystemError 的解析器。

    默认信息

    改善服务器可用性的第一步是默认情况下提供错误堆栈跟踪。下面的示例演示了从Apollo Server返回的响应,该响应带有引发节点 SystemError 的解析器。

    const {
      ApolloServer,
      gql,
    } = require('apollo-server');
    
    const typeDefs = gql`
      type Query {
        readError: String
      }
    `;
    
    const resolvers = {
      Query: {
    
        readError: (parent, args, context) => {
          fs.readFileSync('/does/not/exist');
        },
      },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    错误代号

    除了堆栈跟踪外,Apollo Server 导出的错误还在扩展的代码字段中指定了人类可读的字符串,该字符串使客户端能够执行纠正措施。除了改善客户体验之外,代码字段还允许服务器对错误进行分类。例如,AuthenticationError 将代码设置为 UNAUTHENTICATED,这使客户端可以重新进行身份验证,通常会被视为服务器异常而被忽略。

    const {
      ApolloServer,
      gql,
    
      AuthenticationError,
    } = require('apollo-server');
    
    const typeDefs = gql`
      type Query {
        authenticationError: String
      }
    `;
    
    const resolvers = {
      Query: {
    
        authenticationError: (parent, args, context) => {
          throw new AuthenticationError('must authenticate');
        },
      },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    扩大错误的详细信息

    当客户端提供错误的输入时,您可能希望针对无效的每个字段或参数返回其他信息,例如本地化消息。下面的示例演示如何使用 UserInputError 来添加更多详细信息来增加错误消息。

    const {
      ApolloServer,
      UserInputError,
      gql,
    } = require('apollo-server');
    
    const typeDefs = gql`
      type Mutation {
        userInputError(input: String): String
      }
    `;
    
    const resolvers = {
      Mutation: {
    
        userInputError: (parent, args, context, info) => {
          if (args.input !== 'expected') {
            throw new UserInputError('Form Arguments invalid', {
              invalidArgs: Object.keys(args),
            });
          }
        },
      },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    其它错误

    如果需要定义特定于您的应用程序的其他错误代码,则可以使用 ApolloError 基类。

    new ApolloError(message, code, additionalProperties);
    
    
    • 1
    • 2

    掩盖和记录错误

    对于客户端响应

    Apollo Server 构造函数接受一个 formatError 函数,该函数在传递回客户端的每个错误上运行。这可用于掩盖错误以及记录日志。
    请注意,虽然这会更改发送到客户端的错误,但不会更改发送到 Apollo Studio 的错误。如果需要此行为,请参见下面的 For Apollo Studio 报告中的 rewriteError 函数。
    此示例演示当错误消息以 Database Error: 开头时抛出另一个错误:

    const server = new ApolloServer({
      typeDefs,
      resolvers,
    
      formatError: (err) => {
        // Don't give the specific errors to the client.
        if (err.message.startsWith("Database Error: ")) {
          return new Error('Internal server error');
        }
        // Otherwise return the original error.  The error can also
        // be manipulated in other ways, so long as it's returned.
        return err;
      },
    });
    
    server.listen().then(({ url }) => {
      console.log(`🚀 Server ready at ${url}`);
    });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    formatError(一个GraphQLError)接收到的错误实例包含一个 originalError 属性,该属性表示在解析器中引发的原始错误。这可以用于 instanceof 检查特定的错误类,例如 AuthenticationError,ValidationError 等:

      /* ... */
      formatError(err) {
        if (err.originalError instanceof AuthenticationError) {
          return new Error('Different authentication error message!');
        }
      },
      /* ... */
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    要针对 formatError 接收到的错误进行特定于上下文的调整(例如,本地化或个性化设置),请考虑使用 didEncounterErrors 生命周期挂钩将其他属性附加到错误,可以在 formatError 中访问和利用附加属性。

    权限认证

    如何在 GraphQL API 中授权用户和权限控制。

    在构建GraphQL端点时的某个时间点(可能很早),您可能不得不面对一个问题,即如何控制谁可以看到您的API中的数据并与之交互。

    身份验证是确定用户是否已登录,然后确定某人是哪个用户。然后,授权将决定用户有权执行或查看的内容。

    一旦知道用户尝试发出请求,本文将主要侧重于如何为您的 schema 设置授权,但是我们将通过一个身份验证示例来了解我们正在做的事情的背景。

    将用户信息放到 context 中

    在弄清楚用户权限之前,我们必须先弄清楚如何识别用户。从 HTTP 标头到 JSON Web Token,有多种方法可以处理用户身份验证,但是一旦拥有用户,控制访问就非常相似。
    我们将以 HTTP 授权标头中的登录令牌为例。

    // using apollo-server 2.x
    const { ApolloServer } = require('apollo-server');
    
    const server = new ApolloServer({
     typeDefs,
     resolvers,
     context: ({ req }) => {
       // Note! This example uses the `req` object to access headers,
       // but the arguments received by `context` vary by integration.
       // This means they will vary for Express, Koa, Lambda, etc.!
       //
       // To find out the correct arguments for a specific integration,
       // see the `context` option in the API reference for `apollo-server`:
       // https://www.apollographql.com/docs/apollo-server/api/apollo-server/
    
       // Get the user token from the headers.
       const token = req.headers.authorization || '';
    
       // try to retrieve a user with the token
       const user = getUser(token);
    
       // add the user to the context
       return { user };
     },
    });
    
    server.listen().then(({ url }) => {
     console.log(`🚀 Server ready at ${url}`)
    });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    那么,这里到底发生了什么?此代码块使用 Apollo Server 2.0 设置新的 GraphQL 服务器。新版本的 Apollo Server 简化了用于创建新服务器的 API,并具有一些更智能的默认值。

    在此构造函数中,我们将类型定义和解析器传递给构造函数以及用于构建上下文对象的函数。上下文对象是传递给每个级别的每个解析器的对象,因此我们可以在 schema 代码中的任何位置访问它。在这里我们可以存储数据提取程序,数据库连接以及(方便地)有关发出请求的用户的信息。

    由于每次新请求都会重新生成上下文,因此我们不必担心在执行结束时清理用户数据。

    这里的上下文函数查看请求标头,提取名为 authorization 的标头,并将其存储到变量中。然后,它将使用该令牌调用 getUser 函数,并期望在令牌有效的情况下返回用户。之后,它返回一个包含(潜在)用户的上下文对象,供我们所有的解析器使用。

    对于每种身份验证方法,检索用户的细节将有所不同,但是最终部分每次都会看起来相同。您的 schema 需要的授权可能要求您仅在上下文中添加 { loggingIn: true },但可能还需要 ID 或角色,例如 {user: {id: 12345,role: [‘user’, ‘admin’ ]}}。

    在下一节中,我们将探讨使用现在需要保护用户信息的用户信息的方法。

  • 相关阅读:
    java中的volatile
    Vue2.0+AntvX6—边 Edge
    Python运算符 成员运算符、身份运算符,三目运算符
    马斯克:虽然我是Rust的粉丝,但我选择C,其次是C++和Python
    C# OpenCvSharp 图片模糊检测(拉普拉斯算子)
    cmd命令跳转至指定目录
    1732. 找到最高海拔
    【全志T113-S3_100ask】1-编译buildroot初体验
    软件测试 —— 移动端测试
    bgp链路聚合配置与详解
  • 原文地址:https://blog.csdn.net/qq_42308316/article/details/128164284