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

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

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:123meta:testing,因为 Lambda 函数将它们重复发送回来了。几秒钟后,您应该会在 CloudWatch 日志中看到一条包含这些详细信息的记录。

Variables

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

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 中一样)。这类似于 https://developer.mozilla 中的 JavaScript模板文字。 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) 引用值。来自中客户端的 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