如何通过查询对AWS AppSync中的嵌套字段进行过滤

时间:2019-09-16 19:06:47

标签: filter nested graphql amazon-dynamodb aws-appsync

问题和预期结果

我正在使用概念证明模式和DynamoDB表设置来对嵌套字段值进行过滤。我一般都遵循here的想法以及$utils.transform.toDynamoDBFilterExpressionhere)的文档。

基本思想是:使用相同的原则,我想按任意深度的嵌套字段(小于DynamoDB中32个文档路径长度的限制)进行过滤。相关设置如下:

AppSync模式(为命名约定道歉;应该是一种快速而肮脏的PoC):

query {
    listActiveListingsBySubAndFilter(
        filter: TableTestMasterDataTable_ImportV1FilterInput!,
        limit: Int,
        nextToken: String
    ): TestMasterDataTable_ImportV1Connection
}

input TableBooleanFilterInput {
    ne: Boolean
    eq: Boolean
}

input TableDataObjectFilterInput {
    beds: TableFloatFilterInput
    baths: TableFloatFilterInput
}

input TableFloatFilterInput {
    ne: Float
    eq: Float
    le: Float
    lt: Float
    ge: Float
    gt: Float
    contains: Float
    notContains: Float
    between: [Float]
}

input TableIDFilterInput {
    ne: ID
    eq: ID
    le: ID
    lt: ID
    ge: ID
    gt: ID
    contains: ID
    notContains: ID
    between: [ID]
    beginsWith: ID
}

input TableIntFilterInput {
    ne: Int
    eq: Int
    le: Int
    lt: Int
    ge: Int
    gt: Int
    contains: Int
    notContains: Int
    between: [Int]
}

input TableStringFilterInput {
    ne: String
    eq: String
    le: String
    lt: String
    ge: String
    gt: String
    contains: String
    notContains: String
    between: [String]
    beginsWith: String
}

input TableTestMasterDataTable_ImportV1FilterInput {
    id: TableStringFilterInput
    status: TableStringFilterInput
    sub: TableStringFilterInput
    data: TableDataObjectFilterInput
}

type TestMasterDataTable_ImportV1 {
    id: String!
    status: String!
    sub: String!
    data: AWSJSON
}

type TestMasterDataTable_ImportV1Connection {
    items: [TestMasterDataTable_ImportV1]
    nextToken: String
}

input UpdateTestMasterDataTable_ImportV1Input {
    id: String!
    status: String
    sub: String!
    data: AWSJSON
}

VTL请求和响应解析器:

## Request resolver

#set( $filter = $ctx.args.filter )
#set( $path = $filter.data )

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "listings-index",  ## GSI on table with HASH: status, RANGE: sub
    "query" : {
        "expression": "#status = :status and #sub = :sub",
        "expressionNames" : {
            "#status" : "status",
            "#sub" : "sub"
        },
        "expressionValues" : {
            ":status" : $util.dynamodb.toDynamoDBJson("Active"),
            ":sub" : $util.dynamodb.toDynamoDBJson($filter.sub.eq)
        }
    },
    "filter" : $util.transform.toDynamoDBFilterExpression($path),
    "limit": $util.defaultIfNull($ctx.args.limit, 20),
    "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
}


## Response resolver

{
    "items": $util.toJson($ctx.result.items),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}

示例DynamoDB表元素:

{
  "_meta": {
    "exposure": 0.08,
    "lastActive": 1557800000,
    "lastUpdated": 1557878400,
    "lastView": 1557878500,
    "numViews": 63,
    "posted": 1557878400
  },
  "buildingID": "325-5th-Ave,-New-York,-NY-10016,-USA",
  "data": {
    "agent": [
      {
        "agentID": "daeo@gmail.com"
      },
      {
        "agentID": "ben@gmail.com"
      }
    ],
    "amenities": [
      "hot tub",
      "time machine"
    ],
    "baths": 2,
    "beds": 2
  },
  "id": "325-5th-Ave,-New-York,-NY-10016,-USA#37C:1557878400",
  "status": "Active",
  "sub": "new-york/manhattan/listings",
  "unitNum": "37C",
  "unitRefID": "325-5th-Ave,-New-York,-NY-10016,-USA#37C"
}

基于所有这些,如果我运行以下查询:

listActiveListingsBySubAndFilter(filter: {
    "sub" : {
      "eq" : "new-york/manhattan/listings"
    },
    "data": {
      "beds": {
        "eq": 2.0
      }
    }) {
    items {
      id
      status
    }
    nextToken
}

我希望得到这样的回报:

{
  "data": {
    "listActiveListingsBySubAndFilter": {
      "items": [
          {
              "id": "325-5th-Ave,-New-York,-NY-10016,-USA#37C:1557878400",
              "status": "Active"
          }
      ],
      "nextToken": null
    }
  }
}

注意:这是唯一的预期收益,因为此时数据库中只有一项与这些要求匹配。

实际结果

所有这些都说明,我得到的结果(或缺乏结果)没有多大意义。无论查询(data.bedsdata.baths)如何,如果字段嵌套在data中,返回的结果都是相同的:

{
  "data": {
    "listActiveListingsBySubAndFilter": {
      "items": [],
      "nextToken": null
    }
  }
}

我已经确认查询可以正常工作,并且过滤器表达式的格式正确(它可以用于其他id等非嵌套字段)。令人困惑的是,似乎没有应用过滤器(或者可能是以某种非直观的方式应用过滤器?)。作为参考,这是上面的典型CloudWatch日志的片段:

{
    "context": {
        "arguments": {
            "filter": {
                "sub": {
                    "eq": "new-york/manhattan/listings"
                },
                "data": {
                    "beds": {
                        "eq": 2
                    }
                }
            },
            "limit": 200
        },
        "stash": {},
        "outErrors": []
    },
    "fieldInError": false,
    "errors": [],
    "parentType": "Query",
    "graphQLAPIId": "q7ueubhsorehbjpr5e6ymj7uua",
    "transformedTemplate": "\n\n{\n    \"version\" : \"2017-02-28\",\n    \"operation\" : \"Query\",\n    \"index\" : \"listings-index\",\n    \"query\" : {\n        \"expression\": \"#status = :status and #sub = :sub\",\n        \"expressionNames\" : {\n        \t\"#status\" : \"status\",\n            \"#sub\" : \"sub\"\n    \t},\n        \"expressionValues\" : {\n            \":status\" : {\"S\":\"Active\"},\n            \":sub\" : {\"S\":\"new-york/manhattan/listings\"}\n        }\n    },\n    \"filter\" : {\"expression\":\"(#beds = :beds_eq)\",\"expressionNames\":{\"#beds\":\"beds\"},\"expressionValues\":{\":beds_eq\":{\"N\":2.0}}},\n    \"limit\": 200,\n    \"nextToken\": null\n}"
}

请注意expressionValuestransformedTemplate(无{ "N" : 2.0 }格式)中的过滤器$util.toDynamoDBJson值,并将其与DynamoDB中该字段上的对象中的值进行比较。

我已经尝试了所有方法,包括将字段本身更改为字符串,并进行了各种过滤器操作,例如eqcontains,以查看这是否是一些奇怪的类型不一致,但没有运气。

到目前为止,我有两个备份解决方案,它们涉及“收集”我可能要过滤的所有相关字段(使用我想保留嵌套的属性来整理记录)或创建一个仅包含新的嵌套类型用于筛选的高级字段,即有效地将记录分为记录引用和记录过滤器引用。在这种情况下,我们将获得一些“ Listing”记录,该记录的data字段值为ListingFilterData,例如:

type Listing {
    id: String!
    sub: String!
    status: String!
    data: ListingFilterData!
}

type ListingFilterData {
    beds: Float!
    baths: Float!
}

两者都是可行的,但我宁愿尝试解决当前问题,而不是在表中添加一堆额外的数据。

有什么想法吗?

更新9/17/19

经过一番摆弄之后,我偶然发现了暗示here的解决方案。根据解决方案的思路,我成功使用以下VTL请求解析程序实现了硬编码嵌套查询过滤器(并更改了过滤器表达式键名,以避免在data上保留字):

#set( $filter = $ctx.args.filter )
#set( $path = $filter.filterData ) ## currently, unused

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "listings-index",
    "query" : {
        "expression": "#status = :status and #sub = :sub",
        "expressionNames" : {
            "#status" : "status",
            "#sub" : "sub"
        },
        "expressionValues" : {
            ":status" : $util.dynamodb.toDynamoDBJson("Active"),
            ":sub" : $util.dynamodb.toDynamoDBJson($filter.sub.eq)
        }
    },
    "filter" : {
        "expression" : "#filterData.beds = :beds",
        "expressionValues" : {
            ":beds" : $util.dynamodb.toDynamoDBJson(2.0)
        }
    },
    "limit": $util.defaultIfNull($ctx.args.limit, 20),
    "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
}

这将返回我的预期结果:

{
  "data": {
    "listActiveListingsBySubAndFilter": {
      "items": [
        {
          "id": "325-5th-Ave,-New-York,-NY-10016,-USA#37C:1557878400",
          "status": "Active"
        }
      ],
      "nextToken": null
    }
  }
}

似乎是进步,但是关于如何动态创建docpath并为嵌套属性创建表达式名称的任何想法?运行更多的想法,如果有有趣的事情出现,则会报告...

从19/17/19更新#2

在进一步处理请求解析器之后,我认为我找到了一种快速而肮脏的方法来动态获取路径和目标var,以为嵌套属性创建过滤器表达式。 注意:整个过程仍然返回一个空结果集,并且假设目前只有一个过滤键,但是保留关键字位似乎已经解决。仍然想知道为什么结果没有按预期显示。

#set( $filter = $ctx.args.filter )
#foreach( $parent in $filter.keySet() )
    #set( $path = $parent )
#end
#set( $target = $filter[$path] )
#foreach( $ff in $target.keySet() ) ## should only contain one Map key-value pair
    #set( $fp = $ff )
#end
#set( $fv = $target[$fp] )

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "listings-index",
    "query" : {
        "expression": "#status = :status and #sub = :sub",
        "expressionNames" : {
            "#status" : "status",
            "#sub" : "sub"
        },
        "expressionValues" : {
            ":status" : $util.dynamodb.toDynamoDBJson("Active"),
            ":sub" : $util.dynamodb.toDynamoDBJson($filter.sub.eq)
        }
    },
    "filter" : {
        "expression" : "#ffp = :$fp",  ## filter path parent.target = :target
        "expressionNames" : {
            "#ffp" : "${path}.${fp}"
        },
        "expressionValues" : {
            ":$fp" : $util.dynamodb.toDynamoDBJson(${fv.eq}), ## :target : value to filter for
        }
    },
    "limit": $util.defaultIfNull($ctx.args.limit, 200),
    "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
}

检查CloudWatch日志transformedTemplate显示表达式名称和值已被适当替换:

"filter" : {
    "expression\" : "#ffp = :beds",
    "expressionNames" : {
        "#ffp" : "filterData.beds"
    },
    "expressionValues" : {
        ":beds" : { "N": 2.0 }
    }
}

从19/18/19起更新

我可能终于发现了问题的根源:似乎评估expressionNames的方式不允许键成为docpath。如果我运行以下任一过滤器(请注意,使用非保留的DynamoDB关键字来说明问题在于表达式名称替换),那么我将得到想要的结果:

"filter" : {
    "expression" : "filterData.beds = :beds",  ## filter path parent.target = :target
        "expressionValues" : {
            ":beds" : $util.dynamodb.toDynamoDBJson(${fv.eq}) ## :target : value to filter for
        }
    }

"filter" : {
    "expression" : "filterData.beds = :${fp}",  ## filter path parent.target = :target
        "expressionValues" : {
            ":{fp}" : $util.dynamodb.toDynamoDBJson(${fv.eq}) ## :target : value to filter for
        }
    }

现在,如果我做了较小的更改,则仅尝试用表达式名称值替换

"filter" : {
    "expression" : "#filterData.beds = :${fp}",  ## filter path parent.target = :target
        "expressionNames": {
            "#filterData.beds" : "filterData.beds"
        },
        "expressionValues" : {
            ":{fp}" : $util.dynamodb.toDynamoDBJson(${fv.eq}) ## :target : value to filter for
        }
    }

我收到以下错误消息:

"ExpressionAttributeNames contains invalid key: Syntax error; key: \"#filterData.beds\" (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException"

即使使用硬编码的路径替换,VTL似乎也将路径作为单个键名读取。动态交换表达式的值时出现相同的问题,因此没有硬编码的字符串。

1 个答案:

答案 0 :(得分:2)

已解决

我偶然遇到了这个gem,它给了我一些额外的东西,我需要用动态键名找到可行的解决方案!

下面是过滤器表达式的样子:

"filter" : {
        "expression" : "#path.#filter = :${fp}",  ## filter path parent.target = :target
        "expressionNames": {
            "#path" : "${path}",
            "#filter" : "${fp}"
        },
        "expressionValues" : {
            ":${fp}" : $util.dynamodb.toDynamoDBJson(${fv.eq}) ## :target : value to filter for
        }
    }

这里的主张是,虽然表达式属性名称通常被解释为文档路径,但在引入替换名称的同时,解释器将键名称视为标量属性,而不是文档路径。您需要单独标识路径元素,并用它们替代。