Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to override Data Resolver functions in AmplifyGraphqlApi ? #2467

Open
harshit9715 opened this issue Apr 17, 2024 · 5 comments
Open

How to override Data Resolver functions in AmplifyGraphqlApi ? #2467

harshit9715 opened this issue Apr 17, 2024 · 5 comments
Labels
pending-triage question Further information is requested

Comments

@harshit9715
Copy link

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//resolvers folder with the same name.

@harshit9715 harshit9715 added pending-triage question Further information is requested labels Apr 17, 2024
@harshit9715
Copy link
Author

harshit9715 commented Apr 17, 2024

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.

@renebrandel
Copy link
Contributor

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

@harshit9715
Copy link
Author

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
Copy link
Author

Here is a quick video.

Screen.Recording.2024-04-17.at.7.01.39.PM.mov

@harshit9715
Copy link
Author

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"
  );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pending-triage question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants