AppSyncとGithub APIをマージして一つのGraphQL APIを作る

# 目的

[前回記事](https://qiita.com/Hiroyuki_OSAKI/items/ecaca21d47441f95030b)で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の仕組みを「スキーマスティッチング」といい、[別記事](https://qiita.com/Hiroyuki_OSAKI/items/9f8e64b93d159b631f96)に詳述しています。
必要なNode.jsサーバはステートレスで簡単なものですので、NetlifyやHeroku、AWS Lambdaなどのマネージドサービス上に構築すればいいです。
**今回はAWS Lambdaに起動**します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/7acae674-6fdc-dfcb-59a4-e07a24efecce.png)


## AppSync側のAPIの構築

AppSync上に作成するAPI「お気に入りの組織」のモデルは以下とします。

```graphql
type FeaturedOrganizations {
	id: ID!
	name: String
	github_name: String # 後ほど出てくるGithub APIの引数
}
```

以下のように、AppSyncのコンソールでモデルを構築します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/1526c76e-4703-7202-0794-0f03442a7756.png)

AppSyncのQueriesやDynamoDBの直接操作で、あらかじめいくつかのデータを登録しておきます。
私は以下のようにしました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/708e504b-d88b-7600-93cf-4cf3be578b26.png)

### APIアクセスの準備

作成したAPIのサマリ画面からaws-exports.jsをダウンロードします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/9a274eb4-8486-893e-7ad4-c2bddf392431.png)


## GithubのAPI

Github API v4のモデルの一例に、以下のようなOrganizationがあります。
引数が`login`という文字列で、返り値として`Organization`を取得できます。

`Organization`には、`repositories`というその組織のレポジトリを取得できるフィールドがあります。

```graphql
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!
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/71e6dcba-f773-a1e2-cc10-d8aed56b8898.png)

スキーマは[Github API Explorer](https://developer.github.com/v4/explorer/)で確認できます。

### APIアクセスの準備

githubのsettings -> developers settings -> personal access tokenから、アクセストークンをあらかじめ取得します。

取得したトークンを、`.env`に記載します。

```.env
GITHUB_ACCESS_TOKEN=<取得したトークン>
```

## 新APIの仕様とAWS Lambda上のNode.jsサーバの実装

Github APIの`organization`の引数`login`に先ほどのAppSync APIの`github-name`を入れることができます。
これにより、AppSync APIの`github-name`を使って両者を結合したAPIを作成できます。

新APIに対するクエリは全体でこうなります。

```graphql
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`)の返り値を入れ子に持つ**構造になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/88b1438e-8313-8861-de4a-c7dfff7ba0c2.png)

APIの返り値は以下のように作られます。
- 1) AppSyncから値をコピーする
- 2) `github_name`はGithub APIの引数として使われる
- 3) `organization`は新たなフィールド`githubOrganization`として埋め込まれる

さて、結合APIの実装であるNode.jsサーバは以下のように実装します。
同じフォルダに先ほどのAppSyncからダウンロードした`aws-exports.js`を格納しておきます。

```index.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`を以下のように準備します。

```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も準備しました。

```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がマージできていることがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96277/478b498d-95a6-003b-ba32-d8a4a4692588.png)

# まとめ

既存APIであるGithub APIと自作のAppSync APIを結合させました。
結果、Lambda上に新たなGraphQL APIを起動できました。
スキーマスティッチングを使うと、もとのAPIのスキーマの差分だけを書くことで結合ができました。
AppSyncやGithubの個々の認証情報もLambda上でまとめてくれるので、端末から入力不要になります。
AppSyncのAPIと外のAPIをさくっと結合した新たなBFFを作るときに、Lambdaは重宝しそうです。