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

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

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark