将两个相邻的GraphQL API(AppSync/Github)合并成一个API
GraphQL的特征性关联模式 “Union”
我之前在一篇文章中创建了一个调用AppSync的应用程序,并在上一篇文章中将AppSync与现有的GraphQL API进行了整合,使它们成为一个API。我现在发现了另一个GraphQL整合的案例!(最后我会解释,但实际上这个发现并不算是全新的概念。)
这可以被称为联合类型(由我起的名字,后面将解释其意义)。
与前文所述有关API的父子关系的集成不同,使用这个Union类型可以进行全面的数据处理,这是令人高兴的。
这次我也将Github API v4和AppSync的API进行了结合,验证了这个令人高兴的功能。
联盟型的使用案例
假设有以下两个GraphQL API。
-
- AppSync上的GraphQL API:可以获取公司内私有组织名称列表的GraphQL API。
- Github的API:可以获取公众所知的组织名称列表的GraphQL API。
例如,「GitHub」这个公司的组织名称属于2.,而「●●株式会社総務部」这个组织名称属于1.,相同的名称没有出现在2.中。
实际上,GitHub GraphQL API的最上层(称为根字段)具有一个名为organization的查询,您可以指定类似”github”或”facebook”这样的知名组织名称来获取组织信息。
现在,分别保存着两个不同的组织名称。(1)(2)虽然只是范围不同,但本质上都是存储”组织名称”,因此可以被视为同级别的API。
然后,我们将这些相同API通过Union合并起来,创建一个能够全面处理(1)(2)的API。
让我举出两个Union的使用案例。
获取两个API的并集的具体用例。
创建一个名为listUnionedOrganizations的API,返回(1)和(2)的组织名称列表的并集。简言之,返回值是(1)的组织名称列表加上(2)的组织名称列表的总和。
具體的用例2. 從兩個API中只搜尋一個元素
创建一个名为getOneUnionedOrganization的API,它只返回(1)和(2)中的一个组织。
可以使用某个ID来识别一个组织并将其作为返回值,也可以通过类似名称的搜索来实现。
返回(1)或(2)中的哪个组织的名称取决于具体情况。
架构
如果要合并同一列的API,架构大致如下。
通过启动一个服务器,从AppSync和Github的API中提取数据,并将它们合并成一个新的GraphQL API。(与上一次一样,使用了合并GraphQL的机制 “Schema Stitching”,请参阅另一篇文章)。
此次将使用union这种类型定义方法(稍后详述),除了使用Schema Stitching。
创建一个合并了同类API的类型——Union。
在GraphQL中,数据具有类型,并且前面提到的两个API也各自具有类型。
使用GraphQL中的union类型定义方式,可以将多个类型结合在一起。
比如说,假设有以下类型。
union UnionedOrganization = MyOrganization | Organization
type Query {
getUnionedOrganization: UnionedOrganization
}
1行中使用union定义了一个类型,可以存储多个类型的实例作为其组合。
例如,通过在第一行中定义的UnionedOrganization,可以存储MyOrganization和Organization的任何一个值。
在第二行中定义了一个名为getUnionedOrganization的字段(函数),它返回UnionedOrganization的实例。但实际获取到的值可能是MyOrganiztion,也可能是Organization。
因此,雖然前言很長,但現在我們來看一下Union型的結構。
这次也要在AWS Lambda上启动。
构建各种API
1. 建立AppSync API端点
在AppSync上创建的API“私人组织列表”的模型如下。
type MyOrganizations {
id: ID!
name: String
}
type Query {
listMyOrganizations: MyOrganizationConnection
getMyOrganization: MyOrganization
}
以下是特征。
-
- データの型はMyOrganizationで、idとnameの2つのフィールドしかない。
- クエリは、一覧を取得するlistMyOrganizationsと、単体を取得するgetMyOrganizationという2つのAPIを持つ。
在AppSync控制台上构建模型的步骤如下。
使用AppSync的查询和直接操作DynamoDB,在事先注册了一些数据。
我做了如下操作。
API访问的准备工作
与上一次相同,从创建的API的概要页面下载aws-exports.js文件。
2. GitHub的API
这也和上次一样,Github API v4的模型之一是如下的Organization。
通过一个名为login的字符串参数,您可以获取到一个Organization作为返回值。
Organization被定义为具有一个字段repositories,可以获取该组织的仓库。
type Organization {
id
location
name
repositories
}
type Query {
organization(login: String!): Organization
}
以下是特征。
-
- データの型はOrganizationで、idとname、location、repositoriesという4つのフィールドを持っていて、前述のMyOrganizationより多いフィールドを持つ。
- クエリは、一覧を取得するAPIはなく、単体を取得するorganizationという1つのAPIを持つ。
你可以在Github API Explorer上查看模式。
准备API访问
请事先通过github的设置 -> 开发者设置 -> 个人访问令牌获取访问令牌。
将获取的令牌写入.env文件中。
GITHUB_ACCESS_TOKEN=<token>
用原生的中文重新解释如下:
联合API的规范和在AWS Lambda上使用Node.js实现的服务器。
作为准备工作,将刚才从AppSync下载的aws-exports.js和.env文件存储在下面提到的服务器实现文件的同一文件夹中。
在服务器实现中,将AppSync API的MyOrganization和Github API的organization的结果进行合并。
以下是合并API的返回值和字段名。
union UnionedOrganization = MyOrganization | Organization
type Query {
listUnionedOrganizations: [UnionedOrganization]
getUnionedOrganization: UnionedOrganization
}
以下是它的特点 shì tā de
-
- リターン値UnionedOrganizationは、2つの型のunion。
- クエリは、一覧を取得するlistUnionedOrganizationsと、単体を取得するgetUnionedOrganizationの2つのAPIを持つ。
API的返回值可以按照以下方式构建。
-
- 1) AppSyncから値をコピーする
-
- 2) Github APIから値をコピーする。
- 3) organizationは1)と2)を合わせて(必要に応じてフィルターして)返す
那么,关于结合API实现的Node.js服务器,可以按照以下方式进行实现。
import {makeRemoteExecutableSchema, mergeSchemas, introspectSchema, makeExecutableSchema } from 'graphql-tools';
import fetch from 'node-fetch';
import { HttpLink } from 'apollo-link-http';
import { ApolloServer, gql } from "apollo-server-express";
import serverless from "serverless-http";
import express from "express";
import { config } from 'dotenv';
import { graphql } from "graphql";
config()
const aws_exports = require('./aws-exports-2').default;
const github_url = 'https://api.github.com/graphql';
const createSchema = async () => {
const createRemoteSchema = async (uri, headers) => {
const link = new HttpLink({uri, fetch, headers});
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link
});
};
const appsyncSchema = await createRemoteSchema(
aws_exports.aws_appsync_graphqlEndpoint,
{'X-Api-Key': aws_exports.aws_appsync_apiKey}
);
const githubSchema = await createRemoteSchema(
github_url,
{ Authorization: `bearer ${process.env.GITHUB_ACCESS_TOKEN}`}
);
const linkSchemaDefs = gql`
union UnionedOrganization = MyOrganization | Organization
extend type Query {
getOneUnionedOrganization(name: String, type: String): [UnionedOrganization]
listUnionedOrganizations: [UnionedOrganization]
}
`;
const schema = mergeSchemas({
schemas:[githubSchema, appsyncSchema, linkSchemaDefs],
resolvers: {
Query: {
getOneUnionedOrganization: {
async resolve(parent, args, context, info) {
const delegate = (schema, fieldName, args) => {
const operation = 'query'
const _paramsForDelegate = { schema, operation, fieldName, args, context, info }
return info.mergeInfo.delegateToSchema(_paramsForDelegate)
}
if(args.type === "github") {
return delegate(githubSchema, 'organization', {login: args.name})
}
return delegate(appsyncSchema, 'getMyOrganizationsByName', {name: args.name})
.then(a => (a.length > 0) ? a[0] : null); }
},
listUnionedOrganizations: {
async resolve(parent, args, context, info) {
const delegate = (schema, fieldName, args) => {
const operation = 'query'
const _paramsForDelegate = { schema, operation, fieldName, args, context, info }
return info.mergeInfo.delegateToSchema(_paramsForDelegate)
}
const githubResponse = delegate(githubSchema, 'organization', {login: 'serverless'})
.then(a => [a])
const appsyncResponse = graphql(appsyncSchema, `{ listMyOrganizations { items{__typename, id, name}}}`)
.then(a => a.data.listMyOrganizations.items)
return Promise.all([githubResponse, appsyncResponse]).then(arr => arr.flat())
}
}
},
}
});
return schema
}
const createServer = (schema) => {
const app = express();
const server = new ApolloServer({ schema });
server.applyMiddleware({ app });
return serverless(app);
};
let schema
let sls
exports.graphqlHandler = async (event, context) => {
if(sls == null) {
schema = await createSchema();
sls = createServer(schema);
} else {
console.log("Already initialized")
}
return await sls(event, context);
}
逐步解釋
我在下面的函数中封装了从Github和AppSync获取数据的处理过程。
const delegate = (schema, fieldName, args) => {
const operation = 'query'
const _paramsForDelegate = { schema, operation, fieldName, args, context, info }
return info.mergeInfo.delegateToSchema(_paramsForDelegate)
}
由于在listUnionedOrganizations中,Github没有提供获取列表的API,因此我们使用单个获取API作为替代,并将其放入数组中(数组中只包含一个元素),然后将其与AppSync的结果连接在一起。
const githubResponse = delegate(githubSchema, 'organization', {login: 'serverless'})
.then(a => [a])
const appsyncResponse = graphql(appsyncSchema, `{ listMyOrganizations { items{__typename, id, name}}}`)
.then(a => a.data.listMyOrganizations.items)
在这里的要点是,由于AppSync的特性,返回值会变得稍微复杂,在委托中无法正确地执行查询并获取结果,因此无法直接合并。(确切地说,似乎有一些解决方法,但我还没有理解)
因此,由于委托本身并不方便使用,我稍微作了手脚,直接在graphql函数中编写查询。
最终将这些结果合并。arr是一个二维数组,如[[<GitHub的结果>], [<AppSync的结果>]] ,使用flat()函数可以将其转换成一维数组,然后进行合并。
Promise.all([githubResponse, appsyncResponse]).then(arr => arr.flat())
部署
为了使用serverless在AWS Lambda上部署,我们要按照下方的样式准备serverless.yml。
service: github-appsync
provider:
name: aws
runtime: nodejs8.10
functions:
graphql:
# this is formatted as <FILENAME>.<HANDLER>
handler: index.graphqlHandler
events:
- http:
path: graphql
method: post
cors: true
plugins:
- serverless-webpack
- serverless-offline
custom:
webpack:
webpackConfig: ./webpack.config.js
includeModules: true
我已经准备了webpack.config.js文件以便在webpack中进行构建。
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals");
module.exports = {
entry: slsw.lib.entries,
target: "node",
devtool: 'source-map',
externals: [nodeExternals()],
mode: slsw.lib.webpack.isLocal ? "development" : "production",
optimization: {
minimize: false
},
performance: {
hints: false
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
include: __dirname,
exclude: /node_modules/
}
]
}
};
在上述环境中,通过以下命令将部署到AWS Lambda。
sls deploy
验证结果
作为验证方法,同时执行以下两个查询(列表获取和单个获取)。
query simple($name: String = "HumanResourceDept", $type: String = "appsync") {
getOneUnionedOrganization(name: $name, type: $type) {
...any
}
listUnionedOrganizations {
...any
}
}
fragment any on UnionedOrganization {
__typename
... on Organization {
... git
}
... on MyOrganization {
id
name
}
}
fragment git on Organization {
id
location
name
repositories(first: 3) {
nodes {
name
stargazers {
totalCount
}
}
}
}
您需要在参数中输入预先在AppSync中注册的组织名称。
{
"name": "ProcurementDept",
"type": "appsync"
}
经过验证,结果如下。
首先,listUnionedOrganizations中合并了一个Github API的结果和四个AppSync的结果。
此外,在getOneUnionedOrganization中,取出了一个具有指定值为ProcurementDept的$name的组织。
{
"data": {
"listUnionedOrganizations": [
{
"__typename": "Organization",
"id": "MDEyOk9yZ2FuaXphdGlvbjEzNzQyNDE1",
"location": "San Francisco, CA",
"name": "Serverless",
"repositories": {
"nodes": [
{
"name": "serverless",
"stargazers": {
"totalCount": 27890
}
},
{
"name": "serverless-helpers-js",
"stargazers": {
"totalCount": 11
}
},
{
"name": "serverless-plugin-boilerplate",
"stargazers": {
"totalCount": 24
}
}
]
}
},
{
"__typename": "MyOrganization",
"id": "id3",
"name": "SiteReliabilityEngineeringDept"
},
{
"__typename": "MyOrganization",
"id": "id1",
"name": "HumanResourceDept"
},
{
"__typename": "MyOrganization",
"id": "id4",
"name": "ServiceDevelopmentDept"
},
{
"__typename": "MyOrganization",
"id": "id2",
"name": "ProcurementDept"
}
],
"getOneUnionedOrganization": {
"__typename": "MyOrganization",
"id": "id2",
"name": "ProcurementDept"
}
}
}
在离线环境下的执行结果
总结
我将Github API和自定义的AppSync API通过使用union进行同级联接的场景进行了实施。
结果是,我能够在Lambda上运行联接后的API。
虽然解析器的编写方式需要一些巧妙处理,但只要能够很好地进行合并处理,就可以在一个API中进行多个数据源的搜索了。
同一级别的连接与父子关系的连接
对同一表的连接处理进行简单解析并与上一篇文章中的连接进行比较。我们将连接两个API的方法命名为“Join”,使用前一篇文章中的父子关系,将连接同一表的API命名为“Union”。
下图展示了Join类型和Union类型所生成数据的差异。
Join型和Union型的行为差异类似于SQL中的JOIN和UNION。
加入:通过横向连接表,增加列数的效果。
联合:通过纵向连接表,增加行数的效果。
我们也可以在GraphQL中以同样的方式思考。
Join型: 将多个API的返回值进行父子关系的合并,增加层次效果
Union型: 将多个API的返回值作为同一列进行合并,增加数组元素数量的效果(能够进行全面处理)
通过灵活地组合Join型和Union型,数据科学家可以获得所需的整理数据,不是吗?