在多个数据源上实现单个 GraphQL

Avatar of Shadid Haque
Shadid Haque

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

在本文中,我们将讨论如何将 模式拼接 应用于多个 Fauna 实例。我们还将讨论如何将其他 GraphQL 服务和数据源与 Fauna 合并到一个图中。

什么是模式拼接?

模式拼接 是从多个基础 GraphQL API 创建单个 GraphQL API 的过程。

它在哪些地方有用?

在构建大型应用程序时,我们通常将各种功能和业务逻辑分解成微服务。它确保了关注点分离。但是,当我们的客户端应用程序需要从多个来源查询数据时,就会出现这种情况。最佳做法是为所有客户端应用程序公开一个统一的图。但是,这可能很困难,因为我们不希望最终得到一个紧密耦合的单体 GraphQL 服务器。如果您使用的是 Fauna,每个数据库都有自己的原生 GraphQL。理想情况下,我们希望尽可能利用 Fauna 的原生 GraphQL,并避免编写应用程序层代码。但是,如果我们使用多个数据库,我们的前端应用程序将不得不连接到多个 GraphQL 实例。这种安排会导致紧密耦合。我们希望避免这种情况,转而使用一个统一的 GraphQL 服务器。

为了解决这些问题,我们可以使用模式拼接。模式拼接将允许我们将多个 GraphQL 服务合并成一个统一的模式。在本文中,我们将讨论

  1. 将多个 Fauna 实例合并成一个 GraphQL 服务
  2. 将 Fauna 与其他 GraphQL API 和数据源合并
  3. 如何使用 AWS Lambda 构建无服务器 GraphQL 网关?

将多个 Fauna 实例合并成一个 GraphQL 服务

首先,让我们看一下如何将多个 Fauna 实例合并成一个 GraphQL 服务。假设我们有三个 Fauna 数据库实例 `Product`、`Inventory` 和 `Review`。每个实例都是独立的。每个实例都有自己的图(我们将它们称为子图)。我们希望创建一个统一的图界面,并将其公开给客户端应用程序。客户端将能够查询下游数据源的任何组合。

我们将调用统一的图来接口我们的网关服务。让我们开始编写此服务。

我们将从一个新的节点项目开始。我们将创建一个新文件夹。然后导航到它并使用以下命令启动一个新的节点应用程序。

mkdir my-gateway 
cd my-gateway
npm init --yes

接下来,我们将创建一个简单的 express GraphQL 服务器。因此,让我们继续使用以下命令安装 `express` 和 `express-graphql` 包。

npm i express express-graphql graphql --save

创建网关服务器

我们将创建一个名为 `gateway.js` 的文件。这是我们应用程序的主要入口点。我们将从创建一个非常简单的 GraphQL 服务器开始。

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema }  = require('graphql');

// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// The root provides a resolver function for each API endpoint
const rootValue = {
    hello: () => 'Hello world!',
};

const app = express();

app.use(
  '/graphql',
  graphqlHTTP((req) => ({
    schema,
    rootValue,
    graphiql: true,
  })),
);

app.listen(4000);
console.log('Running a GraphQL API server at <http://localhost:4000/graphql>');

在上面的代码中,我们创建了一个基本的 `express-graphql` 服务器,它包含一个示例查询和一个解析器。让我们通过运行以下命令测试我们的应用程序。

node gateway.js

导航到 `[<http://localhost:4000/graphql>](<http://localhost:4000/graphql>)`,您将能够与 GraphQL playground 进行交互。

创建 Fauna 实例

接下来,我们将创建三个 Fauna 数据库。它们将充当 GraphQL 服务。让我们前往 fauna.com 并创建我们的数据库。我将它们命名为 `Product`、`Inventory` 和 `Review`

数据库创建完毕后,我们将为它们生成管理密钥。这些密钥是连接到我们的 GraphQL API 所必需的。

让我们创建三个不同的 GraphQL 模式,并将它们上传到各自的数据库。以下是我们模式的结构。

# Schema for Inventory database
type Inventory {
  name: String
  description: String
  sku: Float
  availableLocation: [String]
}
# Schema for Product database
type Product {
  name: String
  description: String
  price: Float
}
# Schema for Review database
type Review {
  email: String
  comment: String
  rating: Float
}

转到相应的数据库,从侧边栏中选择 GraphQL,并导入每个数据库的模式。

现在,我们有三个在 Fauna 上运行的 GraphQL 服务。我们可以继续通过 Fauna 内部的 GraphQL playground 与这些服务进行交互。如果您正在一起学习,请随意输入一些虚拟数据。它在稍后查询多个数据源时会派上用场。

设置网关服务

接下来,我们将使用模式拼接将这些服务合并成一个图。为此,我们需要一个网关服务器。让我们创建一个新的文件 `gateway.js`。我们将使用来自 graphql tools 的一些库来拼接图。

让我们继续在我们的网关服务器上安装这些依赖项。

npm i @graphql-tools/schema @graphql-tools/stitch @graphql-tools/wrap cross-fetch --save

在我们的网关中,我们将创建一个名为 `makeRemoteExecutor` 的新通用函数。此函数是一个工厂函数,它返回另一个函数。返回的异步函数将进行 GraphQL 查询 API 调用。

// gateway.js

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema }  = require('graphql');

 function makeRemoteExecutor(url, token) {
    return async ({ document, variables }) => {
      const query = print(document);
      const fetchResult = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
        body: JSON.stringify({ query, variables }),
      });
      return fetchResult.json();
    }
 }

// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// The root provides a resolver function for each API endpoint
const rootValue = {
    hello: () => 'Hello world!',
};

const app = express();

app.use(
  '/graphql',
  graphqlHTTP(async (req) => {
    return {
      schema,
      rootValue,
      graphiql: true,
    }
  }),
);

app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');

如您在上面看到的,`makeRemoteExecutor` 有两个解析后的参数。`url` 参数指定远程 GraphQL url,`token` 参数指定授权令牌。

我们将创建另一个名为 `makeGatewaySchema` 的函数。在此函数中,我们将使用之前创建的 `makeRemoteExecutor` 函数对远程 GraphQL API 进行代理调用。

// gateway.js

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { introspectSchema } = require('@graphql-tools/wrap');
const { stitchSchemas } = require('@graphql-tools/stitch');
const { fetch } = require('cross-fetch');
const { print } = require('graphql');

function makeRemoteExecutor(url, token) {
  return async ({ document, variables }) => {
    const query = print(document);
    const fetchResult = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
      body: JSON.stringify({ query, variables }),
    });
    return fetchResult.json();
  }
}

async function makeGatewaySchema() {

    const reviewExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQZPUejACQ2xuvfi50APAJ397hlGrTjhdXVta');
    const productExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQbI02HACQwTaUF9iOBbGC3fatQtclCOxZNfp');
    const inventoryExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQbI02HACQwTaUF9iOBbGC3fatQtclCOxZNfp');

    return stitchSchemas({
        subschemas: [
          {
            schema: await introspectSchema(reviewExecutor),
            executor: reviewExecutor,
          },
          {
            schema: await introspectSchema(productExecutor),
            executor: productExecutor
          },
          {
            schema: await introspectSchema(inventoryExecutor),
            executor: inventoryExecutor
          }
        ],
        
        typeDefs: 'type Query { heartbeat: String! }',
        resolvers: {
          Query: {
            heartbeat: () => 'OK'
          }
        }
    });
}

// ...

我们正在使用 `makeRemoteExecutor` 函数来创建我们的远程 GraphQL 执行器。这里我们有三个远程执行器,分别指向 `Product`、`Inventory` 和 `Review` 服务。由于这是一个演示应用程序,我已经将来自 Fauna 的管理员 API 密钥直接硬编码到代码中。**在实际应用程序中避免这样做。这些密钥绝不应该在代码中暴露。** 请使用环境变量或密钥管理器在运行时提取这些值。

如您从上面突出显示的代码中看到的那样,我们正在从 `@graphql-tools` 返回 `switchSchemas` 函数的输出。该函数有一个名为 *subschemas* 的参数属性。在此属性中,我们可以传入我们想要获取和合并的所有子图的数组。我们还使用了一个名为 `introspectSchema` 的函数,该函数来自 `graphql-tools`。此函数负责转换来自网关的请求,并对下游服务进行代理 API 请求。

您可以在 graphql-tools 文档网站 上了解更多关于这些函数的信息。

最后,我们需要调用 `makeGatewaySchema`。我们可以从我们的代码中删除之前硬编码的模式,并用拼接的模式替换它。

// gateway.js

// ...

const app = express();

app.use(
  '/graphql',
  graphqlHTTP(async (req) => {
    const schema = await makeGatewaySchema();
    return {
      schema,
      context: { authHeader: req.headers.authorization },
      graphiql: true,
    }
  }),
);

// ...

当我们重新启动服务器并返回到 `localhost` 时,我们将看到来自所有 Fauna 实例的查询和变异都可以在我们的 GraphQL playground 中使用。

让我们编写一个简单的查询,它将同时从所有 Fauna 实例中获取数据。

拼接第三方 GraphQL API

我们也可以将第三方 GraphQL API 拼接到我们的网关中。在此演示中,我们将把 SpaceX 公开的 GraphQL API 拼接到我们的服务中。

该过程与上面相同。我们创建一个新的执行器,并将其添加到我们的子图数组中。

// ...

async function makeGatewaySchema() {

  const reviewExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQdRZVpACRMEEM1GKKYQxH2Qa4TzLKusTW2gN');
  const productExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQdSdXiACRGmgJgAEgmF_ZfO7iobiXGVP2NzT');
  const inventoryExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQdR0kYACRWKJJUUwWIYoZuD6cJDTvXI0_Y70');

  const spacexExecutor = await makeRemoteExecutor('https://api.spacex.land/graphql/')

  return stitchSchemas({
    subschemas: [
      {
        schema: await introspectSchema(reviewExecutor),
        executor: reviewExecutor,
      },
      {
        schema: await introspectSchema(productExecutor),
        executor: productExecutor
      },
      {
        schema: await introspectSchema(inventoryExecutor),
        executor: inventoryExecutor
      },
      {
        schema: await introspectSchema(spacexExecutor),
        executor: spacexExecutor
      }
    ],
        
    typeDefs: 'type Query { heartbeat: String! }',
    resolvers: {
      Query: {
        heartbeat: () => 'OK'
      }
    }
  });
}

// ...

部署网关

为了使它成为一个真正的无服务器解决方案,我们应该将网关部署到一个无服务器函数中。在这个演示中,我将把网关部署到一个 AWS Lambda 函数中。Netlify 和 Vercel 是 AWS Lambda 的另外两个替代方案。

我将使用无服务器框架将代码部署到 AWS。让我们安装它所需的依赖项。

npm i -g serverless # if you don't have the serverless framework installed already
npm i serverless-http body-parser --save 

接下来,我们需要创建一个名为 serverless.yaml 的配置文件。

# serverless.yaml

service: my-graphql-gateway

provider:
  name: aws
  runtime: nodejs14.x
  stage: dev
  region: us-east-1

functions:
  app:
    handler: gateway.handler
    events:
      - http: ANY /
      - http: 'ANY {proxy+}'

在 serverless.yaml 中,我们定义了云提供商、运行时以及 Lambda 函数的路径等信息。请随意查看 无服务器框架的官方文档 以获取更深入的信息。

在将代码部署到 AWS 之前,我们需要对代码进行一些小的更改。

npm i -g serverless # if you don't have the serverless framework installed already
npm i serverless-http body-parser --save 

注意上面突出显示的代码。我们添加了 body-parser 库来解析 JSON 主体。我们还添加了 serverless-http 库。将 Express 应用程序实例与无服务器函数包装起来将负责所有底层的 Lambda 配置。

我们可以运行以下命令将其部署到 AWS Lambda。

serverless deploy

这将需要一两分钟才能部署。部署完成后,我们将在终端中看到 API URL。

确保你在生成的 URL 末尾添加 /graphql 。(例如 https://gy06ffhe00.execute-api.us-east-1.amazonaws.com/dev/graphql)。

就这样。我们实现了完全的无服务器涅槃 😉。我们现在运行着三个独立的 Fauna 实例,它们通过 GraphQL 网关连接在一起。

请随时查看 本文的代码 。

结论

模式拼接是分解单体并实现数据源之间关注点分离的最流行解决方案之一。但是,还有其他解决方案,例如 Apollo Federation,它们的工作方式几乎相同。如果您想看到一篇关于 Apollo Federation 的类似文章,请在评论区告诉我们。今天就到这里,下次再见。