本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
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 日志,然后从 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。第二,变量 $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}
调用方法
在之前的示例中,我们向您演示了如何在创建变量的同时设置值。您还可以通过两个步骤在映射中添加数据,实现相同的目的,如下所示:
#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"))
字符串
与许多编程语言一样,字符串有时可能较难处理,特别是当您希望通过变量生成字符串,情况更是如此。有一些常见的东西会想出来VTL。
假设您将数据作为字符串插入到 DynamoDB 等数据来源中,但它是通过变量(如 GraphQL 参数)填充的。字符串具有双引号,要在字符串中引用变量,您只需使用 "${}"
(没有 !
,就像 quiet reference notation
#set($firstname = "Jeff") $!{myMap.put("Firstname", "${firstname}")}
您可以在 DynamoDB 请求模板中看到这种用法,例如,在使用来自 GraphQL 客户端的参数时的 "author": { "S" :
"${context.arguments.author}"}
,或者自动生成 ID 时的 "id" : { "S" : "$util.autoId()"}
。这就意味着您可以引用变量,或方法的结果,在字符串内部填充数据。
您还可以使用 Java String class
#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"))
Loops
您已了解了如何创建变量及调用方法,现在可以在代码中添加一些逻辑了。与其他语言不同,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
这两个示例展示了否定 (!) 和相等 (==)。我们还可以使用 ||、&&、>、<、>=、<= 和 !=。
#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()
循环访问这些参数,可通过它将键和值作为集进行访问,从而填充其他变量或执行复杂的条件检查,例如验证或转换输入:
#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"])
上下文
既然你已经更熟悉了在 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