amplify-category-api icon indicating copy to clipboard operation
amplify-category-api copied to clipboard

How to override Data Resolver functions in AmplifyGraphqlApi ?

Open harshit9715 opened this issue 10 months ago • 5 comments

Amplify CLI Version

12.11.0

Question

I am using the CDK construct for building the graphql schema.

const graphApi = new AmplifyGraphqlApi(stack, "GraphqlApi", {
    functionSlots: [...getCustomSlots],
    definition: graphQLDefinition,
    apiName: "GraphqlApi",
    translationBehavior: {},
    transformerPlugins: [],
    authorizationModes: {
      apiKeyConfig: {
        expires: Duration.days(365),
        description: "API Key for GraphQL API - " + stack.stage,
      },
      defaultAuthorizationMode: "OPENID_CONNECT",
      oidcConfig: {
        oidcIssuerUrl: process.env.CLERK_ISSUER_BASE_URL!,
        oidcProviderName: "Clerk",
        tokenExpiryFromAuth: Duration.millis(0),
        tokenExpiryFromIssue: Duration.millis(0),
      },
    },
  });

I want to know if there is a way of overriding some of the auto-generated resolvers?

This option is available through amplify cli. we just need to place the edited resolvers in the amplify/backend/api/<apiName>/resolvers folder with the same name.

harshit9715 avatar Apr 17 '24 00:04 harshit9715

const graphApi = new AmplifyGraphqlApi(stack, "GraphqlApi", {
    functionSlots: [...getCreateSlots],
    definition: graphQLDefinition,
    apiName: "GraphqlApi",
    translationBehavior: {},
    transformerPlugins: [],
    authorizationModes: {
      apiKeyConfig: {
        expires: Duration.days(365),
        description: "API Key for GraphQL API - " + stack.stage,
      },
      defaultAuthorizationMode: "OPENID_CONNECT",
      oidcConfig: {
        oidcIssuerUrl: process.env.CLERK_ISSUER_BASE_URL!,
        oidcProviderName: "Clerk",
        tokenExpiryFromAuth: Duration.millis(0),
        tokenExpiryFromIssue: Duration.millis(0),
      },
    },
  });


  const MutationUpdateQuestionDataResolverFn = new Asset(
    stack,
    "ModifiedResolvers",
    {
      path: path.join("stacks/resolvers/Mutation.updateQuestion.req.vtl"),
    }
  );
  
  graphApi.resources.cfnResources.cfnFunctionConfigurations[
    "MutationUpdateQuestionDataResolverFn"
  ].requestMappingTemplateS3Location =
    MutationUpdateQuestionDataResolverFn.s3ObjectUrl;

This is what I am using but it requires management overhead. It would be great if we can use the same slot behavior to override the data resolvers too.

harshit9715 avatar Apr 17 '24 11:04 harshit9715

Hi @harshit9715 - can you provide more context on the type of updates you'd like to override?

renebrandel avatar Apr 17 '24 13:04 renebrandel

Sure!

I am trying to implement atomic counters for certain fields.

type Question @model {
id: ID!
content: String!
upvoteCount: Int! @default("0") 
downvoteCount: Int! @default("0") 
viewCount: Int! @default("0") 
}

Now the vtl template does not support atomic counters out of the box but then I noticed the generated code is very close to what I need.

Basically, if there is a fieldName ending with Count, i am overriding its behaviour on update mutation.

Instead of using $expSet.put i am using $expAdd.put. This way I can simply add the value entered to the existing value. and I believe it would be write consistent. work with subscriptions and also return right value incase the value changes in the backend.

TLDR;

The changed lines

from this

#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

To this

#if( $util.isNull($entry.value) )
    #set( $discard = $expRemove.add("#$entryKeyAttributeName") )
    $util.qr($expNames.put("#$entryKeyAttributeName", "$entry.key"))
  #else
    #if ( $entry.key.endsWith("Count"))
      #if ( $entry.value != 1 && $entry.value != -1) ## this condition makes sure that if the client sent anything other than +1 or -1, then no change on the value will occur.
        #set( $entry.value = 0 )
      #end
      $util.qr($expAdd.put("#$entryKeyAttributeName", ":$entryKeyAttributeName")) ## NOTICE THE expAdd USED ON THIS LINE.
  	#else
      $util.qr($expSet.put("#$entryKeyAttributeName", ":$entryKeyAttributeName"))
  	#end
    ## $util.qr($expSet.put("#$entryKeyAttributeName", ":$entryKeyAttributeName"))
    $util.qr($expNames.put("#$entryKeyAttributeName", "$entry.key"))
    $util.qr($expValues.put(":$entryKeyAttributeName", $util.dynamodb.toDynamoDB($entry.value)))
  #end

Full code

Here is what was generated for Mutation.updateQuestion.req.vtl

## [Start] Mutation Update resolver. **
#set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) )
## Set the default values to put request **
#set( $mergedValues = $util.defaultIfNull($ctx.stash.defaultValues, {}) )
## copy the values from input **
$util.qr($mergedValues.putAll($util.defaultIfNull($args.input, {})))
## set the typename **
## Initialize the vars for creating ddb expression **
#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
#set( $expAdd = {} )
#set( $expRemove = [] )
#if( $ctx.stash.metadata.modelObjectKey )
  #set( $Key = $ctx.stash.metadata.modelObjectKey )
#else
  #set( $Key = {
  "id":   $util.dynamodb.toDynamoDB($args.input.id)
} )
#end
## Model key **
#if( $ctx.stash.metadata.modelObjectKey )
  #set( $keyFields = [] )
  #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() )
    $util.qr($keyFields.add("$entry.key"))
  #end
#else
  #set( $keyFields = ["id"] )
#end
#foreach( $entry in $util.map.copyAndRemoveAllKeys($mergedValues, $keyFields).entrySet() )
  #if( !$util.isNull($ctx.stash.metadata.dynamodbNameOverrideMap) && $ctx.stash.metadata.dynamodbNameOverrideMap.containsKey("$entry.key") )
    #set( $entryKeyAttributeName = $ctx.stash.metadata.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
## Begin - key condition **
#if( $ctx.stash.metadata.modelObjectKey )
  #set( $keyConditionExpr = {} )
  #set( $keyConditionExprNames = {} )
  #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() )
    $util.qr($keyConditionExpr.put("keyCondition$velocityCount", {
  "attributeExists": true
}))
    $util.qr($keyConditionExprNames.put("#keyCondition$velocityCount", "$entry.key"))
  #end
  $util.qr($ctx.stash.conditions.add($keyConditionExpr))
#else
  $util.qr($ctx.stash.conditions.add({
  "id": {
      "attributeExists": true
  }
}))
#end
## End - key condition **
#if( $args.condition )
  $util.qr($ctx.stash.conditions.add($args.condition))
#end
## Start condition block **
#if( $ctx.stash.conditions && $ctx.stash.conditions.size() != 0 )
  #set( $mergedConditions = {
  "and": $ctx.stash.conditions
} )
  #set( $Conditions = $util.parseJson($util.transform.toDynamoDBConditionExpression($mergedConditions)) )
  #if( $Conditions.expressionValues && $Conditions.expressionValues.size() == 0 )
    #set( $Conditions = {
  "expression": $Conditions.expression,
  "expressionNames": $Conditions.expressionNames
} )
  #end
  ## End condition block **
#end
#set( $UpdateItem = {
  "version": "2018-05-29",
  "operation": "UpdateItem",
  "key": $Key,
  "update": $update
} )
#if( $Conditions )
  #if( $keyConditionExprNames )
    $util.qr($Conditions.expressionNames.putAll($keyConditionExprNames))
  #end
  $util.qr($UpdateItem.put("condition", $Conditions))
#end
$util.toJson($UpdateItem)
## [End] Mutation Update resolver. **

and here is what I override the template with.

## [Start] Mutation Update resolver. **
#set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) )
## Set the default values to put request **
#set( $mergedValues = $util.defaultIfNull($ctx.stash.defaultValues, {}) )
## copy the values from input **
$util.qr($mergedValues.putAll($util.defaultIfNull($args.input, {})))
## set the typename **
## Initialize the vars for creating ddb expression **
#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
#set( $expAdd = {} )
#set( $expRemove = [] )
#if( $ctx.stash.metadata.modelObjectKey )
  #set( $Key = $ctx.stash.metadata.modelObjectKey )
#else
  #set( $Key = {
  "id":   $util.dynamodb.toDynamoDB($args.input.id)
} )
#end
## Model key **
#if( $ctx.stash.metadata.modelObjectKey )
  #set( $keyFields = [] )
  #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() )
    $util.qr($keyFields.add("$entry.key"))
  #end
#else
  #set( $keyFields = ["id"] )
#end
#foreach( $entry in $util.map.copyAndRemoveAllKeys($mergedValues, $keyFields).entrySet() )
  #if( !$util.isNull($ctx.stash.metadata.dynamodbNameOverrideMap) && $ctx.stash.metadata.dynamodbNameOverrideMap.containsKey("$entry.key") )
    #set( $entryKeyAttributeName = $ctx.stash.metadata.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
    #if ( $entry.key.endsWith("Count"))
      #if ( $entry.value != 1 && $entry.value != -1)
        #set( $entry.value = 0 )
      #end
      $util.qr($expAdd.put("#$entryKeyAttributeName", ":$entryKeyAttributeName"))
  	#else
      $util.qr($expSet.put("#$entryKeyAttributeName", ":$entryKeyAttributeName"))
  	#end
    ## $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
## Begin - key condition **
#if( $ctx.stash.metadata.modelObjectKey )
  #set( $keyConditionExpr = {} )
  #set( $keyConditionExprNames = {} )
  #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() )
    $util.qr($keyConditionExpr.put("keyCondition$velocityCount", {
  "attributeExists": true
}))
    $util.qr($keyConditionExprNames.put("#keyCondition$velocityCount", "$entry.key"))
  #end
  $util.qr($ctx.stash.conditions.add($keyConditionExpr))
#else
  $util.qr($ctx.stash.conditions.add({
  "id": {
      "attributeExists": true
  }
}))
#end
## End - key condition **
#if( $args.condition )
  $util.qr($ctx.stash.conditions.add($args.condition))
#end
## Start condition block **
#if( $ctx.stash.conditions && $ctx.stash.conditions.size() != 0 )
  #set( $mergedConditions = {
  "and": $ctx.stash.conditions
} )
  #set( $Conditions = $util.parseJson($util.transform.toDynamoDBConditionExpression($mergedConditions)) )
  #if( $Conditions.expressionValues && $Conditions.expressionValues.size() == 0 )
    #set( $Conditions = {
  "expression": $Conditions.expression,
  "expressionNames": $Conditions.expressionNames
} )
  #end
  ## End condition block **
#end
#set( $UpdateItem = {
  "version": "2018-05-29",
  "operation": "UpdateItem",
  "key": $Key,
  "update": $update
} )
#if( $Conditions )
  #if( $keyConditionExprNames )
    $util.qr($Conditions.expressionNames.putAll($keyConditionExprNames))
  #end
  $util.qr($UpdateItem.put("condition", $Conditions))
#end
$util.toJson($UpdateItem)
## [End] Mutation Update resolver. **

harshit9715 avatar Apr 17 '24 13:04 harshit9715

Here is a quick video.

https://github.com/aws-amplify/amplify-category-api/assets/55243567/5b41a335-9eb7-463f-a55a-771ea36af50b

harshit9715 avatar Apr 17 '24 13:04 harshit9715

const graphApi = new AmplifyGraphqlApi(stack, "GraphqlApi", {
    functionSlots: [...getCreateSlots],
    definition: graphQLDefinition,
    apiName: "GraphqlApi",
    translationBehavior: {},
    transformerPlugins: [],
    authorizationModes: {
      apiKeyConfig: {
        expires: Duration.days(365),
        description: "API Key for GraphQL API - " + stack.stage,
      },
      defaultAuthorizationMode: "OPENID_CONNECT",
      oidcConfig: {
        oidcIssuerUrl: process.env.CLERK_ISSUER_BASE_URL!,
        oidcProviderName: "Clerk",
        tokenExpiryFromAuth: Duration.millis(0),
        tokenExpiryFromIssue: Duration.millis(0),
      },
    },
  });


  const MutationUpdateQuestionDataResolverFn = new Asset(
    stack,
    "ModifiedResolvers",
    {
      path: path.join("stacks/resolvers/Mutation.updateQuestion.req.vtl"),
    }
  );
  
  graphApi.resources.cfnResources.cfnFunctionConfigurations[
    "MutationUpdateQuestionDataResolverFn"
  ].requestMappingTemplateS3Location =
    MutationUpdateQuestionDataResolverFn.s3ObjectUrl;

This is what I am using but it requires management overhead. It would be great if we can use the same slot behavior to override the data resolvers too.

And I realised that the construct can use the local template if it is available. Here is the updated code.

const resolverConfig = graphApi.resources.cfnResources.cfnFunctionConfigurations[functionName];
  if (!resolverConfig) return;
  resolverConfig.requestMappingTemplateS3Location = undefined;
  resolverConfig.requestMappingTemplate = fs.readFileSync(
    path.join("stacks/resolvers", filePath),
    "utf8"
  );

harshit9715 avatar Apr 17 '24 13:04 harshit9715

Hi @harshit9715, this is intended workflow for overriding when using the construct directly. I'll leave this open as a feature request.

dpilch avatar Jun 03 '24 19:06 dpilch