目的
前回記事でAppSyncによりAPIを簡単に作りましたが、今度は既存のGraphQL APIと結合させ一つのAPIにしたいです。
既存APIとして今回はGithub API v4を取り上げ、AppSyncのAPIと結合させてみます。
APIの結合のユースケース
次のケースを考えます。
AppSyncには(1)自分のお気に入りの組織一覧が入っており、Githubには(2)その組織ごとのレポジトリの一覧があります。
両者を結合させたAPIでは、(1)お気に入りの組織に関係する(2)Githubのレポジトリの一覧を取得することにします
アーキテクチャ
AppSyncとGithubから両者のAPIを取り込むNode.jsサーバを起動し、両者を結合させ新たなGraphQL APIを作ります。
結合させるGraphQLの仕組みを「スキーマスティッチング」といい、別記事に詳述しています。
必要なNode.jsサーバはステートレスで簡単なものですので、NetlifyやHeroku、AWS Lambdaなどのマネージドサービス上に構築すればいいです。
今回はAWS Lambdaに起動します。
AppSync側のAPIの構築
AppSync上に作成するAPI「お気に入りの組織」のモデルは以下とします。
type FeaturedOrganizations {
id: ID!
name: String
github_name: String # 後ほど出てくるGithub APIの引数
}
以下のように、AppSyncのコンソールでモデルを構築します。
AppSyncのQueriesやDynamoDBの直接操作で、あらかじめいくつかのデータを登録しておきます。
私は以下のようにしました。
APIアクセスの準備
作成したAPIのサマリ画面からaws-exports.jsをダウンロードします。
GithubのAPI
Github API v4のモデルの一例に、以下のようなOrganizationがあります。
引数がlogin
という文字列で、返り値としてOrganization
を取得できます。
Organization
には、repositories
というその組織のレポジトリを取得できるフィールドがあります。
type Query {
organization(login: String!): Organization
}
type Organization {
repositories(
privacy: RepositoryPrivacy
orderBy: RepositoryOrder
affiliations: [RepositoryAffiliation]
ownerAffiliations: [RepositoryAffiliation]
isLocked: Boolean
after: String
before: String
first: Int
last: Int
isFork: Boolean): RepositoryConnection!
}
スキーマはGithub API Explorerで確認できます。
APIアクセスの準備
githubのsettings -> developers settings -> personal access tokenから、アクセストークンをあらかじめ取得します。
取得したトークンを、.env
に記載します。
GITHUB_ACCESS_TOKEN=<取得したトークン>
新APIの仕様とAWS Lambda上のNode.jsサーバの実装
Github APIのorganization
の引数login
に先ほどのAppSync APIのgithub-name
を入れることができます。
これにより、AppSync APIのgithub-name
を使って両者を結合したAPIを作成できます。
新APIに対するクエリは全体でこうなります。
query {
# AppSyncをベースとした新規API
listFeaturedOrganizations {
items {
name
github_name
# 以上がAppSync API(FeaturedOrganizations)から引き継いだ内容
# 以下がGithub API(Organization)から引き継いだ内容
githubOrganization {
repositories(first: 1) {
nodes {
name
}
}
}
}
}
}
新APIは、以下の図のようにAppSync API (FeaturedOrganizations
)をベースとして、Github API (organization
)の返り値を入れ子に持つ構造になります。
APIの返り値は以下のように作られます。
- 1) AppSyncから値をコピーする
- 2) github_name
はGithub APIの引数として使われる
- 3) organization
は新たなフィールドgithubOrganization
として埋め込まれる
さて、結合APIの実装であるNode.jsサーバは以下のように実装します。
同じフォルダに先ほどのAppSyncからダウンロードしたaws-exports.js
を格納しておきます。
import {makeRemoteExecutableSchema, mergeSchemas, introspectSchema} 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';
config()
const aws_exports = require('./aws-exports').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 schema = mergeSchemas({
schemas:[githubSchema, appsyncSchema, linkSchemaDefs],
resolvers: {
FeaturedOrganizations: {
githubOrganization: {
fragment: `fragment git on FeaturedOrganization {github_name}`,
resolve(parent, args, context, info) {
return info.mergeInfo.delegateToSchema({
schema: githubSchema,
operation: 'query',
fieldName: 'organization',
args: {login: parent.github_name},
context,
info
});
}
}
}
}
});
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);
}
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でビルドするためにwebpack.config.jsも準備しました。
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
オフラインで検証した結果、以下の通り2つのAPIがマージできていることがわかります。
まとめ
既存APIであるGithub APIと自作のAppSync APIを結合させました。
結果、Lambda上に新たなGraphQL APIを起動できました。
スキーマスティッチングを使うと、もとのAPIのスキーマの差分だけを書くことで結合ができました。
AppSyncやGithubの個々の認証情報もLambda上でまとめてくれるので、端末から入力不要になります。
AppSyncのAPIと外のAPIをさくっと結合した新たなBFFを作るときに、Lambdaは重宝しそうです。