aws-amplify / amplify-cli

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development.
Apache License 2.0
2.81k stars 821 forks source link

Incrementing and Decrementing Numeric Attributes GraphQL #3681

Closed tfendt closed 4 years ago

tfendt commented 4 years ago

Which Category is your question related to? GraphQL

Amplify CLI Version 4.13.1

What AWS Services are you utilizing? Cognito, AppSync, DynamoDB, API Gateway, Lambda

Is it possible to increment or decrement a number in a graph model like you can do with the dynamodb SET Update Expression (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement)?

For example I have this model:

type ResetHistory @model @auth(
  rules: [ {allow: owner} ])
{
  id: ID!
  resetCount: Int!
  totalResetTime: Float!
}

I currently have a function that updates the dynamodb table directly using the following command:

...[snip]
const params = {
          TableName: `ResetHistory-${apiGraphQLAPIIdOutput}-${environment}`,
          Key: {
            id: unmarshalledNewRecord.id
          },
          UpdateExpression: 'SET resetCount = resetCount + :count, totalResetTime = totalResetTime + :resetTime',
          ExpressionAttributeValues: {
            ":count": 1,
            ":resetTime": parseFloat(resetTimeMinutes)
          },
        };
        try {
          //Write updates to agg table
          await documentClient.update(params).promise();
        ....[more code below]

This works but I need to create a subscription on this data for my app so I am looking to rewrite the function to execute an AppSync function (updateResetHistory) so it can trigger subscriptions. Is there a way to accomplish the same increment functionality within the amplify framework?

At the end of the day I really just need to set a field = field + [some number] but I am not seeing anything in the docs. I have been reading over posts about custom resolvers. Is that the only way to get this to work?

tfendt commented 4 years ago

Is this not possible with Amplify? It is such a simple operation to not be able to do.

SwaySway commented 4 years ago

Hi @tfendt if you are referring to setting this kind of increment type operation in the schema, a custom implementation of this could be done through a customer transformer. Another way would be to create a custom resolver which would include this set expression in the VTL code.

xitanggg commented 4 years ago

I am looking for this same feature as well. DynamoDB supports Atomic Counters. Can Amplify support this? It is such a common feature to keep track of upvotes, likes, etc that any app would use nowadays.

xitanggg commented 4 years ago

Just an idea and not sure if this would solve your issue (I think it might). Use @function and invoke the same function as Lambda resolver to perform the increment operation for resetCount and totalResetTime. This perhaps trigger subscription update, cause going directly to dynamoDB via a function wouldn't but @function should.

type ResetHistory @model @auth(
  rules: [ {allow: owner} ])
{
  id: ID!
  resetCount: Int! @function(name: "atomicAddResetCount-${env}")
  totalResetTime: Float! @function(name: "atomicAddTotalResetTime-${env}")
}
tfendt commented 4 years ago

Thanks @SwaySway I will give it a try.

@xitanggg looks interesting and easier than the other options. I'll give this a try first.

tfendt commented 4 years ago

@SwaySway I tried the following but for some reason when executing the mutation it doesn't trigger the subscription. Am I missing anything? I would expect calling the updateResetTime would then trigger the subscription. If it makes a difference I am calling updateResetTime from a lambda function.

type Mutation {
  updateResetTime(owner: String!, id: String!, resetCount: Int!, resetTime: Float!): String @function(name: "updateResetTime-${env}") @aws_iam
}

type Subscription {
  onUpdateResetTime(owner: String!, id: String!, resetCount: Int!, resetTime: Float!): String @aws_subscribe(mutations: ["updateResetTime"])
}
stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

egreenmachine commented 4 years ago

So I ended up creating a custom Mutation to do this. I made it pretty generic so it should help others. It's not great - but it is a start and should be able to help people. I have left out the authentication and some other boilerplate included with the default update Mutation. You can copy it from what is generated in the amplify/backend/api/<ApiName>/build/resolvers/Mutation.<operation>.req.vtl. You should see where to start inserting this code by looking at the boilerplate. You will want to put your custom file at amplify/backend/api/<ApiName>/resolvers/Mutation.<operation>.req.vtl so it gets built every time you build the graphql schema.

The code looks at the values provided and instead of overwriting numbers will add them to the value that is already there. So if you pass 5 it will add 5 to whatever value is already in the field. It works with negative numbers as well. The caveat is that ALL NUMERIC FIELDS ARE ADDITIVE. There is no way to simply override the value with these operations.

#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
#set( $expAdd = {} )
#set( $expRemove = [] )
#set( $expNumbers = [] )
#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"))
    #if ( $util.isNumber($entry.value) )
      $util.qr($expNumbers.add("#$entryKeyAttributeName"))
    #end
    $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() )
    #if ( $expNumbers.contains($entry.key) )
      #set( $expression = "$expression $entry.key = $entry.key + $entry.value" )
    #else
      #set( $expression = "$expression $entry.key = $entry.value" )
    #end
    #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": "2017-02-28",
  "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)
}
SwaySway commented 4 years ago

Closing this issue as it's not a bug. Should you have any other comments on discussion on this we recommend our community discord.

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels for those types of questions.