解析器映射模板編程指南 - AWS AppSync

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

解析器映射模板編程指南

注意

我們現在主要支援 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:123meta:testing,因為 Lambda 函式會參考這兩者。幾秒鐘後,您應該會在記錄 CloudWatch 檔中看到包含這些詳細資料的記錄。

Variables

VTL使用參考,您可以使用它來儲存或操作資料。中有三種類型的引用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 的變數,其擁有 idmeta 以及 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 引數。一個字符串將有雙引號,並且只需要引用字符串中的變量"${}"(所以不要!安靜的參考符號那樣)。這與以下內容中的模板文字類似 JavaScript:https://developer.mozilla.org/en-US/docs/Web/ JavaScript /參考文字/ 模板文字

#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.FALSEnull 會被視為 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