AWS AppSync のリゾルバーのマッピングテンプレートプログラミングガイド
注記
現在、主に APPSYNC_JS ランタイムとそのドキュメントをサポートしています。こちらにある APPSYNC_JS ランタイムとそのガイドの使用をご検討ください。
これは、AWS AppSync での Apache Velocity Template Language (VTL) プログラミングに関するクックブックスタイルのチュートリアルです。JavaScript、C、Java など他のプログラミング言語に慣れている場合は、かなり容易に進められます。
AWS AppSync は VTL 使用してクライアントからの GraphQL リクエストを、データソースへのリクエストに変換します。その後、このプロセスを逆転して、データソースレスポンスを GraphQL レスポンスに変換します。VTL はロジカルなテンプレート言語で、ウェブアプリケーションの標準的なリクエスト/レスポンスフローで、次のような手法を利用した、リクエストとレスポンスの両方を操作する機能が用意されています。
-
新しい項目のデフォルト値
-
入力の検証とフォーマット
-
データ変換および整形
-
リスト、マップ、配列の値を取り出す、または変更するための繰り返し処理
-
ユーザー ID に基づくレスポンスのフィルタリング/変更
-
複合認可チェック
たとえば、GraphQL 引数でサービスの電話番号の検証を実行する、または DynamoDB に格納する前に入力パラメータを大文字に変換する場合を考えます。または、クライアントシステムで、GraphQL 引数、JWT トークンクレーム、または HTTP ヘッダーの一部としてコードを渡し、コードがリストの特定の文字列と一致する場合にのみ、データを返す場合を考えます。これらは、すべて AWS AppSync の VTL で行うことができる論理チェックです。
VTL では、使い慣れているプログラミング手法を使用してロジックを適用できます。ただし、標準的なリクエスト/レスポンスフロー内での実行に限られています。これにより、ユーザーベースが拡大しても、GraphQL API のスケーラビリティが確保されます。AWS AppSync はリゾルバーとして AWS Lambda もサポートするため、さらに高い柔軟性が必要な場合に、選択した言語 (Node.js、Python、Go、Java など) で Lambda 関数をいつでも利用できます。
セットアップ
プログラミング言語を学ぶ一般的な手法は、結果 (たとえば、JavaScript の console.log(variable)
) を出力し、何が起こるのかを確認することです。このチュートリアルでは、シンプルな GraphQL スキーマを作成し、Lambda 関数に値のマップを渡すことで、このデモを実行します。Lambda 関数で値を出力し、それらに応答します。これにより、リクエスト/レスポンスフローを理解し、さまざまなプログラミング手法を確認できます。
以下の GraphQL スキーマを作成することから開始します。
type Query { get(id: ID, meta: String): Thing } type Thing { id: ID! title: String! meta: String } schema { query: Query }
ここで言語として Node.js を使用して、次の AWS Lambda 関数を作成します。
exports.handler = (event, context, callback) => { console.log('VTL details: ', event); callback(null, event); };
AWS AppSync コンソールの [Data Sources (データソース)] ペインで、新しいデータソースとして、この Lambda 関数を追加します。コンソールの [スキーマ] ページに戻り、右側で、get(...):Thing
クエリの横にある [アタッチ] ボタンをクリックします。リクエストテンプレートでは、[Invoke and forward arguments (引数の呼び出しと転送)] メニューから既存のテンプレートを選択します。レスポンステンプレートでは、[Return Lambda result (Lambda 関数の結果を返す)] を選択します。
Lambda 関数の 1 か所から Amazon CloudWatch Logs を開き、AWS AppSync コンソールの [クエリ] タブから、以下の GraphQL クエリを実行します。
query test { get(id:123 meta:"testing"){ id meta } }
GraphQL のレスポンスには、id:123
と meta:testing
が含まれます。Lambda 関数がエコーバックするためです。数秒後、CloudWatch Logs でこれらの詳細とともにレコードが表示されます。
変数
VTL では参照$
記号が付き、#set
ディレクティブで作成されます。
#set($var = "a string")
変数は、他の言語で使い慣れた、同様の型 (数値、文字列、配列、リスト、マップなど) に保存します。Lambda リゾルバーのデフォルトリクエストテンプレートで JSON ペイロードが送信されていることがわかります。
"payload": $util.toJson($context.arguments)
ここでは、次の 2 点に注意してください。まず、AWS AppSync には共通オペレーションに対する便利な関数がいくつか用意されています。この例では、$util.toJson
は変数を JSON に変換します。次に、変数 $context.arguments
は GraphQL リクエストからマップオブジェクトとして自動的に入力されます。新しいマップを次のように作成できます。
#set( $myMap = { "id": $context.arguments.id, "meta": "stuff", "upperMeta" : $context.arguments.meta.toUpperCase() } )
$myMap
という変数が作成されました。これには id
、meta
、および upperMeta
のキーがあります。これは、以下の特徴も示しています。
-
id
には、GraphQL 引数からキーが入力されます。これは VTL で一般的で、クライアントから引数を取得するためのものです。 -
meta
は、値がハードコードされ、デフォルト値を示します。 -
upperMeta
はmeta
引数を.toUpperCase()
メソッドを使用して変換します。
リクエストテンプレートの先頭に以前のコードを配置して、payload
を変更して、新しい $myMap
変数を使用するようにします。
"payload": $util.toJson($myMap)
Lambda 関数を実行し、CloudWatch のログのデータとともに、レスポンスの変更を確認できます。このチュートリアルの残りの部分を実行するので、同様のテストを実行できるように $myMap
の入力を保持します。
変数に properties_ を設定することもできます。これらは単純な文字列、配列、または JSON です。
#set($myMap.myProperty = "ABC") #set($myMap.arrProperty = ["Write", "Some", "GraphQL"]) #set($myMap.jsonProperty = { "AppSync" : "Offline and Realtime", "Cognito" : "AuthN and AuthZ" })
静的参照
VTL がテンプレート作成言語であるため、デフォルトでは、そこで使用するすべての参照は .toString()
を実行します。参照が未定義の場合、実際の参照表現を、文字列として出力します。例:
#set($myValue = 5) ##Prints '5' $myValue ##Prints '$somethingelse' $somethingelse
これに対応するために、VTL には「静的参照」、または「サイレント参照」構文があり、この動作を抑制するかどうかをテンプレートエンジンに指示します。この構文は $!{}
です。たとえば、前のコードで $!{somethingelse}
を使用してわずかに変更した場合、印刷は抑制されます。
#set($myValue = 5) ##Prints '5' $myValue ##Nothing prints out $!{somethingelse}
メソッドの呼び出し
前の例では、変数を作成し、同時に値を設定する方法を説明しました。次に示すように、データをマップに追加して、これを 2 つのステップで実行することもできます。
#set ($myMap = {}) #set ($myList = []) ##Nothing prints out $!{myMap.put("id", "first value")} ##Prints "first value" $!{myMap.put("id", "another value")} ##Prints true $!{myList.add("something")}
ただし、この動作について留意すべき点があります。静的参照表記 $!{}
で、上記のようにメソッドを呼び出すことができますが、実行されたメソッドの戻り値は抑制されません。そのため ##Prints "first value"
と ##Prints true
が追加されています。キーがすでに存在している場合に、値を挿入するなど、リストやマップで反復処理をしているときに、エラーが発生する場合があります。出力で評価時に予期しない文字列がテンプレートに追加されるためです。
これに対する回避策は、#set
ディレクティブを使用してメソッドを呼び出し、変数を無視するとうまくいくことがあります。例:
#set ($myMap = {}) #set($discard = $myMap.put("id", "first value"))
テンプレートでこの手法を使用することができます。予期しない文字列がテンプレートに出力されるのを避けるためです。AWSAppSync では、シンプルな表記で同じ動作をする便利な代替関数が用意されています。これにより、これらの実装の詳細を検討する必要がなくなります。$util.quiet()
またはそのエイリアス $util.qr()
でこの関数にアクセスできます。例:
#set ($myMap = {}) #set ($myList = []) ##Nothing prints out $util.quiet($myMap.put("id", "first value")) ##Nothing prints out $util.qr($myList.add("something"))
文字列
多くのプログラミング言語の場合と同じように、文字列は、特に変数から構築する場合に対処が困難なことがあります。VTL で発生する共通の事項がいくつかあります。
DynamoDB のようなデータソースに文字列としてデータを挿入する場合、GraphQL 引数などの変数から入力されます。文字列は、二重引用符で囲まれ、文字列で変数を参照するために必要なのは "${}"
だけです (静的参照表記!
がない場合)。これは JavaScript のテンプレートリテラル (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
#set($firstname = "Jeff") $!{myMap.put("Firstname", "${firstname}")}
GraphQL クライアントから引数を使用するときの "author": { "S" :
"${context.arguments.author}"}
、または "id" : { "S" : "$util.autoId()"}
のような ID 自動生成などの DynamoDB リクエストテンプレートでこれを確認できます。つまり、データを入力するため、文字列の中でメソッドの結果または変数を参照できるということです。
部分文字列を取り出すなど、Java String クラス
#set($bigstring = "This is a long string, I want to pull out everything after the comma") #set ($comma = $bigstring.indexOf(',')) #set ($comma = $comma +2) #set ($substring = $bigstring.substring($comma)) $util.qr($myMap.put("substring", "${substring}"))
文字列連結も非常に一般的なタスクです。これを行うには、変数参照を単独で、または静的な値とともに使用します。
#set($s1 = "Hello") #set($s2 = " World") $util.qr($myMap.put("concat","$s1$s2")) $util.qr($myMap.put("concat2","Second $s1 World"))
loop
変数を作成して、メソッドを呼び出したので、ロジックをコードに追加できます。他の言語とは異なり、VTL では反復回数が事前に定義されている場合にのみ、ループが許可されます。Velocity には do..while
はありません。この設計により、評価プロセスは常に確実に終了します。これは GraphQL オペレーションを実行するときに、スケーラビリティのための境界になります。
ループは #foreach
で作成され、ループ変数や、配列、リスト、マップ、コレクションなど反復可能オブジェクトの入力が必要です。#foreach
ループの典型的なプログラミング例では、コレクションの項目をループし、出力します。このケースでは、それらを取り出し、マップに追加します。
#set($start = 0) #set($end = 5) #set($range = [$start..$end]) #foreach($i in $range) ##$util.qr($myMap.put($i, "abc")) ##$util.qr($myMap.put($i, $i.toString()+"foo")) ##Concat variable with string $util.qr($myMap.put($i, "${i}foo")) ##Reference a variable in a string with "${varname}" #end
この例では、いくつかの注意点を示します。まず、範囲 [..]
演算子で変数を使用して、反復可能オブジェクトを作成します。次に、各項目は、操作できる $i
変数によって参照されます。前の例では、コメントは、二重の ##
記号で示されます。これは、文字列を使用した、異なるメソッドの連結であるとともに、キー (値) の両方に、ループ変数を使用する例でもあります。
$i
は整数であることに注意してください。.toString()
メソッドを呼び出すことができます。GraphQL の INT タイプでは、これが役に立ちます。
範囲演算子を直接使用することもできます。次に例を示します。
#foreach($item in [1..5]) ... #end
配列
ここまで、マップを操作しましたが、配列も VTL で一般的です。配列では、.isEmpty()
、.size()
、.set()
、.get()
、.add()
などのいくつかの基本メソッドで、以下のようにアクセスできます。
#set($array = []) #set($idx = 0) ##adding elements $util.qr($array.add("element in array")) $util.qr($myMap.put("array", $array[$idx])) ##initialize array vals on create #set($arr2 = [42, "a string", 21, "test"]) $util.qr($myMap.put("arr2", $arr2[$idx])) $util.qr($myMap.put("isEmpty", $array.isEmpty())) ##isEmpty == false $util.qr($myMap.put("size", $array.size())) ##Get and set items in an array $util.qr($myMap.put("set", $array.set(0, 'changing array value'))) $util.qr($myMap.put("get", $array.get(0)))
前の例では配列インデックス表記を使用して arr2[$idx]
の要素を取得しました。同様に、マップ/ディクショナリから名前で参照することができます。
#set($result = { "Author" : "Nadia", "Topic" : "GraphQL" }) $util.qr($myMap.put("Author", $result["Author"]))
条件式を使用して、レスポンステンプレートでデータソースから返される結果をフィルタリングするときに、これがよく使われます。
条件チェック
前の #foreach
のセクションで、VTL でデータを変換するロジックを使用するいくつかの例を示しました。データを評価する条件チェックがランタイムにも適用されます。
#if(!$array.isEmpty()) $util.qr($myMap.put("ifCheck", "Array not empty")) #else $util.qr($myMap.put("ifCheck", "Your array is empty")) #end
上記のブール式の #if()
チェックは、問題ありませんが、分岐に演算子と #elseif()
を使用することもできます。
#if ($arr2.size() == 0) $util.qr($myMap.put("elseIfCheck", "You forgot to put anything into this array!")) #elseif ($arr2.size() == 1) $util.qr($myMap.put("elseIfCheck", "Good start but please add more stuff")) #else $util.qr($myMap.put("elseIfCheck", "Good job!")) #end
これらの 2 つの例は否定 (!) と一致 (==) を示しています。||、&&、>、<、>=、<=、および != を使用することもできます。
#set($T = true) #set($F = false) #if ($T || $F) $util.qr($myMap.put("OR", "TRUE")) #end #if ($T && $F) $util.qr($myMap.put("AND", "TRUE")) #end
注意 : Boolean.FALSE
と null
だけが条件で false と見なされます。ゼロ (0) と空の文字列 ("") は false に該当しません。
演算子
何らかの演算処理を実行する演算子がないと、プログラミング言語は完全とはいえません。手始めにいくつか例を紹介します。
#set($x = 5) #set($y = 7) #set($z = $x + $y) #set($x-y = $x - $y) #set($xy = $x * $y) #set($xDIVy = $x / $y) #set($xMODy = $x % $y) $util.qr($myMap.put("z", $z)) $util.qr($myMap.put("x-y", $x-y)) $util.qr($myMap.put("x*y", $xy)) $util.qr($myMap.put("x/y", $xDIVy)) $util.qr($myMap.put("x|y", $xMODy))
ループと条件式を組み合わせて使用する
オブジェクトをループしアクションを実行する前にチェックを実行することは、データソースからの書き込みや読み込みの前など、VTL でデータを変換する際によく使われます。前のセクションのいくつかのツールを組み合わせることで、多くの機能が利用できます。1 つの便利な手法は #foreach
では各項目に .count
が自動的に使用できる点を理解することです。
#foreach ($item in $arr2) #set($idx = "item" + $foreach.count) $util.qr($myMap.put($idx, $item)) #end
たとえば、一定サイズ未満の値をマップから取り出す場合があります。#break
ステートメントで、条件とともにカウントを使用してこれを実行できます。
#set($hashmap = { "DynamoDB" : "https://aws.amazon.com/dynamodb/", "Amplify" : "https://github.com/aws/aws-amplify", "DynamoDB2" : "https://aws.amazon.com/dynamodb/", "Amplify2" : "https://github.com/aws/aws-amplify" }) #foreach ($key in $hashmap.keySet()) #if($foreach.count > 2) #break #end $util.qr($myMap.put($key, $hashmap.get($key))) #end
前の #foreach
はマップに対して使用できる .keySet()
で反復します。これにより、$key
を取得し .get($key)
で値を参照するためにアクセスできます。AWS AppSync でクライアントからの GraphQL 引数は、マップとして保存されます。また、.entrySet()
で反復処理を実行でき、Set としてのキーと値の両方にアクセスでき、入力の検証や変換などの複雑な条件チェックを実行するか、他の変数を入力できます。
#foreach( $entry in $context.arguments.entrySet() ) #if ($entry.key == "XYZ" && $entry.value == "BAD") #set($myvar = "...") #else #break #end #end
他の一般的な例は、データを同期するときの初期オブジェクトバージョンなど、デフォルト情報の自動入力です (競合の解決に非常に重要)。または認可チェック用のオブジェクトのデフォルト所有者もあります。Mary がこのブログ記事を作成した場合は以下のようになります。
#set($myMap.owner ="Mary") #set($myMap.defaultOwners = ["Admins", "Editors"])
Context
AWS AppSync リゾルバーと VTL での論理チェック実行に慣れてきたところで、コンテキストオブジェクトを確認します。
$util.qr($myMap.put("context", $context))
これには、GraphQL リクエストの、アクセスできるすべての情報が含まれます。詳細な説明については、コンテキストリファレンスを参照してください。
フィルタリング
これまでチュートリアルで、Lambda 関数からのすべての情報が、非常にシンプルな JSON 変換と GraphQL クエリに返されます。
$util.toJson($context.result)
VTL ロジックは、データソースからレスポンスを取得するとき、特にリソースで許可チェックを実行するときに強力です。いくつかの例を見てみましょう。最初に、次のようにレスポンステンプレートの変更を試します。
#set($data = { "id" : "456", "meta" : "Valid Response" }) $util.toJson($data)
GraphQL オペレーションで何が起こるかにかかわらず、ハードコードされた値がクライアントに返されます。meta
フィールドに Lambda レスポンスから値が設定されるように少し変更し、条件式についての前のチュートリアルのように、elseIfCheck
値に設定します。
#set($data = { "id" : "456" }) #foreach($item in $context.result.entrySet()) #if($item.key == "elseIfCheck") $util.qr($data.put("meta", $item.value)) #end #end $util.toJson($data)
$context.result
はマップで、キーまたは値のいずれかで返されるロジックを実行するために entrySet()
を使用できます。GraphQL オペレーションを実行したユーザーに関する情報が $context.identity
に含まれているため、データソースから認可情報を返す場合は、ロジックに基づいて、ユーザーに返すデータ (すべて、一部、なし) を決定できます。次のようにレスポンステンプレートを変更します。
#if($context.result["id"] == 123) $util.toJson($context.result) #else $util.unauthorized() #end
GraphQL クエリを実行した場合、データがノーマルとして返されます。ただし、id 引数を 123 以外のものに変更した場合 (query test { get(id:456
meta:"badrequest"){} }
)、認可失敗のメッセージが表示されます。
認可ユースケースセクションに他の認可シナリオがあります。
テンプレートサンプル
このチュートリアルを実行していくと、ステップごとに拡張されて、このテンプレートになります。このテンプレートがまだない場合は、以下のテンプレートをテスト用にコピーします。
リクエストテンプレート
#set( $myMap = { "id": $context.arguments.id, "meta": "stuff", "upperMeta" : "$context.arguments.meta.toUpperCase()" } ) ##This is how you would do it in two steps with a "quiet reference" and you can use it for invoking methods, such as .put() to add items to a Map #set ($myMap2 = {}) $util.qr($myMap2.put("id", "first value")) ## Properties are created with a dot notation #set($myMap.myProperty = "ABC") #set($myMap.arrProperty = ["Write", "Some", "GraphQL"]) #set($myMap.jsonProperty = { "AppSync" : "Offline and Realtime", "Cognito" : "AuthN and AuthZ" }) ##When you are inside a string and just have ${} without ! it means stuff inside curly braces are a reference #set($firstname = "Jeff") $util.qr($myMap.put("Firstname", "${firstname}")) #set($bigstring = "This is a long string, I want to pull out everything after the comma") #set ($comma = $bigstring.indexOf(',')) #set ($comma = $comma +2) #set ($substring = $bigstring.substring($comma)) $util.qr($myMap.put("substring", "${substring}")) ##Classic for-each loop over N items: #set($start = 0) #set($end = 5) #set($range = [$start..$end]) #foreach($i in $range) ##Can also use range operator directly like #foreach($item in [1...5]) ##$util.qr($myMap.put($i, "abc")) ##$util.qr($myMap.put($i, $i.toString()+"foo")) ##Concat variable with string $util.qr($myMap.put($i, "${i}foo")) ##Reference a variable in a string with "${varname)" #end ##Operators don't work #set($x = 5) #set($y = 7) #set($z = $x + $y) #set($x-y = $x - $y) #set($xy = $x * $y) #set($xDIVy = $x / $y) #set($xMODy = $x % $y) $util.qr($myMap.put("z", $z)) $util.qr($myMap.put("x-y", $x-y)) $util.qr($myMap.put("x*y", $xy)) $util.qr($myMap.put("x/y", $xDIVy)) $util.qr($myMap.put("x|y", $xMODy)) ##arrays #set($array = ["first"]) #set($idx = 0) $util.qr($myMap.put("array", $array[$idx])) ##initialize array vals on create #set($arr2 = [42, "a string", 21, "test"]) $util.qr($myMap.put("arr2", $arr2[$idx])) $util.qr($myMap.put("isEmpty", $array.isEmpty())) ##Returns false $util.qr($myMap.put("size", $array.size())) ##Get and set items in an array $util.qr($myMap.put("set", $array.set(0, 'changing array value'))) $util.qr($myMap.put("get", $array.get(0))) ##Lookup by name from a Map/dictionary in a similar way: #set($result = { "Author" : "Nadia", "Topic" : "GraphQL" }) $util.qr($myMap.put("Author", $result["Author"])) ##Conditional examples #if(!$array.isEmpty()) $util.qr($myMap.put("ifCheck", "Array not empty")) #else $util.qr($myMap.put("ifCheck", "Your array is empty")) #end #if ($arr2.size() == 0) $util.qr($myMap.put("elseIfCheck", "You forgot to put anything into this array!")) #elseif ($arr2.size() == 1) $util.qr($myMap.put("elseIfCheck", "Good start but please add more stuff")) #else $util.qr($myMap.put("elseIfCheck", "Good job!")) #end ##Above showed negation(!) and equality (==), we can also use OR, AND, >, <, >=, <=, and != #set($T = true) #set($F = false) #if ($T || $F) $util.qr($myMap.put("OR", "TRUE")) #end #if ($T && $F) $util.qr($myMap.put("AND", "TRUE")) #end ##Using the foreach loop counter - $foreach.count #foreach ($item in $arr2) #set($idx = "item" + $foreach.count) $util.qr($myMap.put($idx, $item)) #end ##Using a Map and plucking out keys/vals #set($hashmap = { "DynamoDB" : "https://aws.amazon.com/dynamodb/", "Amplify" : "https://github.com/aws/aws-amplify", "DynamoDB2" : "https://aws.amazon.com/dynamodb/", "Amplify2" : "https://github.com/aws/aws-amplify" }) #foreach ($key in $hashmap.keySet()) #if($foreach.count > 2) #break #end $util.qr($myMap.put($key, $hashmap.get($key))) #end ##concatenate strings #set($s1 = "Hello") #set($s2 = " World") $util.qr($myMap.put("concat","$s1$s2")) $util.qr($myMap.put("concat2","Second $s1 World")) $util.qr($myMap.put("context", $context)) { "version" : "2017-02-28", "operation": "Invoke", "payload": $util.toJson($myMap) }
レスポンステンプレート
#set($data = { "id" : "456" }) #foreach($item in $context.result.entrySet()) ##$context.result is a MAP so we use entrySet() #if($item.key == "ifCheck") $util.qr($data.put("meta", "$item.value")) #end #end ##Uncomment this out if you want to test and remove the below #if check ##$util.toJson($data) #if($context.result["id"] == 123) $util.toJson($context.result) #else $util.unauthorized() #end