aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
89 stars 77 forks source link

How to change GQL VTL to stop overwriting nested data #236

Open michael-drury opened 3 years ago

michael-drury commented 3 years ago

Which Category is your question related to? GraphQL API

Amplify CLI Version 4.32.1

What AWS Services are you utilizing? AppSync, DynamoDb

Provide additional details e.g. code snippets

I have a nested GQL schema in the following style:

type User
@model
@key (name: "byFoo", fields:["foo"], queryField: "userByFoo")
@auth(rules: [
  {allow: owner}
  {allow: private provider: iam}
]
) {
  id: ID!
  foo: String!
  bar: barType
}

type barType {
  a: barData
  b: barData
  c: barData
}

type barData{
   d: Float
   e: Float
   f: Float
}

And am trying to use the use the following GQL mutation to update specific, individual nested values:

mutation MyMutation {
  updateUser(input: {id: "user-id", bar: {a: {d: 1.5}}}) {
    id
  }
}

However, when I do this, all the other nested data stored of type 'bar' is removed. From my understanding, this is due to the way that amplify generates the VTL. I believe that the data is overwritten during this section:

#if( !$expSet.isEmpty() )
  #set( $expression = "SET" )
  #foreach( $entry in $expSet.entrySet() )
    #set( $expression = "$expression $entry.key = $entry.value" )
    #if( $foreach.hasNext() )
      #set( $expression = "$expression," )
    #end
  #end
#end

As the key is being set to "bar" with the value as "{a:{d:1}}", as opposed to the key being "bar.a.d" and value being 1.

Is there a way to alter this behaviour so I can input new nested data, without overwriting any previously entered data?

For simplicity i'd rather keep just a VTL, instead of using a lambda resolver if possible.

The full auto-generated VTL is as follows:

## [Start] Determine request authentication mode **
#if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) )
  #set( $authMode = "userPools" )
#end
## [End] Determine request authentication mode **
## [Start] Check authMode and execute owner/group checks **
#if( $authMode == "userPools" )
  ## No Static Group Authorization Rules **

  #if( ! $isStaticGroupAuthorized )
    ## No dynamic group authorization rules **

    ## [Start] Owner Authorization Checks **
    #set( $ownerAuthExpressions = [] )
    #set( $ownerAuthExpressionValues = {} )
    #set( $ownerAuthExpressionNames = {} )
    ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **
    $util.qr($ownerAuthExpressions.add("#owner0 = :identity0"))
    $util.qr($ownerAuthExpressionNames.put("#owner0", "owner"))
    $util.qr($ownerAuthExpressionValues.put(":identity0", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")))))
    ## [End] Owner Authorization Checks **

    ## [Start] Collect Auth Condition **
    #set( $authCondition = $util.defaultIfNull($authCondition, {
  "expression": "",
  "expressionNames": {},
  "expressionValues": {}
}) )
    #set( $totalAuthExpression = "" )
    ## Add dynamic group auth conditions if they exist **
    #if( $groupAuthExpressions )
      #foreach( $authExpr in $groupAuthExpressions )
        #set( $totalAuthExpression = "$totalAuthExpression $authExpr" )
        #if( $foreach.hasNext )
          #set( $totalAuthExpression = "$totalAuthExpression OR" )
        #end
      #end
    #end
    #if( $groupAuthExpressionNames )
      $util.qr($authCondition.expressionNames.putAll($groupAuthExpressionNames))
    #end
    #if( $groupAuthExpressionValues )
      $util.qr($authCondition.expressionValues.putAll($groupAuthExpressionValues))
    #end
    ## Add owner auth conditions if they exist **
    #if( $totalAuthExpression != "" && $ownerAuthExpressions && $ownerAuthExpressions.size() > 0 )
      #set( $totalAuthExpression = "$totalAuthExpression OR" )
    #end
    #if( $ownerAuthExpressions )
      #foreach( $authExpr in $ownerAuthExpressions )
        #set( $totalAuthExpression = "$totalAuthExpression $authExpr" )
        #if( $foreach.hasNext )
          #set( $totalAuthExpression = "$totalAuthExpression OR" )
        #end
      #end
    #end
    #if( $ownerAuthExpressionNames )
      $util.qr($authCondition.expressionNames.putAll($ownerAuthExpressionNames))
    #end
    #if( $ownerAuthExpressionValues )
      $util.qr($authCondition.expressionValues.putAll($ownerAuthExpressionValues))
    #end
    ## Set final expression if it has changed. **
    #if( $totalAuthExpression != "" )
      #if( $util.isNullOrEmpty($authCondition.expression) )
        #set( $authCondition.expression = "($totalAuthExpression)" )
      #else
        #set( $authCondition.expression = "$authCondition.expression AND ($totalAuthExpression)" )
      #end
    #end
    ## [End] Collect Auth Condition **
  #end

  ## [Start] Throw if unauthorized **
  #if( !($isStaticGroupAuthorized == true || ($totalAuthExpression != "")) )
    $util.unauthorized()
  #end
  ## [End] Throw if unauthorized **
#end
## [End] Check authMode and execute owner/group checks **

#if( $authCondition && $authCondition.expression != "" )
  #set( $condition = $authCondition )
  #if( $modelObjectKey )
    #foreach( $entry in $modelObjectKey.entrySet() )
      $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)"))
      $util.qr($condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key"))
    #end
  #else
    $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#id)"))
    $util.qr($condition.expressionNames.put("#id", "id"))
  #end
#else
  #if( $modelObjectKey )
    #set( $condition = {
  "expression": "",
  "expressionNames": {},
  "expressionValues": {}
} )
    #foreach( $entry in $modelObjectKey.entrySet() )
      #if( $velocityCount == 1 )
        $util.qr($condition.put("expression", "attribute_exists(#keyCondition$velocityCount)"))
      #else
        $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)"))
      #end
      $util.qr($condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key"))
    #end
  #else
    #set( $condition = {
  "expression": "attribute_exists(#id)",
  "expressionNames": {
      "#id": "id"
  },
  "expressionValues": {}
} )
  #end
#end
## Automatically set the updatedAt timestamp. **
$util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601())))
$util.qr($context.args.input.put("__typename", "User"))
## Update condition if type is @versioned **
#if( $versionedCondition )
  $util.qr($condition.put("expression", "($condition.expression) AND $versionedCondition.expression"))
  $util.qr($condition.expressionNames.putAll($versionedCondition.expressionNames))
  $util.qr($condition.expressionValues.putAll($versionedCondition.expressionValues))
#end
#if( $context.args.condition )
  #set( $conditionFilterExpressions = $util.parseJson($util.transform.toDynamoDBConditionExpression($context.args.condition)) )
  $util.qr($condition.put("expression", "($condition.expression) AND $conditionFilterExpressions.expression"))
  $util.qr($condition.expressionNames.putAll($conditionFilterExpressions.expressionNames))
  $util.qr($condition.expressionValues.putAll($conditionFilterExpressions.expressionValues))
#end
#if( $condition.expressionValues && $condition.expressionValues.size() == 0 )
  #set( $condition = {
  "expression": $condition.expression,
  "expressionNames": $condition.expressionNames
} )
#end
#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
#set( $expAdd = {} )
#set( $expRemove = [] )
#if( $modelObjectKey )
  #set( $keyFields = [] )
  #foreach( $entry in $modelObjectKey.entrySet() )
    $util.qr($keyFields.add("$entry.key"))
  #end
#else
  #set( $keyFields = ["id"] )
#end
#foreach( $entry in $util.map.copyAndRemoveAllKeys($context.args.input, $keyFields).entrySet() )
  #if( !$util.isNull($dynamodbNameOverrideMap) && $dynamodbNameOverrideMap.containsKey("$entry.key") )
    #set( $entryKeyAttributeName = $dynamodbNameOverrideMap.get("$entry.key") )
  #else
    #set( $entryKeyAttributeName = $entry.key )
  #end
  #if( $util.isNull($entry.value) )
    #set( $discard = $expRemove.add("#$entryKeyAttributeName") )
    $util.qr($expNames.put("#$entryKeyAttributeName", "$entry.key"))
  #else
    $util.qr($expSet.put("#$entryKeyAttributeName", ":$entryKeyAttributeName"))
    $util.qr($expNames.put("#$entryKeyAttributeName", "$entry.key"))
    $util.qr($expValues.put(":$entryKeyAttributeName", $util.dynamodb.toDynamoDB($entry.value)))
  #end
#end
#set( $expression = "" )
#if( !$expSet.isEmpty() )
  #set( $expression = "SET" )
  #foreach( $entry in $expSet.entrySet() )
    #set( $expression = "$expression $entry.key = $entry.value" )
    #if( $foreach.hasNext() )
      #set( $expression = "$expression," )
    #end
  #end
#end
#if( !$expAdd.isEmpty() )
  #set( $expression = "$expression ADD" )
  #foreach( $entry in $expAdd.entrySet() )
    #set( $expression = "$expression $entry.key $entry.value" )
    #if( $foreach.hasNext() )
      #set( $expression = "$expression," )
    #end
  #end
#end
#if( !$expRemove.isEmpty() )
  #set( $expression = "$expression REMOVE" )
  #foreach( $entry in $expRemove )
    #set( $expression = "$expression $entry" )
    #if( $foreach.hasNext() )
      #set( $expression = "$expression," )
    #end
  #end
#end
#set( $update = {} )
$util.qr($update.put("expression", "$expression"))
#if( !$expNames.isEmpty() )
  $util.qr($update.put("expressionNames", $expNames))
#end
#if( !$expValues.isEmpty() )
  $util.qr($update.put("expressionValues", $expValues))
#end
{
  "version": "2018-05-29",
  "operation": "UpdateItem",
  "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
  "id": {
      "S": $util.toJson($context.args.input.id)
  }
} #end,
  "update": $util.toJson($update),
  "condition": $util.toJson($condition)
}
UnleashedMind commented 3 years ago

You can overwrite the auto generated resolvers https://docs.amplify.aws/cli/graphql-transformer/resolvers

michael-drury commented 3 years ago

Thanks for the reply @UnleashedMind! I'm aware that I can customise the VTL, I'm just not sure how to change it in such a way that allows me to achieve the desired results.