AWSJavaScriptNode.js

AlexaのPersistent AttributesをAppSyncからGraphQLで取得する

R3の山内さんにAppSync&Lambdaでの使い方を教えて頂けたので、そこにAlexaを乗せてみようと思います。ということでAlexa skill Advent Calendar 2018 20日目 &amp […]

広告ここから
広告ここまで

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

手札としてもっているだけでも、発想の幅が広がると思います。

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts