解析程序映射模板编程指南 - AWS AppSync
AWS 文档中描述的 AWS 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 AWS 服务入门

解析程序映射模板编程指南

此说明书风格的教程介绍如何在 AWS AppSync 中利用 Apache Velocity 模板语言 (VTL) 进行编程。如果您熟悉 JavaScript、C 或 Java 等其他编程语言,可能会比较容易理解本书内容。

AWS AppSync 使用 VTL 将来自客户端的 GraphQL 请求转换为对您的数据源的请求。然后,它将这一过程反转,将数据源响应转换回 GraphQL 响应。VTL 是一种逻辑模板语言,使您能够处理 Web 应用程序的标准请求/响应流程中的请求和响应,它用到的技术包括:

  • 新项目的默认值

  • 输入验证和格式化

  • 数据转换和整形

  • 遍历列表、映射和数组,从而提取值或更改值

  • 根据用户身份筛选/更改响应

  • 复杂的授权检查

例如,您可能想要针对服务中的 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 函数添加为新数据源。导航回到控制台的 Schema (架构) 页面,单击右侧 get(...):Thing 查询旁的 ATTACH (附加) 按钮。对于请求模板,从 Invoke and forward arguments (调用并转发参数) 菜单中选择现有模板。对于响应模板,选择 Return Lambda result (返回 Lambda 结果)

在一个位置打开 Lambda 函数的 Amazon CloudWatch Logs,然后在 AWS AppSync 控制台的 Queries (查询) 选项卡中运行以下 GraphQL 查询:

query test { get(id:123 meta:"testing"){ id meta } }

GraphQL 响应应包含 id:123meta:testing,因为 Lambda 函数将它们重复发送回来了。几秒钟后,您应在 CloudWatch Logs 中看到一条记录,包含如下详细信息。

变量

VTL 使用引用存储或处理数据。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 的变量,它的键有 idmetaupperMeta。这一操作也说明了几件事:

  • 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")}

但是对于这种行为,您需要了解以下内容。虽然您可使用无提示引用表示法 $!{} 调用方法(如上所示),但它不会禁止所执行方法的返回值。因此在以上示例中我们会标注 ##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 参数)填充的。字符串有双引号标记,而要在字符串的引用变量,您只需要 "${}"(在无提示引用表示法中没有 !)。这与 JavaScript 中的模板文本类似:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

#set($firstname = "Jeff") $!{myMap.put("Firstname", "${firstname}")}

您可以在 DynamoDB 请求模板中看到上述用法,如通过 "author": { "S" : "${context.arguments.author}"} 使用来自 GraphQL 客户端的参数;或通过 "id" : { "S" : "$utils.autoId()"} 自动生成 ID。这就意味着您可以引用变量,或方法的结果,在字符串内部填充数据。

您还可以使用 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)))

以上示例使用数组索引表示法检索包含 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.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) 引用值。在 AWS AppSync 中,来自客户端的 GraphQL 参数存储为映射。也可以通过 .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") and default ownership #set($myMap.defaultOwners = ["Admins", "Editors"]``

上下文

您已较为熟悉如何在 AWS 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) $utils.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 ##Operatorsdoesn'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) $utils.toJson($context.result); #else $util.unauthorized() #end