Amazon Dynomo DB:BatchPutItem 因为 APPSYNC_ASSUME_ROLE 和类型不匹配错误

Amazon Dynomo DB: BatchPutItem because of APPSYNC_ASSUME_ROLE and type mismatch error

我正在使用 AWS Amplify 构建一个 React 应用程序。我使用 Cognito 用户池进行身份验证,并使用 GraphQL AppSync 后端作为我的后端。

我正在尝试编写一个自定义解析器来批量修改。这是我使用的架构:

type Todo @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  title: String!
  description: String
  completed: Boolean
}

input CreateTodoInput {
  id: ID
  title: String!
  description: String
  completed: Boolean
}

type Mutation {
  batchAddTodos(todos: [CreateTodoInput]): [Todo]
}

此架构使用 Cognito User Pools.

为 GraphQL API 启用身份验证

为了让这个自定义突变起作用,需要添加 custom resolvers。我更改了 amplify/api/<your-api-name>/stacks/CustomResources.json 以包含以下资源:

// Left everything as it was
 "Resources": {
    "EmptyResource": {
      "Type": "Custom::EmptyResource",
      "Condition": "AlwaysFalse"
    },
    "BatchAddTodosResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "DataSourceName": "TodoTable",
        "TypeName": "Mutation",
        "FieldName": "batchAddTodos",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.batchAddTodos.req.vtl",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.batchAddTodos.res.vtl",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        }
      }
    }
  },
// ... more code that I didn't touch

对于自定义请求解析器,我编写了以下模板:

#foreach($item in ${ctx.args.todos})
    ## [Start] Owner Authorization Checks **
    #set( $isOwnerAuthorized = false )
    ## Authorization rule: { allow: "owner", ownerField: "owner", identityField: "cognito:username" } **
    #set( $allowedOwners0 = $util.defaultIfNull($item.owner, null) )
    #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"),
    $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
    #if( $util.isList($allowedOwners0) )
        #foreach( $allowedOwner in $allowedOwners0 )
            #if( $allowedOwner == $identityValue )
                #set( $isOwnerAuthorized = true )
            #end
        #end
    #end
    #if( $util.isString($allowedOwners0) )
        #if( $allowedOwners0 == $identityValue )
            #set( $isOwnerAuthorized = true )
        #end
    #end
    #if( $util.isNull($allowedOwners0) && (! $item.containsKey("owner")) )
        $util.qr($item.put("owner", $identityValue))
        #set( $isOwnerAuthorized = true )
    #end
    ## [End] Owner Authorization Checks **

    ## [Start] Throw if unauthorized **
    #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized
        == true) )
        $util.unauthorized()
    #end
    ## [End] Throw if unauthorized **
#end

#set($todosdata = [])
#foreach($item in ${ctx.args.todos})
    $util.qr($item.put("createdAt", $util.time.nowISO8601()))
    $util.qr($item.put("updatedAt", $util.time.nowISO8601()))
    $util.qr($item.put("__typename", "Todo"))
    $util.qr($item.put("id", $util.defaultIfNullOrBlank($item.id, $util.autoId())))
    $util.qr($todosdata.add($util.dynamodb.toMapValues($item)))
#end
{
  "version": "2018-05-29",
  "operation": "BatchPutItem",
  "tables": {
      "TodoTable": $utils.toJson($todosdata)
  }
}

在第一个循环中,我试图验证用户是否有权访问他创建的待办事项。在第二个循环中,我添加了由 Amplify CLI 生成的解析器添加的数据。这包括 __typename、时间戳和 id.

然后我请求创建资源。我关注了this tutorial for the code。请注意,我必须将版本更新为 "2018-05-29"。 Amplify CLI 生成的代码通常有一个版本"2017-02-28"(我不知道这是否重要)。

我还为响应解析器编写了以下映射:

#if ($ctx.error)
    $util.appendError($ctx.error.message, $ctx.error.type, null, $ctx.result.data.unprocessedKeys)
#end

$util.toJson($ctx.result.data)

这基本上告诉 AppSync return 数据和所有未处理项目的错误。

我第一次尝试使用 React 发出请求:

import API, { graphqlOperation } from '@aws-amplify/api';

// ... later

async function handleClick() {
  const todoFixtures = [
    { id: 1, title: 'Get groceries', description: '', completed: false },
    { id: 2, title: 'Go to the gym', description: 'Leg Day', completed: true }
  ];

  try {
    const input = { todos: prepareTodos(todoFixtures) };
    const res = await API.graphql(graphqlOperation(batchAddTodos, input));
    console.log(res);
  } catch (err) {
    console.log('error ', err);
  }
}

prepareTodos 只是摆脱了 id 字段并将空字段设置为 null(以避免 DynamoDB 对我大喊大叫)。代码在底部,因为它是无关紧要的。

由于失败,我尝试通过 AppSync 控制台进行更改:

mutation add {
  batchAddTodos(todos: [
    {title: "Hello", description: "Test", completed: false}
  ]) {
    id title
  }
}

但是两次尝试都抛出以下错误:

{
  "data": {
    "batchAddTodos": null
  },
  "errors": [
    {
      "path": [
        "batchAddTodos"
      ],
      "data": null,
      "errorType": "DynamoDB:AmazonDynamoDBException",
      "errorInfo": null,
      "locations": [
        {
          "line": 32,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "User: arn:aws:sts::655817346595:assumed-role/Todo-role-naona7ytt5drxazwmtp7a2uccy-batch/APPSYNC_ASSUME_ROLE is not authorized to perform: dynamodb:BatchWriteItem on resource: arn:aws:dynamodb:eu-central-1:655817346595:table/TodoTable (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: AccessDeniedException; Request ID: EP48SJPVMB9G9M69HPR0BO8SKJVV4KQNSO5AEMVJF66Q9ASUAAJG)"
    },
    {
      "path": [
        "batchAddTodos"
      ],
      "locations": null,
      "message": "Can't resolve value (/batchAddTodos) : type mismatch error, expected type LIST"
    }
  ]
}

这让我相信 React 代码 "correct" 或者至少与 AppSync 代码一样错误。但我怀疑错误出在解析器模板映射中的某个地方。我只是找不到它。这里出了什么问题?

可能生成的 assume-role 不支持 "2018-05-19" 版本?下面是角色默认生成角色的代码(这个不是我写的):

 "AssumeRolePolicyDocument": {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "appsync.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
},
"Policies": [
    {
        "PolicyName": "DynamoDBAccess",
        "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "dynamodb:BatchGetItem",
                        "dynamodb:BatchWriteItem",
                        "dynamodb:PutItem",
                        "dynamodb:DeleteItem",
                        "dynamodb:GetItem",
                        "dynamodb:Scan",
                        "dynamodb:Query",
                        "dynamodb:UpdateItem"
                    ],
                    "Resource": [
                        {
                            "Fn::Sub": [
                                "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
                                {
                                    "tablename": {
                                        "Fn::If": [
                                            "HasEnvironmentParameter",
                                            {
                                                "Fn::Join": [
                                                    "-",
                                                    [
                                                        "Todo",
                                                        {
                                                            "Ref": "GetAttGraphQLAPIApiId"
                                                        },
                                                        {
                                                            "Ref": "env"
                                                        }
                                                    ]
                                                ]
                                            },
                                            {
                                                "Fn::Join": [
                                                    "-",
                                                    [
                                                        "Todo",
                                                        {
                                                            "Ref": "GetAttGraphQLAPIApiId"
                                                        }
                                                    ]
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        },
                        {
                            "Fn::Sub": [
                                "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
                                {
                                    "tablename": {
                                        "Fn::If": [
                                            "HasEnvironmentParameter",
                                            {
                                                "Fn::Join": [
                                                    "-",
                                                    [
                                                        "Todo",
                                                        {
                                                            "Ref": "GetAttGraphQLAPIApiId"
                                                        },
                                                        {
                                                            "Ref": "env"
                                                        }
                                                    ]
                                                ]
                                            },
                                            {
                                                "Fn::Join": [
                                                    "-",
                                                    [
                                                        "Todo",
                                                        {
                                                            "Ref": "GetAttGraphQLAPIApiId"
                                                        }
                                                    ]
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    }
]

prepareTodos:

const map = f => arr => arr.map(f);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const dissoc = prop => ({ [prop]: _, ...obj }) => obj;
const mapObj = f => obj =>
  Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: f(obj[key]) }), {});
const replaceEmptyStringWithNull = x => (x === '' ? null : x);
const prepareTodos = map(
  pipe(
    dissoc('id'),
    mapObj(replaceEmptyStringWithNull)
  )
);

编辑: 我设法解决了类型不匹配问题。在回复中我必须 return: $util.toJson($ctx.result.data.TodoTable).

$util.toJson($ctx.result.data.TodoTable) 在响应模板中,以及将 TodoTable 更改为实际的 table 名称(您可以在控制台的 DynamoDB 下查找它,它看起来像这样 Todo-dqeronnsgvd2pf3facjmlgtsjk-master) 解决了错误。

另外 here 是我在偶然发现这个问题时编写的分步教程。