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

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

解析器映射模板编程指南

注意

我们现在主要支持 APPSYNC_JS 运行时环境及其文档。请考虑使用 APPSYNC_JS 运行时环境和此处的指南。

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

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

  • 新项目的默认值

  • 输入验证和格式化

  • 转换数据和设置数据形状

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

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

  • 复杂的授权检查

例如,您可能希望在该服务中对 GraphQL 参数执行电话号码验证,或者在将输入参数存储到 DynamoDB 之前将其转换为大写。或者您可能希望客户端系统提供一个代码,作为 GraphQL 参数、JWT 令牌声明或 HTTP 标头的一部分,并且仅在该代码与列表中的特定字符串匹配时才利用数据做出响应。这些都是您可以在 Amazon AppSync 中使用 VTL 执行的逻辑检查。

您可以通过 VTL 使用可能已很熟悉的编程技术应用逻辑。但是,它仅限于在标准请求/响应流程之内运行,以确保您的 GraphQL API 可随用户群的增长而扩展。由于 Amazon AppSync 还支持将 Amazon 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 语言创建以下 Amazon Lambda 函数:

exports.handler = (event, context, callback) => { console.log('VTL details: ', event); callback(null, event); };

在 Amazon AppSync 控制台的数据源窗格中,将该 Lambda 函数添加为新数据源。导航回控制台的架构页面,然后单击右侧的 get(...):Thing 查询旁边的附加按钮。对于请求模板,从 Invoke and forward arguments (调用并转发参数) 菜单中选择现有模板。对于响应模板,选择 Return Lambda result (返回 Lambda 结果)

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

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

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

Variables

VTL 使用引用存储或处理数据。VTL 中有三类引用:变量、属性和方法。变量前面有一个 $ 符号,由 #set 指令创建:

#set($var = "a string")

变量存储的类型与您熟悉的其他语言类似,例如数字、字符串、数组、列表和映射。您可能已经注意到了,在 Lambda 解析器的默认请求模板中发送了 JSON 负载:

"payload": $util.toJson($context.arguments)

此处需要注意一些事项 - 首先,Amazon 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"))

您可以在模板中使用该技术,因为它可以防止在模板中输出意外的字符串。AmazonAppSync 提供了另一种便利函数,它以更简洁的表示法提供相同的行为。使用这一函数不必考虑这些具体的实施规范。您可以通过 $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 中一样)。这与 JavaScript 中的模板文本类似:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

#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.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) 引用值。在 Amazon 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") #set($myMap.defaultOwners = ["Admins", "Editors"])

上下文

您现在充分了解了如何在 Amazon 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