

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

# AWS AppSync 解析器映射模板编程指南
<a name="resolver-mapping-template-reference-programming-guide"></a>

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-js-version.html)的指南。

这是一本食谱式的编程教程，里面有 Apache Velocity 模板语言 (VTL)。 AWS AppSync如果您熟悉其他编程语言 JavaScript，例如 C 或 Java，则应该相当简单。

AWS AppSync 使用 VTL 将来自客户端的 GraphQL 请求转换为对您的数据源的请求。然后，它将这一过程反转，将数据来源响应转换回 GraphQL 响应。VTL 是一种逻辑模板语言，它使您能够使用以下技术在 Web 应用程序的标准 request/response 流程中同时操作请求和响应：
+ 新项目的默认值
+ 输入验证和格式化
+ 转换数据和设置数据形状
+ 遍历列表、映射和数组，从而提取值或更改值
+ 根据用户身份筛选/更改响应
+ 复杂的授权检查

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

您可以通过 VTL 使用可能已很熟悉的编程技术应用逻辑。但是，它只能在标准 request/response 流程中运行，以确保您的GraphQL API可以随着用户群的增长而扩展。由于 AWS AppSync 还支持 AWS Lambda 作为解析器，所以如果你需要更大的灵活性，你可以用你选择的编程语言（Node.js、Python、Go、Java 等）编写 Lambda 函数。

## 设置
<a name="setup"></a>

学习语言时的一种常用技巧是打印出结果（例如，`console.log(variable)`在 JavaScript），看看会发生什么。在本教程中，我们将演示这一方法：创建一个简单的 GraphQL 架构，并将值的映射传递到 Lambda 函数中。Lambda 函数会输出这些值，并用这些值进行响应。这将使您能够了解 request/response 流程并了解不同的编程技术。

首先要创建以下 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` 查询旁边的**附加**按钮。对于请求模板，从 **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 日志中看到一条包含这些详细信息的记录。

## 变量
<a name="variables"></a>

VTL 使用[引用](https://velocity.apache.org/engine/1.7/user-guide.html#references)存储或处理数据。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\$1*。它们可以是简单的字符串、数组或 JSON：

```
#set($myMap.myProperty = "ABC")
#set($myMap.arrProperty = ["Write", "Some", "GraphQL"])
#set($myMap.jsonProperty = {
    "AppSync" : "Offline and Realtime",
    "Cognito" : "AuthN and AuthZ"
})
```

### 无提示引用
<a name="quiet-references"></a>

由于 VTL 是一种模板化的语言，默认情况下，您进行的每次引用都会执行 `.toString()`。如果未定义引用，它会以字符串输出实际的引用表示形式。例如：

```
#set($myValue = 5)
##Prints '5'
$myValue

##Prints '$somethingelse'
$somethingelse
```

为了应对这一问题，VTL 有一种*无提示引用* 或*静默引用* 语法，告知模板引擎禁止此行为。该语法为 `$!{}`。例如，如果我们稍微改动一下之前的代码，使用 `$!{somethingelse}`，输出就会禁止：

```
#set($myValue = 5)
##Prints '5'
$myValue

##Nothing prints out
$!{somethingelse}
```

## 调用方法
<a name="calling-methods"></a>

在之前的示例中，我们向您演示了如何在创建变量的同时设置值。您还可以通过两个步骤在映射中添加数据，实现相同的目的，如下所示：

```
#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"))
```

## 字符串
<a name="strings"></a>

与许多编程语言一样，字符串有时可能较难处理，特别是当您希望通过变量生成字符串，情况更是如此。VTL 包含了许多处理字符串的常用功能。

假设您将数据作为字符串插入到 DynamoDB 等数据来源中，但它是通过变量（如 GraphQL 参数）填充的。字符串具有双引号，要在字符串中引用变量，您只需使用 `"${}"`（没有 `!`，就像 [quiet reference notation](https://velocity.apache.org/engine/1.7/user-guide.html#quiet-reference-notation) 中一样）。这类似于 [https://developer.mozilla 中的 JavaScript模板文字。 org/en-US/docs/Web/JavaScript/Reference/Template\$1literals](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](https://docs.oracle.com/javase/6/docs/api/java/lang/String.html) 的公共方法，例如提取子字符串：

```
#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
<a name="loops"></a>

您已了解了如何创建变量及调用方法，现在可以在代码中添加一些逻辑了。与其他语言不同，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
```

## 数组
<a name="arrays"></a>

目前，您已经可以处理映射了，但在 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]` 的元素。你可以用类似的方式从 a Map/dictionary 中按名字查找：

```
#set($result = {
    "Author" : "Nadia",
    "Topic" : "GraphQL"
})

$util.qr($myMap.put("Author", $result["Author"]))
```

如果使用条件在响应模板中筛选数据来源中的结果，这种方法非常常用。

## 条件检查
<a name="conditional-checks"></a>

之前介绍 `#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
```

这两个示例展示了否定 (\$1) 和相等 (==)。我们还可以使用 \$1\$1、&&、>、<、>=、<= 和 \$1=。

```
#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。

## 运算符
<a name="operators"></a>

如果没有一些执行数学运算的运算符，那么编程语言就不完整。以下是一些入门示例：

```
#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))
```

### 结合使用循环和条件
<a name="loops-and-conditionals-together"></a>

在 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"])
```

## 上下文
<a name="context"></a>

您现在充分了解了如何在 AWS AppSync 解析器中使用 VTL 执行逻辑检查，让我们了解一下上下文对象：

```
$util.qr($myMap.put("context", $context))
```

其中包含您可以在 GraphQL 请求中访问的所有信息。有关详细解释，请参阅[上下文参考](resolver-context-reference.md#aws-appsync-resolver-mapping-template-context-reference)。

## 筛选
<a name="filtering"></a>

在此教程中，目前来自 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"){} }`)，将收到授权失败的消息。

您可以在[授权使用案例](security-authorization-use-cases.md#aws-appsync-security-authorization-use-cases)部分找到有关授权场景的更多示例。

### 模板示例
<a name="appendix-template-sample"></a>

如果您按照此教程的步骤执行，那么可能已逐步构建了此模板。如果您还没有这么做，我们将在下面提供模板，供您复制用于测试。

 **请求模板** 

```
#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
```