本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。
解析器映射模板編程指南
注意
我們現在主要支援 APPSYNC _JS 執行階段及其文件。請考慮在此處使用 APPSYNC _JS 運行時及其指南。
這是一個使用 Apache 速度模板語言(VTL)編程的烹飪書式教程。 AWS AppSync如果您熟悉其他編程語言(例如 JavaScript C 或 Java),則應該非常簡單。
AWS AppSync 用來將VTL來自用戶端的 GraphQL 要求轉換為資料來源的要求。然後,它會反轉程序,來將資料來源轉換回 GraphQL 回應。VTL是一種邏輯模板語言,使您能夠使用以下技術在 Web 應用程序的標準請求/響應流中操作請求和響應:
-
新項目的預設值
-
輸入驗證和格式化
-
轉換與打造資料
-
逐一查看清單、映射和陣列以移出或更改值
-
根據使用者身分篩選條件/變更回應
-
複雜的授權檢查
例如,您可能想要在 GraphQL 引數上在服務中執行電話號碼驗證,或將輸入參數轉換為大寫,然後再將其儲存在 DynamoDB 中。或者,也許您希望客戶端系統提供代碼,作為 GraphQL 參數,JWT令牌聲明或HTTP標題的一部分,並且僅在代碼與列表中的特定字符串匹配時才使用數據進行響應。這些都是您可以VTL在中執行的邏輯檢查 AWS AppSync。
VTL允許您使用可能熟悉的編程技術應用邏輯。但是,它必須在標準請求/響應流程中運行,以確保 GraphQL API 可以隨著用戶群的增長而擴展。由於 AWS AppSync 還支持 AWS Lambda 作為解析器,因此如果您需要更多靈活性,可以使用您選擇的編程語言(Node.js,Python,Go,Java 等)編寫 Lambda 函數。
設定
學習語言時的常用技巧是打印出結果(例如,console.log(variable)
in JavaScript)以查看會發生什麼。在本教學中,我們透過建立簡單的 GraphQL 結構描述並將對應值傳遞到 Lambda 函式來示範此技巧。Lambda 函式會列印出值,然後加以回應。這可讓您了解請求/回應流程,並查看不同的程式設計技巧。
開始建立以下 GraphQL 結構描述:
type Query { get(id: ID, meta: String): Thing } type Thing { id: ID! title: String! meta: String } schema { query: Query }
現在創建以下 AWS Lambda 函數,使用 Node.js 作為語言:
exports.handler = (event, context, callback) => { console.log('VTL details: ', event); callback(null, event); };
在 AWS AppSync 主控台的 [資料來源] 窗格中,將此 Lambda 函數新增為新資料來源。瀏覽回主控台的「結構描述」頁面,然後ATTACH按一下get(...):Thing
查詢旁邊右側的按鈕。如需請求範本,從 Invoke and forward arguments (叫用和轉送引數) 功能表中選擇現有的範本。如需回應範本,請選擇 Return Lambda result (傳回 Lambda 結果)。
在一個位置為您的 Lambda 函數開啟 Amazon CloudWatch 日誌,然後從 AWS AppSync 主控台的 [查詢] 索引標籤執行下列 GraphQL 查詢:
query test { get(id:123 meta:"testing"){ id meta } }
GraphQL 回應應包含 id:123
和 meta:testing
,因為 Lambda 函式會參考這兩者。幾秒鐘後,您應該會在記錄 CloudWatch 檔中看到包含這些詳細資料的記錄。
Variables
VTL使用參考$
符號且這些變數的建立是透過 #set
指令:
#set($var = "a string")
變數會使用其他語言存放您所熟悉的類似類型,例如數字、字串、陣列、清單和映射。您可能已經注意到 Lambda 解析器的默認請求模板中發送了有JSON效負載:
"payload": $util.toJson($context.arguments)
這裡需要注意的幾件事-首先,為常見操作 AWS AppSync 提供了幾個便利功能。在此範例中,$util.toJson
將變數轉換為JSON。其次,會自動從 GraphQL 請求填入變數 $context.arguments
做為映射物件。您可以建立新的映射,如下所示:
#set( $myMap = { "id": $context.arguments.id, "meta": "stuff", "upperMeta" : $context.arguments.meta.toUpperCase() } )
現在,您可以建立名為 $myMap
的變數,其擁有 id
、meta
以及 upperMeta
的金鑰。這也展現了以下幾點:
-
從 GraphQL 引數將金鑰填入
id
。這在從客戶端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}
呼叫方法
在之前的範例中,我們示範了如何建立變數並同時設定值。您也可以如下所示將資料新增到映射,以兩個步驟執行此操作:
#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")}
HOWEVER有一些關於這種行為的知識。雖然安靜參考表示法可讓您 $!{}
如以上方式呼叫方法,它將不會抑制執行方法的傳回值。這也是為什麼我們會說明以上的 ##Prints "first value"
及 ##Prints true
。您逐一查看映射或清單可能會導致錯誤,例如插入索引鍵已存在的值,因為輸出會在評估時將未預期的字串加入範本中。
此問題的解決方法有時會使用 #set
指令來呼叫方法並忽略變數。例如:
#set ($myMap = {}) #set($discard = $myMap.put("id", "first value"))
您可以在模板中使用此技術,因為它可以防止在模板中打印意外的字符串。 AWS AppSync 提供了一個替代的便利功能,以更簡潔的表示法提供相同的行為。這可讓您不需要考慮這些實作特性。您可以透過 $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"))
Strings
隨著使用多種程式設計語言,字串可能會變得難以處理,尤其是當您想要透過變數建構字串時。有一些常見的事情想出來VTL。
假設您要以字串形式將資料插入 DynamoDB 等資料來源,但是會從變數填入,例如 GraphQL 引數。一個字符串將有雙引號,並且只需要引用字符串中的變量"${}"
(所以不要!
像安靜的參考符號
#set($firstname = "Jeff") $!{myMap.put("Firstname", "${firstname}")}
您可以在 DynamoDB 要求範本中看到這一點,例如使用 GraphQL 用戶端的引數"author": { "S" :
"${context.arguments.author}"}
時,或是用於自動識別碼產生類似的。"id" : { "S" : "$util.autoId()"}
這表示您可以參考變數或字串內部的方法結果來填入資料。
您也可以使用 Java 字串類別
#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"))
迴圈
現在您已建立變數和呼叫方法,您可以將一些邏輯到新增到程式碼。與其他語言不同,只VTL允許循環,其中迭代次數是預定的。在速度中沒有 do..while
。此設計可確保評估處理一律終止,並提供 GraphQL 操作執行時的擴充界限。
迴圈是使用 #foreach
所建立並需要您輸入迴圈變數和 iterable 物件 (例如陣列、清單、映射或集合)。#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
此範例顯示一些考量。第一種是使用變數與範圍 [..]
運算子來建立 iterable 物件。然後,您可以操作的變數 $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]
。您可以透過類似的方式從 Map/字典中來查詢名稱:
#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
這兩個範例示範 negation(!) 和 equality (==)。我們也可以使用 ||、&&、>、<、>=、<= 和 !=。
#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從資料來源寫入或讀取資料之前) 在物件上迴圈,然後在執行動作之前執行檢查,這是很常見的。將之前區段的一些工具進行整合,讓您有更多功能可使用。一個便利的工具是知道 #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)
來參考該值。來自中用戶端的 GraphQL 引數 AWS AppSync 會儲存為對應。也可以透過 .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 操作進行何種操作,系統會將硬式編碼值傳回用戶端。稍微變更此項目,以從 Lambda 回應填入 meta
欄位,如需了解條件時則在教學課程中以 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()
來對金鑰或傳回的值執行邏輯。由於 $context.identity
包含使用者執行 GraphQL 操作的相關資訊,如果您從資料來源傳回授權資訊,則可以根據邏輯決定是否向使用傳回全部、部分資料或不傳回資料。將您的回應範本變更為如下所示:
#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