AlexaのPersistent AttributesをAppSyncからGraphQLで取得する
R3の山内さんにAppSync&Lambdaでの使い方を教えて頂けたので、そこにAlexaを乗せてみようと思います。ということでAlexa skill Advent Calendar 2018 20日目 & […]
目次
R3の山内さんにAppSync&Lambdaでの使い方を教えて頂けたので、そこにAlexaを乗せてみようと思います。ということでAlexa skill Advent Calendar 2018 20日目 & Alexa Skills Kit SDK Advent Calendar 2018 20日目です。
今回使うデータベース
ちょうどre:invent2018のワークショップで使ったものがありましたので、これを使います。データ構造はこんな感じです。
{
"Item": {
"attributes": {
"M": {
"hintsUsed": {
"N": "2"
},
"hintsPurchased": {
"N": "10"
}
}
},
"id": {
"S": "amzn1.ask.account.XXXXXXXX"
}
}
}
これをGraphQL(AppSync)とってきましょう。
AppSyncでAPIを作る
DynamoDBがすでにあるので、これをインポートして作ります。
リージョンとテーブル・ロールの指定がありますので選びましょう。
モデル定義では、attributes
を追加してやりましょう。Map形式なので、JSON Objectで良さそうです。
作成すると、以下のようにlist~
でデータが取れるようになります。
しかしQueryVariables
を見ると、「attributesの値が想定したものになっていない」ということに気づきます。次はここを修正します。
ModelをAlexaに合わせる
このままだとちょっとAlexaのバックエンドにするには厳しい点がありますので、手を入れていきましょう。初回は手で組んで、AWS CLI + CloudFormation でコード化してしまえば良いでしょう。
Schemaの修正
[Schema]からモデルを修正しましょう。今回は以下のように各値を修正しました。Attributesのモデル定義とcreate時のid入力必須化を実行しています。
type Attributes {
hintsPurchased: Int
hintsUsed: Int
}
input CreateNameTheShowInput {
id: String!
attributes: InputAttributes
}
input DeleteNameTheShowInput {
id: String!
}
input InputAttributes {
hintsPurchased: Int
hintsUsed: Int
}
type Mutation {
createNameTheShow(input: CreateNameTheShowInput!): NameTheShow
updateNameTheShow(input: UpdateNameTheShowInput!): NameTheShow
deleteNameTheShow(input: DeleteNameTheShowInput!): NameTheShow
}
type NameTheShow {
id: String!
attributes: Attributes
}
type NameTheShowConnection {
items: [NameTheShow]
nextToken: String
}
type Query {
getNameTheShow(id: String!): NameTheShow
listNameTheShows(filter: TableNameTheShowFilterInput, limit: Int, nextToken: String): NameTheShowConnection
}
type Subscription {
onCreateNameTheShow(id: String, attributes: InputAttributes): NameTheShow
@aws_subscribe(mutations: ["createNameTheShow"])
onUpdateNameTheShow(id: String, attributes: InputAttributes): NameTheShow
@aws_subscribe(mutations: ["updateNameTheShow"])
onDeleteNameTheShow(id: String, attributes: InputAttributes): NameTheShow
@aws_subscribe(mutations: ["deleteNameTheShow"])
}
input TableBooleanFilterInput {
ne: Boolean
eq: Boolean
}
input TableFloatFilterInput {
ne: Float
eq: Float
le: Float
lt: Float
ge: Float
gt: Float
contains: Float
notContains: Float
between: [Float]
}
input TableIDFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
}
input TableIntFilterInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
contains: Int
notContains: Int
between: [Int]
}
input TableNameTheShowFilterInput {
id: TableStringFilterInput
}
input TableStringFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
}
input UpdateNameTheShowInput {
id: String!
attributes: InputAttributes
}
最後に[Save Schema] して保存しましょう。
Resolverの変更
createについては、デフォルトだとidを自動生成します。しかしAlexaの場合自動生成されると困るので、Resolverのテンプレートを以下のように変更します。
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id",
},
},
}
これでOKです。[Queries]タブから意図したように動くか確認しましょう。
コンソールから動作確認
Lambdaから叩く前にSchemaがうまくいっているか確認します。
新規レコードの作成
まず[Query Variables]に以下の値をいれましょう。
{
"createnametheshowinput": {
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsUsed": 2
}
}
}
続いて以下のようなmutationを用意し、実行します。
mutation createNameTheShow($createnametheshowinput: CreateNameTheShowInput!) {
createNameTheShow(input: $createnametheshowinput) {
id
attributes {
hintsUsed
hintsPurchased
}
}
}
以下のようなレスポンスが返ってくればOKです。
{
"data": {
"createNameTheShow": {
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsUsed": 3,
"hintsPurchased": null
}
}
}
}
AWS CLIなどでDynamoDBからもデータが取れることを確認しておきましょう。
$ aws dynamodb get-item --table-name NameTheShow --key '{"id": {"S": "amzn1.ask.account.EXAMPLE"}}' | jq .
{
"Item": {
"attributes": {
"M": {
"hintsUsed": {
"N": "3"
}
}
},
"id": {
"S": "amzn1.ask.account.EXAMPLE"
}
}
}
レコードの更新
続いてレコードの更新をやってみましょう。
// mutation
mutation updateNameTheShow($createnametheshowinput: UpdateNameTheShowInput!) {
updateNameTheShow(input: $createnametheshowinput) {
id
attributes {
hintsUsed
hintsPurchased
}
}
}
// Query Variables
{
"createnametheshowinput": {
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsUsed": 4,
"hintsPurchased": 1
}
}
}
// Result
{
"data": {
"updateNameTheShow": {
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsUsed": 4,
"hintsPurchased": 1
}
}
}
}
更新できましたね。
List / Getする
最後に取得もやっておきましょう。何気にListできるのありがたいですね。
List
query listNameTheShows {
listNameTheShows {
items {
id
attributes {
hintsPurchased
hintsUsed
}
}
}
}
// Response
{
"data": {
"listNameTheShows": {
"items": [
{
"id": "amzn1.ask.account.XXXXXXXXx",
"attributes": {
"hintsPurchased": 10,
"hintsUsed": 2
}
},
{
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsPurchased": 1,
"hintsUsed": 4
}
}
]
}
}
}
Get
// query
query getNameTheShow($getItem: String!) {
getNameTheShow(id: $getItem) {
id
attributes {
hintsUsed
hintsPurchased
}
}
}
// Query Variables
{
"getItem": "amzn1.ask.account.EXAMPLE"
}
// Result
{
"data": {
"getNameTheShow": {
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsUsed": 4,
"hintsPurchased": 1
}
}
}
}
Node.jsから実行する
つぎはNode.jsからアクセスしてみましょう。最低限必要なのが以下の3つです。
$ yarn add -D isomorphic-fetch graphql-tag aws-appsync
$ vim .envrc
export AWS_REGION=REGION
export API_KEY=YOUR_APPSYNC_API_KEY
export API_URL=https://YOUR_API_URL.appsync-api.us-east-1.amazonaws.com/graphql
$ direnv allow
queryのコードは以下のようになります。
require('isomorphic-fetch');
// Require AppSync module
const gql = require('graphql-tag');
const AUTH_TYPE = require('aws-appsync/lib/link/auth-link').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
// Secrets
const APIKey = process.env.API_KEY
const REGION = process.env.AWS_REGION
const URL = process.env.API_URL
// Import gql helper and craft a GraphQL query
const queryName1 = 'listNameTheShows'
const query1 = gql(`
query ${queryName1} {
${queryName1} {
items {
id
attributes {
hintsPurchased
hintsUsed
}
}
}
}`);
const queryName2 = 'getNameTheShow'
const query2 = gql(`
query ${queryName2}($id: String!) {
${queryName2}(id: $id) {
id
attributes {
hintsPurchased
hintsUsed
}
}
}`);
const variables2 = {
id: "amzn1.ask.account.EXAMPLE"
}
// Set up Apollo client
const client = new AWSAppSyncClient({
url: URL,
region: REGION,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: APIKey
},
disableOffline: true
});
// List ALL
const queryListItems = async (queryName, query) => {
const params = {query}
const { data } = await client.query(params)
const { items } = data[queryName]
console.log(`====${queryName}====`)
items.forEach(item => console.log(item))
};
// Get
const queryGetItem = async (queryName, query, variables = {}) => {
const params = {query}
if (Object.keys(variables).length > 0) params.variables = variables
const { data } = await client.query(params)
const item = data[queryName]
console.log(`====${queryName}====`)
console.log(item)
};
queryListItems(queryName1, query1)
.then(() => queryGetItem(queryName2, query2, variables2))
.catch(e => console.log(e))
これをローカルで実行すると、このようになります。
====listNameTheShows====
{ id: 'amzn1.ask.account.XXXXXXXXXXXX',
attributes: { hintsPurchased: 10, hintsUsed: 2, __typename: 'Attributes' },
__typename: 'NameTheShow' }
{ id: 'amzn1.ask.account.EXAMPLE',
attributes: { hintsPurchased: 1, hintsUsed: 4, __typename: 'Attributes' },
__typename: 'NameTheShow' }
====getNameTheShow====
{ id: 'amzn1.ask.account.EXAMPLE',
attributes: { hintsPurchased: 1, hintsUsed: 4, __typename: 'Attributes' },
__typename: 'NameTheShow' }
ちゃんととれてますね。mutationはこうなります。
require('isomorphic-fetch');
// Require AppSync module
const gql = require('graphql-tag');
const AUTH_TYPE = require('aws-appsync/lib/link/auth-link').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
// Secrets
const APIKey = process.env.API_KEY
const REGION = process.env.AWS_REGION
const URL = process.env.API_URL
// Import gql helper and craft a GraphQL query
const mutationName1 = 'createNameTheShow'
const mutationItem1 = gql(`
mutation createNameTheShow($createnametheshowinput: CreateNameTheShowInput!) {
createNameTheShow(input: $createnametheshowinput) {
id
attributes {
hintsUsed
hintsPurchased
}
}
}
`);
const variables1 = {
"createnametheshowinput": {
"id": "amzn1.ask.account.EXAMPLE1",
"attributes": {
"hintsPurchased": 10
}
}
}
const mutationName2 = 'updateNameTheShow'
const mutationItem2 = gql(`
mutation updateNameTheShow($createnametheshowinput: UpdateNameTheShowInput!) {
updateNameTheShow(input: $createnametheshowinput) {
id
attributes {
hintsUsed
hintsPurchased
}
}
}`);
const variables2 = {
"createnametheshowinput": {
"id": "amzn1.ask.account.EXAMPLE",
"attributes": {
"hintsUsed": 10,
"hintsPurchased": 11
}
}
}
// Set up Apollo client
const appSyncClient = new AWSAppSyncClient({
url: URL,
region: REGION,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: APIKey
},
disableOffline: true
});
const mutateItem = async (name, item, variables) => {
const params = {
mutation: item,
variables
}
const { data } = await appSyncClient.mutate(params)
console.log(`====${name}====`)
console.log(data[name])
};
mutateItem(mutationName1, mutationItem1, variables1)
.then(() => mutateItem(mutationName2, mutationItem2, variables2))
.catch(e => console.log(e))
1がcreateで2がupdateになっています。createはupdate / put的用途で使うとalready existsでエラーでるのでupdateを使いましょう。
====createNameTheShow====
{ id: 'amzn1.ask.account.EXAMPLE1',
attributes: { hintsUsed: null, hintsPurchased: 10, __typename: 'Attributes' },
__typename: 'NameTheShow' }
====updateNameTheShow====
{ id: 'amzn1.ask.account.EXAMPLE',
attributes: { hintsUsed: 10, hintsPurchased: 11, __typename: 'Attributes' },
__typename: 'NameTheShow' }
ちなみにsubscriptionをする際の注意点がドキュメントにありますので、こちらもご覧くださいませ。
おわりに
あとはこれをLambdaに組み込むことで、AlexaのPersistant AttributesをAppSyncから扱えるようになります。使いどころとしては、こういうところがあるでしょう。
- Alexaでプレイしたゲームの結果をweb / アプリからリアルタイムで確認する
- Alexaの利用状況をリアルタイムでモニタリング
- サイネージなどと組み合わせた受付スキル
- etc
手札としてもっているだけでも、発想の幅が広がると思います。