LoginSignup
2
0

More than 1 year has passed since last update.

AWS CloudFormationでAWS AppSyncのパイプラインリゾルバーを作成する

Last updated at Posted at 2022-06-21

はじめに

AWS Amplifyのamplify pushコマンドでパイプラインリゾルバーを作成したかったのですが、色々とハマったので備忘録として記事にしました。
その時にこんな記事があったらよかったなぁという内容を記事にしました。

AWS Amplifyとは

AWS AmplifyとはAWSの環境上にアプリケーションを構築してくれるツールです。
AWS コマンドラインインターフェース (CLI) を利用して、"amplify push"でバックエンドのリソース設定ができます。
詳細はAmplifyの公式ドキュメントをご覧ください。
https://docs.amplify.aws/

パイプラインリゾルバーとは

パイプラインリゾルバーは関数を構成して順番に実行できる機能です。
Before マッピングテンプレート・関数のリスト・After マッピングテンプレートで構成されます。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/pipeline-resolvers.html

やりたいこと

やりたいことは、AWS AppSyncのパイプラインリゾルバーを作成すること!
そんなの簡単にこの画面👇からResolver「アタッチ」でできるじゃーんと思ったあなた、正解です。
image.png

簡単じゃーんと思ったあなた、まだ早いです。今回はこんな問題があったのです。

手動設定したときの問題

バックエンドの環境はすべてファイル管理しており、AWS Amplifyで構築していたため、
Dynamo DBのテーブル定義の変更やschema.graphqlの定義が更新される度に amplify push が実行されます。
当然のことながら、上図の画面でリゾルバーを手動設定しているとファイル管理されていないため、amplify push でリゾルバー設定は消えてしまうという訳です。

つまり、今回の真の「やりたいこと」はこれです。

amplify pushでパイプラインリゾルバーを作成したい!!!

選ばれたのはCloudFormationでした

AWS CloudFormationとは?

テンプレートから情報を取り込み、簡単にAWSの環境構築ができるサービスです。
テンプレートがあれば、環境を何回でも同じように構築できます。
今回の用途は先程GraphQLに書いたクエリ(querySeated)にアタッチするパイプラインリゾルバーの設定をCloudFormationテンプレートに書きます。

色々と実現する方法はあるのかも知れませんが、私が見つけたのはタイトルにもある通り、AWS CloudFormationを使う方法でした。
ただ実現するにあたり、困ったことがいくつも出てきました、、、

本記事は長いため、すべて読まなくても良いです。
必要とする部分だけを読んでいただくため、Tipsを用意しました。
Tips

これらの不明点を解決しつつ、AWS CloudFormationでパイプラインリゾルバーを作成しましょう!

まずは準備から

取得先のテーブルとリゾルバーをアタッチするクエリを用意します。

取得先のテーブル

テーブル作成スキーマ

 type SeatingHistory @model 
 @key(fields: ["seatId", "unixTimeId"])
 @key(name: "bySeated", fields: ["seatedDate","seatedTime"])
 {
  seatId: ID!
  unixTimeId: Int!
  timestamp: String
  seatedDate: String!
  seatedTime: String!
}

本編の内容とは逸れますので、ご参考まで

着席履歴テーブル(SeatingHistoryテーブル)

  • 着席履歴を管理する。
  • 着席するとuserId・userNameに着席者のデータが登録される。
  • 離席するとuserId・userNameがNULLのデータが登録される。

テーブル定義

  • パーティションキー:seatId(座席ID)
  • ソートキー:unixTimeId(レコードが登録されたUNIXタイム)
  • グローバルセカンダリインデックス
    • パーティションキー:seatedDate(着席時・離席時の日付)
    • ソートキー:seatedTime(着席時・離席時の時間)

補足
DynamoDBのキー・インデックスについてはこちらの記事をご覧ください!
オカメも大変お世話になった記事です
https://qiita.com/shibataka000/items/e3f3792201d6fcc397fd


アタッチするクエリ

クエリ作成用スキーマ

type Query{
	querySeated(
		seatedDate: String,
		seatedTime: ModelStringKeyConditionInput,
		sortDirection: ModelSortDirection,
    	filter: ModelSeatingHistoryFilterInput,
		limit: Int,
		nextToken: String
	): ModelSeatingHistoryConnection
}

input ModelStringKeyConditionInput {
	eq: String
	le: String
	lt: String
	ge: String
	gt: String
	between: [String]
	beginsWith: String
}

enum ModelSortDirection {
	ASC
	DESC
}

type ModelSeatingHistoryConnection {
	items: [SeatingHistory]
  nextToken: String
}

input ModelSeatingHistoryFilterInput {
	seatId: ModelIDInput
	unixTimeId: ModelIntInput
	timestamp: ModelStringInput
	seatedDate: ModelStringInput
	seatedTime: ModelStringInput
	installationLocation: ModelStringInput
	xAxis: ModelFloatInput
	yAxis: ModelFloatInput
	projectCd: ModelStringInput
	projectName: ModelStringInput
	userId: ModelStringInput
	userName: ModelStringInput
	button1Event: ModelStringInput
	button2Event: ModelStringInput
	button3Event: ModelStringInput
	button4Event: ModelStringInput
	button5Event: ModelStringInput
	and: [ModelSeatingHistoryFilterInput]
	or: [ModelSeatingHistoryFilterInput]
	not: ModelSeatingHistoryFilterInput
}

input ModelIDInput {
	ne: ID
	eq: ID
	le: ID
	lt: ID
	ge: ID
	gt: ID
	contains: ID
	notContains: ID
	between: [ID]
	beginsWith: ID
	attributeExists: Boolean
	attributeType: ModelAttributeTypes
	size: ModelSizeInput
}

input ModelIntInput {
	ne: Int
	eq: Int
	le: Int
	lt: Int
	ge: Int
	gt: Int
	between: [Int]
	attributeExists: Boolean
	attributeType: ModelAttributeTypes
}

input ModelStringInput {
	ne: String
	eq: String
	le: String
	lt: String
	ge: String
	gt: String
	contains: String
	notContains: String
	between: [String]
	beginsWith: String
	attributeExists: Boolean
	attributeType: ModelAttributeTypes
	size: ModelSizeInput
}

input ModelFloatInput {
	ne: Float
	eq: Float
	le: Float
	lt: Float
	ge: Float
	gt: Float
	between: [Float]
	attributeExists: Boolean
	attributeType: ModelAttributeTypes
}

enum ModelAttributeTypes {
	binary
	binarySet
	bool
	list
	map
	number
	numberSet
	string
	stringSet
	_null
}

input ModelSizeInput {
	ne: Int
	eq: Int
	le: Int
	lt: Int
	ge: Int
	gt: Int
	between: [Int]
}

  • querySeated
    今回リゾルバーをアタッチするクエリ

CloudFormationテンプレート

準備が整ったのでCloudFormationテンプレートを触っていきましょうー

どこに置けばいいの?

私はCustomResources.jsonファイルに記載しました。
amplify > backend > api > stacks > CustomResources.json
しかし、別のサイトを見ると同じディレクトリに別ファイルを作成しても良いそうです。
私は試していませんが、こちらの記事ではやっていますね。
https://thinkami.hatenablog.com/entry/2019/07/15/201356

project_root/amplify/backend/api/infraAPI/stacks の中に ExistsDynamoDB.json ファイルを作成します。
なお、同じディレクトリには CustomResources.json があります。このファイルに追記しても良いですし、別ファイルとして作成しても良いです。
今回は、 CustomResources.json をベースに必要な項目を追加した ExistsDynamoDB.json となります。

何を書けばいいの?

👇CustomResources.jsonに何を書くの?という部分ですが、こちらに全文を載せました

CustomResources.json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "AppSyncApiId": {
      "Type": "String",
      "Description": "The id of the AppSync API associated with this project."
    }
  },
  "Resources": {
    "FunctionConfiguration1": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "QueryPrevDate",
        "DataSourceName": "SeatingHistoryTable",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "{",
              " \"operation\" : \"Query\",",
              " \"query\" : {",
              "   \"expression\": \"seatedDate = :seatedDate\",",
              "   \"expressionValues\" : {",
              "     \":seatedDate\" : $util.dynamodb.toDynamoDBJson(${ctx.stash.prevDate})",
              "   }",
              " },",
              " \"index\" : \"bySeated\"",
              "}"
            ]
          ]
        },
        "ResponseMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set ($myMap = {})",
              "#foreach ($item in $ctx.result.items)",
              " #if($myMap.containsKey($item.seatId) == false)",
              "   $util.qr($myMap.put($item.seatId, $item))",
              " #end",
              " #if($myMap.get($item.seatId).unixTimeId < $item.unixTimeId)",
              "   $util.qr($myMap.put($item.seatId, $item))",
              " #end",
              "#end",
              "$util.toJson({\"items\": $myMap.values()})"
            ]
          ]
        }
      }
    },
    "FunctionConfiguration2": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "QuerySeatedDateTime",
        "DataSourceName": "SeatingHistoryTable",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set( $modelQueryExpression = {} )",
              "#if( !$util.isNull($ctx.args.seatedDate) )",
              " #set( $modelQueryExpression.expression = \"#seatedDate = :seatedDate\" )",
              " #set( $modelQueryExpression.expressionNames = {",
              " \"#seatedDate\": \"seatedDate\"",
              "} )",
              " #set( $modelQueryExpression.expressionValues = {",
              " \":seatedDate\": {",
              "     \"S\": \"$ctx.args.seatedDate\"",
              " }",
              "} )",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.beginsWith) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND begins_with(#sortKey, :sortKey)\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.beginsWith\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.between) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey BETWEEN :sortKey0 AND :sortKey1\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey0\", { \"S\": \"$ctx.args.seatedTime.between[0]\" }))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey1\", { \"S\": \"$ctx.args.seatedTime.between[1]\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.eq) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey = :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.eq\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.lt) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey < :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.lt\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.le) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey <= :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.le\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.gt) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey > :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.gt\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.ge) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey >= :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.ge\" }))",
              "#end",
              "#set( $limit = $util.defaultIfNull($context.args.limit, 100) )",
              "#set( $QueryRequest = {",
              " \"version\": \"2018-05-29\",",
              " \"operation\": \"Query\",",
              " \"limit\": $limit,",
              " \"query\": $modelQueryExpression,",
              " \"index\": \"bySeated\"",
              "} )",
              "#if( !$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == \"DESC\" )",
              "  #set( $QueryRequest.scanIndexForward = false )",
              "#else",
              "  #set( $QueryRequest.scanIndexForward = true )",
              "#end",
              "#if( $context.args.nextToken ) #set( $QueryRequest.nextToken = $context.args.nextToken ) #end",
              "#if( $context.args.filter ) #set( $QueryRequest.filter = $util.parseJson(\"$util.transform.toDynamoDBFilterExpression($ctx.args.filter)\") ) #end",
              "$util.toJson($QueryRequest)"
            ]
          ]
        },
        "ResponseMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#if($ctx.error)",
              " $util.error($ctx.error.message, $ctx.error.type)",
              "#end",
              "#set ($myMap = {})",
              "#foreach ($item in $ctx.result.items)",
              "  #if($myMap.containsKey($item.seatId) == false)",
              "     $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "  #if($myMap.get($item.seatId).unixTimeId < $item.unixTimeId)",
              "     $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "#end",
              "#foreach ($item in $ctx.prev.result.items)",
              "  #if($myMap.containsKey($item.seatId) == false)",
              "     $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "#end",
              "$util.toJson({",
              " \"items\": $myMap.values()",
              "})"
            ]
          ]
        }
      }
    },
    "PipelineResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "TypeName": "Query",
        "FieldName": "querySeated",
        "Kind": "PIPELINE",
        "PipelineConfig": {
          "Functions": [
            {
              "Fn::GetAtt": [
                "FunctionConfiguration1",
                "FunctionId"
              ]
            },
            {
              "Fn::GetAtt": [
                "FunctionConfiguration2",
                "FunctionId"
              ]
            }
          ]
        },
        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set($utcDate = $util.time.parseFormattedToEpochMilliSeconds(\"$ctx.args.seatedDate 00:00:00+0900\", \"yyyyMMdd HH:mm:ssZ\"))",
              "#set($utcPrevDate = $utcDate - 86400000)",
              "#set($prevDate = $util.time.epochMilliSecondsToFormatted($utcPrevDate, \"yyyyMMdd\", \"+09:00\"))",
              "$util.qr($ctx.stash.put(\"prevDate\", $prevDate))",
              "{}"
            ]
          ]
        },
        "ResponseMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set($myMap = {})",
              "#foreach($item in $ctx.prev.result.items)",
              "  #if($item.userId)",
              "   $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "#end",
              "$util.toJson({",
              "  \"items\": $myMap.values()",
              "})"
            ]
          ]
        }
      }
    }
  },
  "Outputs": {}
}

補足
CloudFormationの設定ファイルはJSONとYAMLの両方をサポートしていますが、Amplifyで作成する場合はJSONのみサポートしています。
そのため、YAMLで記述したCustomResourceを使おうとすると、以下のエラーが発生します。
Yaml is not yet supported. Please convert the CloudFormation stack ExistsDynamoDB.yaml to json.
https://thinkami.hatenablog.com/entry/2019/07/15/201356

詳しく見てみよう!!

では何を設定しているのか、細かく見てみましょう。

  "AWSTemplateFormatVersion": "2010-09-09"
  • テンプレートの形式バージョン
  "Parameters": {
    "AppSyncApiId": {
      "Type": "String",
      "Description": "The id of the AppSync API associated with this project."
    }
  • テンプレートでのパラメーターの定義
    "FunctionConfiguration1": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "QueryPrevDate",
        "DataSourceName": "SeatingHistoryTable",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "{",
              " \"operation\" : \"Query\",",
              " \"query\" : {",
              "   \"expression\": \"seatedDate = :seatedDate\",",
              "   \"expressionValues\" : {",
              "     \":seatedDate\" : $util.dynamodb.toDynamoDBJson(${ctx.stash.prevDate})",
              "   }",
              " },",
              " \"index\" : \"bySeated\"",
              "}"
            ]
          ]
        },
        "ResponseMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set ($myMap = {})",
              "#foreach ($item in $ctx.result.items)",
              " #if($myMap.containsKey($item.seatId) == false)",
              "   $util.qr($myMap.put($item.seatId, $item))",
              " #end",
              " #if($myMap.get($item.seatId).unixTimeId < $item.unixTimeId)",
              "   $util.qr($myMap.put($item.seatId, $item))",
              " #end",
              "#end",
              "$util.toJson({\"items\": $myMap.values()})"
            ]
          ]
        }
      }
    }
  • GraphQLAPIの関数を定義
プロパティ名 説明 必須
ApiId リゾルバーをアタッチするAWSAppSyncGraphQLAPI Yes 文字列
DataSourceName リゾルバーのデータソース名 Yes 文字列
FunctionVersion リクエストマッピングテンプレートのバージョン Yes 文字列
Name 関数の名前 Yes 文字列
RequestMappingTemplate Functionリクエストマッピングテンプレート No 文字列
ResponseMappingTemplate Function応答マッピングテンプレート No 文字列

公式
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-functionconfiguration.html

JSONにVTLのソースをどう埋め込むの?

という疑問もこれでお分かりですね。

        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "{",
              " \"operation\" : \"Query\",",
              " \"query\" : {",
              "   \"expression\": \"seatedDate = :seatedDate\",",
              "   \"expressionValues\" : {",
              "     \":seatedDate\" : $util.dynamodb.toDynamoDBJson(${ctx.stash.prevDate})",
              "   }",
              " },",
              " \"index\" : \"bySeated\"",
              "}"
            ]
          ]
        }

RequestMappingTemplate・ResponseMappingTemplateは文字列型のため、このようにVTLのソースはFn::Joinを使って1つの文字列の値にします。

    "FunctionConfiguration2": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "QuerySeatedDateTime",
        "DataSourceName": "SeatingHistoryTable",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set( $modelQueryExpression = {} )",
              "#if( !$util.isNull($ctx.args.seatedDate) )",
              " #set( $modelQueryExpression.expression = \"#seatedDate = :seatedDate\" )",
              " #set( $modelQueryExpression.expressionNames = {",
              " \"#seatedDate\": \"seatedDate\"",
              "} )",
              " #set( $modelQueryExpression.expressionValues = {",
              " \":seatedDate\": {",
              "     \"S\": \"$ctx.args.seatedDate\"",
              " }",
              "} )",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.beginsWith) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND begins_with(#sortKey, :sortKey)\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.beginsWith\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.between) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey BETWEEN :sortKey0 AND :sortKey1\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey0\", { \"S\": \"$ctx.args.seatedTime.between[0]\" }))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey1\", { \"S\": \"$ctx.args.seatedTime.between[1]\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.eq) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey = :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.eq\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.lt) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey < :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.lt\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.le) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey <= :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.le\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.gt) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey > :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.gt\" }))",
              "#end",
              "#if( !$util.isNull($ctx.args.seatedTime) && !$util.isNull($ctx.args.seatedTime.ge) )",
              " #set( $modelQueryExpression.expression = \"$modelQueryExpression.expression AND #sortKey >= :sortKey\" )",
              " $util.qr($modelQueryExpression.expressionNames.put(\"#sortKey\", \"seatedTime\"))",
              " $util.qr($modelQueryExpression.expressionValues.put(\":sortKey\", { \"S\": \"$ctx.args.seatedTime.ge\" }))",
              "#end",
              "#set( $limit = $util.defaultIfNull($context.args.limit, 100) )",
              "#set( $QueryRequest = {",
              " \"version\": \"2018-05-29\",",
              " \"operation\": \"Query\",",
              " \"limit\": $limit,",
              " \"query\": $modelQueryExpression,",
              " \"index\": \"bySeated\"",
              "} )",
              "#if( !$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == \"DESC\" )",
              "  #set( $QueryRequest.scanIndexForward = false )",
              "#else",
              "  #set( $QueryRequest.scanIndexForward = true )",
              "#end",
              "#if( $context.args.nextToken ) #set( $QueryRequest.nextToken = $context.args.nextToken ) #end",
              "#if( $context.args.filter ) #set( $QueryRequest.filter = $util.parseJson(\"$util.transform.toDynamoDBFilterExpression($ctx.args.filter)\") ) #end",
              "$util.toJson($QueryRequest)"
            ]
          ]
        },
        "ResponseMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#if($ctx.error)",
              " $util.error($ctx.error.message, $ctx.error.type)",
              "#end",
              "#set ($myMap = {})",
              "#foreach ($item in $ctx.result.items)",
              "  #if($myMap.containsKey($item.seatId) == false)",
              "     $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "  #if($myMap.get($item.seatId).unixTimeId < $item.unixTimeId)",
              "     $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "#end",
              "#foreach ($item in $ctx.prev.result.items)",
              "  #if($myMap.containsKey($item.seatId) == false)",
              "     $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "#end",
              "$util.toJson({",
              " \"items\": $myMap.values()",
              "})"
            ]
          ]
        }
      }
    }
  • 上記と同じように2つ目の関数を定義します。
"PipelineResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "TypeName": "Query",
        "FieldName": "querySeated",
        "Kind": "PIPELINE",
        "PipelineConfig": {
          "Functions": [
            {
              "Fn::GetAtt": [
                "FunctionConfiguration1",
                "FunctionId"
              ]
            },
            {
              "Fn::GetAtt": [
                "FunctionConfiguration2",
                "FunctionId"
              ]
            }
          ]
        },
        "RequestMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set($utcDate = $util.time.parseFormattedToEpochMilliSeconds(\"$ctx.args.seatedDate 00:00:00+0900\", \"yyyyMMdd HH:mm:ssZ\"))",
              "#set($utcPrevDate = $utcDate - 86400000)",
              "#set($prevDate = $util.time.epochMilliSecondsToFormatted($utcPrevDate, \"yyyyMMdd\", \"+09:00\"))",
              "$util.qr($ctx.stash.put(\"prevDate\", $prevDate))",
              "{}"
            ]
          ]
        },
        "ResponseMappingTemplate": {
          "Fn::Join": [
            "\n",
            [
              "#set($myMap = {})",
              "#foreach($item in $ctx.prev.result.items)",
              "  #if($item.userId)",
              "   $util.qr($myMap.put($item.seatId, $item))",
              "  #end",
              "#end",
              "$util.toJson({",
              "  \"items\": $myMap.values()",
              "})"
            ]
          ]
        }
      }
  • GraphQLリゾルバーを定義
    補足:上で定義した2つの関数をパイプラインリゾルバーにする、繋ぎの部分になります。
プロパティ名 説明 必須
ApiId このリゾルバーをアタッチするAWSAppSyncGraphQLAPI Yes 文字列
DataSourceName リゾルバーのデータソース名 Yes 文字列
FieldName リゾルバタイプ ※PIPELINE or UNITを指定 Yes 文字列
PipelineConfig パイプラインリゾルバーにリンクされた関数 No PipelineConfig
RequestMappingTemplate リクエストマッピングテンプレート No 文字列
ResponseMappingTemplate 応答マッピングテンプレート No 文字列

※この場合はRequestMappingTemplateがBeforeテンプレート・ResponseMappingTemplateがAfterテンプレートになります。

公式
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-resolver.html


仕組みはなんとなく理解したけど、amplify pushをしたときのエラーではCloudFormationの何が間違えているか分からない!という方もいらっしゃるのでは?

記述ミスに気付くには?

CloudFormationテンプレートの記述ミスに気付くには、CloudFormationのLinterがあります。

例えばこの部分のParametersの定義を消してみましょう
image.png

こうなりますね。
image.png

ではこの状態で
<<<<<< amplify push >>>>>>
image.png

エラーになったということは分かるのですが、どこのエラーなのか…もはやCloudFormation関連のエラーなのかすらも見当がつかない

<<<<<<  Linterだと  >>>>>>
image.png
ちゃんとAppSyncApiiIdがないよーと教えてくれますね。超便利!!これを知らなかったのでオカメは2日ほど訳の分からないエラーに苦しめられていましたね。

インストール方法や使い方はこちらを参考にしてください。

おまけ雑談

CloudFormationに辿り着くまでも、CloudFormationテンプレートでもめちゃめちゃ苦労しました。
何度もエラーを経験し、やっと動いたときには全米が泣いていました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0