教程:AWS Lambda 解析程序 - AWS AppSync
AWS 文档中描述的 AWS 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 AWS 服务入门

教程:AWS Lambda 解析程序

AWS AppSync 允许您使用 AWS Lambda 解析任何 GraphQL 字段。例如,GraphQL 查询可能会向 Amazon RDS 实例发送调用;GraphQL 更改可能会写入 Amazon Kinesis 流。在本节中,我们将介绍如何编写 Lambda 函数,根据 GraphQL 字段操作的调用执行业务逻辑。

创建 Lambda 函数

以下示例展示以 Node.js 编写的 Lambda 函数,它可针对博客文章执行不同的操作,作为博客应用程序示例的一部分。

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got an Invoke Request."); switch(event.field) { case "getPost": var id = event.arguments.id; callback(null, posts[id]); break; case "allPosts": var values = []; for(var d in posts){ values.push(posts[d]); } callback(null, values); break; case "addPost": // return the arguments back callback(null, event.arguments); break; case "addPostErrorWithData": var id = event.arguments.id; var result = posts[id]; // attached additional error information to the post result.errorMessage = 'Error with the mutation, data has changed'; result.errorType = 'MUTATION_ERROR'; callback(null, result); break; case "relatedPosts": var id = event.source.id; callback(null, relatedPosts[id]); break; default: callback("Unknown field, unable to resolve" + event.field, null); break; } };

这个 Lambda 函数可根据 ID 检索文章、添加文章、检索文章列表,并提取与已知文章相关的文章。

注意event.fieldswitch 语句允许 Lambda 函数确定当前正在解析的字段。

现在,让我们使用 AWS 管理控制台或利用 AWS CloudFormation 创建此 Lambda 函数,请选择以下内容:

aws cloudformation create-stack --stack-name AppSyncLambdaExample \ --template-url https://s3.us-west-2.amazonaws.com/awsappsync/resources/lambda/LambdaCFTemplate.yaml \ --capabilities CAPABILITY_NAMED_IAM

您可以在您的 AWS 账户中启动位于美国西部 2(俄勒冈)区域的此 AWS CloudFormation 堆栈:

为 AWS Lambda 配置数据源

创建 AWS Lambda 函数之后,在控制台中导航到 AWS AppSync GraphQL API,选择 Data Sources (数据源) 选项卡。

选择 New (新建),然后为数据源输入一个友好名称(例如,"Lambda"),然后在 Data source type (数据源类型) 中选择 AWS Lambda。选择适当的 AWS 区域,然后您应该看到您的 Lambda 函数列于此处。

选择 Lambda 函数之后,您可以创建新角色(AWS AppSync 为其分配适当的权限)或选择具有以下内联策略的现有角色:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": "arn:aws:lambda:REGION:ACCOUNTNUMBER:function/LAMBDA_FUNCTION" } ] }

您还需要为该角色设置与 AWS AppSync 的信任关系,如下所示:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }

创建 GraphQL 架构

现在数据源已连接到您的 Lambda 函数,让我们创建一个 GraphQL 架构。

在 AWS AppSync 控制台的架构编辑器中,确保您的架构与以下架构匹配:

schema { query: Query mutation: Mutation } type Query { getPost(id:ID!): Post allPosts: [Post] } type Mutation { addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int relatedPosts: [Post] }

配置解析程序

我们已注册了 AWS Lambda 数据源和一个有效的 GraphQL 架构,现在可以通过解析程序将 GraphQL 字段连接到 Lambda 数据源。

要创建解析程序,我们需要映射模板。要进一步了解映射模板,请阅读 AWS AppSync 映射模板概述解析程序映射模板概述

有关 Lambda 映射模板的更多信息,请参阅 Lambda 解析程序映射模板参考

在此步骤中,您将一个解析程序附加到 Lambda 函数的以下字段:getPost(id:ID!): PostallPosts: [Post]addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!Post.relatedPosts: [Post]

在 AWS AppSync 控制台的架构编辑器中,在右侧为 getPost(id:ID!): Post 选择 Attach Resolver (附加解析程序)

选择您的 AWS Lambda 数据源。在请求映射模板部分中,选择 Invoke And Forward Arguments (调用并转发参数)

修改 payload 对象,添加字段名称。您的模板应该类似以下内容:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "getPost", "arguments": $utils.toJson($context.arguments) } }

响应映射模板部分中,选择 Return Lambda Result (返回 Lambda 结果)

在本例中,按原样使用基本模板。它应该类似以下内容:

$utils.toJson($context.result)

选择 Save (保存)。您已成功附加了您的首个解析程序。针对其余字段重复此操作,如下所示:

对于 addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! 请求映射模板:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "addPost", "arguments": $utils.toJson($context.arguments) } }

对于 addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! 响应映射模板:

$utils.toJson($context.result)

对于 allPosts: [Post] 请求映射模板:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "allPosts" } }

对于 allPosts: [Post] 响应映射模板:

$utils.toJson($context.result)

对于 Post.relatedPosts: [Post] 请求映射模板:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "relatedPosts", "source": $utils.toJson($context.source) } }

对于 Post.relatedPosts: [Post] 响应映射模板:

$utils.toJson($context.result)

测试您的 GraphQL API

现在您的 Lambda 函数已与 GraphQL 解析程序连接,您可以使用控制台或客户端应用程序运行一些更改和查询。

在 AWS AppSync 控制台的左侧,选择 Queries (查询) 选项卡,然后粘贴以下代码:

addPost 更改

mutation addPost { addPost( id: 6 author: "Author6" title: "Sixth book" url: "https://www.amazon.com/" content: "This is the book is a tutorial for using GraphQL with AWS AppSync." ) { id author title content url ups downs } }

getPost 查询

query { getPost(id: "2") { id author title content url ups downs } }

allPosts 查询

query { allPosts { id author title content url ups downs relatedPosts { id title } } }

返回错误

对已知字段进行解析可能导致错误。AWS AppSync 允许您针对以下来源引发错误:

  • 请求或响应映射模板

  • Lambda 函数

从映射模板

您可以通过 VTL 模板使用 $utils.error 帮助程序方法,刻意引发错误。它可接收 errorMessageerrorType 以及可选的 data 值作为参数。出现错误后,data 对于将额外的数据返回客户端很有用。在 GraphQL 最终响应中,data 对象将添加到 errors

以下示例显示了如何在 Post.relatedPosts: [Post] 响应映射模板中使用它:

$utils.error("Failed to fetch relatedPosts", "LambdaFailure", $context.result)

这将生成与以下内容类似的 GraphQL 响应:

{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "LambdaFailure", "locations": [ { "line": 5, "column": 5 } ], "message": "Failed to fetch relatedPosts", "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ] } ] }

错误导致 allPosts[0].relatedPostsnull,而 errorMessageerrorTypedata 体现在 data.errors[0] 对象中。

从 Lambda 函数

AWS AppSync 还可理解 Lambda 函数引发的错误。Lambda 编程模型允许您引发已处理的 错误。如果 Lambda 函数引发错误,AWS AppSync 将无法解析当前字段。响应只会设置为 Lambda 返回的错误消息。目前您不能通过从 Lambda 函数中引发错误将无关数据传递回客户端。

注意:如果您的 Lambda 函数引发未处理的 错误,AWS AppSync 将使用 AWS Lambda 设置的错误消息。

以下 Lambda 函数会引发错误:

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); callback("I fail. Always."); };

这将返回与以下内容类似的 GraphQL 响应:

{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "Lambda:Handled", "locations": [ { "line": 5, "column": 5 } ], "message": "I fail. Always." } ] }

高级使用案例:批处理

在本示例中,Lambda 函数有一个 relatedPosts 字段,返回与已知文章关联的文章列表。在查询示例中,从 Lambda 函数调用 allPosts 字段返回了 5 个文章。由于我们已经指定,我们还希望针对每个返回的文章解析 relatedPosts,因此 relatedPosts 字段操作将被调用 5 次。

query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yields 5 posts id title } } }

对于这个具体示例而言这可能并不起眼,但这种复合的过度提取会快速破坏应用程序。

如果您要针对同一查询中返回的相关 Posts 再次提取 relatedPosts,调用数量将显著增加。

query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yield 5 posts = 5 x 5 Posts id title relatedPosts { // 5 x 5 Lambda invocations - each yield 5 posts = 25 x 5 Posts id title author } } } }

在这个相对简单的查询中,AWS AppSync 将调用 Lambda 函数 1 + 5 + 25 = 31 次。

这是相当常见的挑战,常被称为 N+1 问题(在本例中 N = 5),会导致延迟增加,以及应用程序费用提升。

我们解决此问题的方式是批处理类似的字段解析程序请求。在本例中,Lambda 函数并不解析单一已知文章的相关文章列表,而是解析已知的一批文章的相关文章列表。

为了演示此操作,让我们将 Post.relatedPosts: [Post] 解析程序转换为启用批处理的解析程序。

在 AWS AppSync 控制台的右侧,选择已有的 Post.relatedPosts: [Post] 解析程序。将请求映射模板改为以下内容:

{ "version": "2017-02-28", "operation": "BatchInvoke", "payload": { "field": "relatedPosts", "source": $utils.toJson($context.source) } }

只有 operation 字段由 Invoke 改为了 BatchInvoke。负载字段现在成为了模板中指定的一组任意内容。在本例中,Lambda 函数接收以下内容作为输入:

[ { "field": "relatedPosts", "source": { "id": 1 } }, { "field": "relatedPosts", "source": { "id": 2 } }, ... ]

由于在请求映射模板中指定了 BatchInvoke,Lambda 函数将接收一组请求,并返回一组结果。

需要特别指出的是,这组结果必须 与请求负载的大小和顺序匹配,这样 AWS AppSync 才可以相应地匹配结果。

在本批处理示例中,Lambda 函数返回一批结果,如下所示:

[ [{"id":"2","title":"Second book"}, {"id":"3","title":"Third book"}], // relatedPosts for id=1 [{"id":"3","title":"Third book"}] // relatedPosts for id=2 ]

用 Node.js 编写的以下 AWS Lambda 函数演示了 Post.relatedPosts 字段的批处理功能,如下所示:

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length); // event is now an array var field = event[0].field; switch(field) { case "relatedPosts": var results = []; // the response MUST contain the same number // of entries as the payload array for (var i=0; i< event.length; i++) { console.log("post {}", JSON.stringify(event[i].source)); results.push(relatedPosts[event[i].source.id]); } console.log("results {}", JSON.stringify(results)); callback(null, results); break; default: callback("Unknown field, unable to resolve" + field, null); break; } };

返回单个错误

之前的若干示例说明,可以从 Lambda 函数返回单个错误,或者从映射模板引发一个错误。对于批量调用,Lambda 函数引发的一个错误会将整个批次标记为失败。如果发生了不可恢复的错误,例如与数据存储的连接失败,这样的结果可能是可以接受的。但是,如果批处理中的某些项目成功了,但另一些失败了,可能会同时返回错误和有效数据。由于 AWS AppSync 要求将批处理响应列出与批处理的原始大小相匹配的元素,您需要定义可以区分有效数据与错误的数据结构。

例如,如果 Lambda 函数将返回一批相关的文章,您可以选择返回一组 Response 对象,每个对象具有可选的 dataerrorMessageerrorType 字段。如果出现 errorMessage 字段,则表示出现错误。

以下代码显示您如何更新 Lambda 函数:

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length); // event is now an array var field = event[0].field; switch(field) { case "relatedPosts": var results = []; results.push({ 'data': relatedPosts['1'] }); results.push({ 'data': relatedPosts['2'] }); results.push({ 'data': null, 'errorMessage': 'Error Happened', 'errorType': 'ERROR' }); results.push(null); results.push({ 'data': relatedPosts['3'], 'errorMessage': 'Error Happened with last result', 'errorType': 'ERROR' }); callback(null, results); break; default: callback("Unknown field, unable to resolve" + field, null); break; } };

对于此示例,以下响应映射模板解析 Lambda 函数的每个项目并引发出现的任何错误:

#if( $context.result && $context.result.errorMessage ) $utils.error($context.result.errorMessage, $context.result.errorType, $context.result.data) #else $utils.toJson($context.result.data) #end

此示例返回与以下内容类似的 GraphQL 响应:

{ "data": { "allPosts": [ { "id": "1", "relatedPostsPartialErrors": [ { "id": "4", "title": "Fourth book" } ] }, { "id": "2", "relatedPostsPartialErrors": [ { "id": "3", "title": "Third book" }, { "id": "5", "title": "Fifth book" } ] }, { "id": "3", "relatedPostsPartialErrors": null }, { "id": "4", "relatedPostsPartialErrors": null }, { "id": "5", "relatedPostsPartialErrors": null } ] }, "errors": [ { "path": [ "allPosts", 2, "relatedPostsPartialErrors" ], "errorType": "ERROR", "locations": [ { "line": 4, "column": 9 } ], "message": "Error Happened" }, { "path": [ "allPosts", 4, "relatedPostsPartialErrors" ], "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ], "errorType": "ERROR", "locations": [ { "line": 4, "column": 9 } ], "message": "Error Happened with last result" } ] }