概要
以下の様な構成でGraphQLでサーバーレスなECサイトを作ってみましょう。AppSyncのバックエンドにStepFunctionsに置くことで決済のトランザクションを可能にしています。トランザクションの結果はGraphQLのSubscriptionでフロントエンド側に通知を行う構成になっています。
GraphQLスキーマ
スキーマを以下の様に定義します。createPayment
ミューテーションで購入リクエストをブラウザから送ります。そして、StepFunctionsの最後のステップでpublishPaymentResult
ミューテーションに購入処理のトランザクション結果を送り、ブラウザでは onGetResult
サブスクリプションで結果を受け取る設計になってます。
type Mutation {
createPayment(input: PaymentInput!): Payment
publishPaymentResult(result: ResultInput): Result
}
type Payment {
id: ID!
price: Int!
amount: Int!
}
input PaymentInput {
id: ID!
price: Int!
amount: Int!
}
type Query {
getPayment(id: ID!): Payment
}
type Result {
id: ID!
status: ResultStatus!
}
input ResultInput {
id: ID!
status: ResultStatus!
}
enum ResultStatus {
SUCCESS
FAILURE
}
type Subscription {
onGetResult(id: ID!): Result
@aws_subscribe(mutations: ["publishPaymentResult"])
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
AppSyncの設定
Auth0による認証
OIDC認証を以下のように設定します。プロバイダードメインには契約したあなたのAuth0テナントのURLを設定しましょう
StepFunctionsとの連携
Classmethodの岩田さんのAppSyncのクエリからStep Functionsのステートマシンを起動して時間のかかる処理を非同期に実行する を参考にさせてもらいました。
記事のとおりにHTTPのデータソースを作成してそこからIAMで認可させてStepFunctionsのエンドポイントにリクエストを投げています。
リクエストのマッピングテンプレートは以下のように定義しています。$context.identity.claims.sub
やstripe_user_id
などを渡すことでStepFunctions内のLambdaでAuth0上のユーザデータやStripe上のデータとやり取りできるようにしています。
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"content-type": "application/x-amz-json-1.0",
"x-amz-target":"AWSStepFunctions.StartExecution"
},
"body": {
"stateMachineArn": "arn:aws:states:us-east-1:<accountID>:stateMachine:myStateMachine",
"input": "{ \"id\": \"$context.arguments.input.id\", \"idToken\": \"$context.request.headers.authorization\", \"user_id\":\"$context.identity.claims.sub\", \"stripe_user_id\":\"$context.identity.claims['https://serverless-ec.com/stripe_user_id']\" }"
}
}
}
StepFunctionsの設定
以下の様にワークフローを定義します。決済のトランザクション処理を実施できる仕組みになっています。どこかで処理が失敗した場合には処理をロールバックさせて失敗したことを通知出来るような仕組みになっています。
それぞれのステップでは以下の処理を実施しています。
ChargeStripe | StripeのChargeAPIを叩いて決済処理を実施します |
Order | 注文処理を実施します |
NotificationSuccess | PublishPaymentResultミューテーションに成功を送信します |
Rollback | 注文処理が失敗した場合にStripeの決済を取り消して状態を元に戻します |
NotificationError | PublishPaymentResultミューテーションに失敗を送信します |
ChargeStripe
では以下のような形で決済処理を実施します。
'use strict';
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
module.exports.handler = async event => {
const charge = await stripe.charges.create({
amount: 3000,
currency: "jpy",
description: 'テスト決済',
customer: event.stripe_user_id
})
return {
id: event.id,
idToken: event.idToken,
charge: charge.id
}
}
そして処理がすべて成功したらNotificationSuccess
で成功通知用にミューテーションを送ります。以下のコードもClassmethodの岩田さんの記事を参考にさせてもらいました。
'use strict';
const axios = require('axios')
const PublishPaymentResultMutation = `mutation PublishPaymentResult(
$id: ID!,
$status: ResultStatus!
) {
publishPaymentResult(result: {id: $id, status: $status}) {
id
status
}
}`;
module.exports.handler = async event => {
console.log(event)
const mutation = {
query: PublishPaymentResultMutation,
operationName: 'PublishPaymentResult',
variables: {
id: event.id,
status: 'SUCCESS'
},
};
try {
const response = await axios({
method: 'POST',
url: process.env.APPSYNC_URL,
data: JSON.stringify(mutation),
headers: {
'Content-Type': 'application/json',
'Authorization': `${event.idToken}`,
}
});
} catch (error) {
console.error(`[ERROR] ${error.response.status} - ${JSON.stringify(error.response.data)}`);
throw error;
}
};
Rollbackでは以下の様なコードで決済をRefundする処理を書いています
'use strict';
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
module.exports.handler = async event => {
const error = JSON.parse(event.Cause)
const data = JSON.parse(error.errorMessage)
await stripe.refunds.create({
charge: data.charge
})
return {
id: data.id,
idToken: data.idToken
}
}
Vue.js(Nuxt.js)からGraphQLにリクエストを送るためにApolloクライアントを設定する
Auth0 と連携する
フロントエンドのサンプルは Vue.js(Nuxt.js) で作成していきます。先ずはOIDC 認証モードでリクエストを送るためにIDトークンを取得する必要があり、フロントエンド側とAuth0を連携する設定を行います。package.json に @nuxtjs/auth
モジュールを追加し、nuxt.config.js
に以下の設定を追加します。
const config = {
modules: [
// ...
'@nuxtjs/auth'
],
// ...
auth: {
strategies: {
auth0: {
domain: 'xxx.auth0.com' // Auth0 Application Domain
client_id: '...' // Auth0 Application Client ID,
scope: [ 'openid' ],
response_type: 'id_token token',
token_key: 'id_token'
}
},
redirect: {
login: '/',
logout: '/',
callback: '/callback',
home: '/main'
}
}
}
サインイン画面に遷移させたいところで以下のように実装します。IDトークンを取得する方法については後述します。
methods: {
login() {
this.$auth.loginWith('auth0')
}
}
Apollo クライアントを設定、利用する
package.json にいくつか必要なモジュールを追加します。
yarn add @nuxtjs/apollo aws-appsync aws-appsync-subscription-link apollo-link
nuxt.config.js
には以下のように設定を追加します。
const config = {
modules: [
// ...
'@nuxtjs/apollo'
],
// ...
apollo: {
authenticationType: '',
clientConfigs: {
default: '~/apollo/config.js'
}
},
}
注意点としてはauthenticationType
に空文字 ''
を設定する必要があります。Bearer
のような認証タイプは特に指定しません。
続いて、Apollo クライアントの設定ファイル ~/apollo/config.js
を書いていきます。AppSyncのSubscriptionを利用するためには、AppSyncのリアルタイムエンドポイントとのWebSocket接続を確立してくれるモジュールを利用する必要があります。
import { Context } from '@nuxt/types/app'
import { ApolloLink } from 'apollo-link'
import { AUTH_TYPE } from 'aws-appsync'
import { createSubscriptionHandshakeLink } from 'aws-appsync-subscription-link'
export default ctx => {
const { APPSYNC_GRAPHQL_ENDPOINT, APPSYNC_REGION } = ctx.env
const link = ApolloLink.from([
createSubscriptionHandshakeLink({
url: APPSYNC_GRAPHQL_ENDPOINT,
region: APPSYNC_REGION,
auth: {
type: AUTH_TYPE.OPENID_CONNECT,
// Context, Vuex, localStorage 等からセット可能
jwtToken: () => 'idToken'
}
})
])
return { link }
}
以上、AppSync とつなぐための準備ができました。Vue コンポーネントの中で apollo
プロパティを利用するか、this.$apollo.getClient()
でクライアントインスタンスを取得するなどして、query/mutation/subscriptionを実装できます。
以下のようにAuth0連携で取得した ID Token を指定することもできます。
// ID Token を取得
const bearerIdToken = this.$auth.getToken('auth0')
// 先頭 'Bearer' 文字列を外す
const idToken = bearerToken.substring(constants.BEARER_PREFIX.length)
// クライアントに ID Token をセット
this.$apolloHelpers.onLogin(idToken)
// GraphQLリクエストを送る
this.$apollo.getClient().query({ /* ... */ })
this.$apollo.getClient().mutation({ /* ... */ })
this.$apollo.getClient().subscribe({ /* ... */ })
動作確認
ローカルでサイトにアクセスして、まずはAuth0からログインを行います。
すると購入ページに遷移するので購入してみましょう。
以下の通りちゃんと結果をサブスクリプションで受け取ることが出来ました。
今度はOrder処理をわざと落としてちゃんとロールバックして失敗の結果が通知くるか検証してみましょう。
以下の通り失敗のステータスで結果が受け取れました。
StepFunctionsは以下の通りOrderが失敗してRollbackが発生していることがわかります。
Stripeでもちゃんと返金処理が実施されていました。