はじめに
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「アタッチ」でできるじゃーんと思ったあなた、正解です。
簡単じゃーんと思ったあなた、まだ早いです。今回はこんな問題があったのです。
手動設定したときの問題
バックエンドの環境はすべてファイル管理しており、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 文字列
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テンプレートになります。
仕組みはなんとなく理解したけど、amplify pushをしたときのエラーではCloudFormationの何が間違えているか分からない!という方もいらっしゃるのでは?
記述ミスに気付くには?
CloudFormationテンプレートの記述ミスに気付くには、CloudFormationのLinterがあります。
例えばこの部分のParametersの定義を消してみましょう
ではこの状態で
<<<<<< amplify push >>>>>>
エラーになったということは分かるのですが、どこのエラーなのか…もはやCloudFormation関連のエラーなのかすらも見当がつかない
<<<<<< Linterだと >>>>>>
ちゃんとAppSyncApiiIdがないよーと教えてくれますね。超便利!!これを知らなかったのでオカメは2日ほど訳の分からないエラーに苦しめられていましたね。
インストール方法や使い方はこちらを参考にしてください。
おまけ雑談
CloudFormationに辿り着くまでも、CloudFormationテンプレートでもめちゃめちゃ苦労しました。
何度もエラーを経験し、やっと動いたときには全米が泣いていました。