本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。
AWS AppSync 解析器映射範本程式設計指南
注意
我們現在主要支援 APPSYNC_JS 執行期及其文件。請考慮在此處使用 APPSYNC_JS 執行期及其指南。
這是使用 中的 Apache Velocity 範本語言 (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)
在 中 JavaScript),以查看發生的情況。在本教學中,我們透過建立簡單的 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 主控台的資料來源窗格中,將此 Lambda 函數新增為新的資料來源。返回主控台的結構描述頁面,然後按一下get(...):Thing
查詢旁邊的右側ATTACH按鈕。如需請求範本,從 Invoke and forward arguments (叫用和轉送引數) 功能表中選擇現有的範本。如需回應範本,請選擇 Return Lambda result (傳回 Lambda 結果)。
在單一位置開啟 Lambda 函數的 Amazon CloudWatch Logs,然後從 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 時,例如 "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 函數的所有資訊都已傳回至 GraphQL 查詢,轉換非常簡單JSON:
$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