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

Appsync Mock: Datasource with a prefix of another Datasource name calls that one #153

Open mgarabedian opened 3 years ago

mgarabedian commented 3 years ago

Before opening, please confirm:

How did you install the Amplify CLI?

npm

If applicable, what version of Node.js are you using?

14.17.3

Amplify CLI Version

6.3.1

What operating system are you using?

Mac

Amplify Categories

api

Amplify Commands

Not applicable

Describe the bug

While using custom transformers and pipeline functions in a mock environment, I came across a bug. It seems as though mock is picking up the wrong datasource if the name prefixes are the same. See the following example datasource names:

If my custom mutation uses a pipeline function that calls DatasourceGetUserSettings, DatasourceGetUser is executed instead.

Expected behavior

The exact datasource with the name provided should be found and executed.

Reproduction steps

See above in description. Removing the 'e' from User, and changing the name to DatasourceGetUsrSettings, everything works again as expected.

GraphQL schema(s)

No response

Log output

``` # Put your logs below this line ```

Additional information

No response

josefaidt commented 2 years ago

Hey @mgarabedian :wave: thanks for raising this! I'm starting to take a look at this, however in the mean time would you mind providing a small example showing how the resolvers are set up?

josefaidt commented 2 years ago

Hey @mgarabedian just wanted to follow-up on this one to see if you are able to provide a small example showcasing how the resolvers are set up?

mgarabedian commented 2 years ago

Sure. I have DatasourceInvokeLambdaUser as an example. If you create another one also called DatasourceInvokeLambdaUserSettings, and had the pipeline function reference it, DatasourceInvokeLambdaUser would be called.

Resolver:

 "DeleteUserResolver": {
    "Type": "AWS::AppSync::Resolver",
    "Properties": {
        "ApiId": {
            "Ref": "GetAttGraphQLAPIApiId"
        },
        "FieldName": "deleteUser",
        "TypeName": "Mutation",
        "Kind": "PIPELINE",
        "PipelineConfig": {
            "Functions": [
                {
                    "Fn::GetAtt": [
                        "RandomPipelineFunction",
                        "FunctionId"
                    ]
                },
                {
                    "Fn::GetAtt": [
                        "InvokeLambdaUser",
                        "FunctionId"
                    ]
                }
            ]
        },
        "RequestMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Mutation",
                                "deleteUser",
                                "req",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        },
        "ResponseMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Mutation",
                                "deleteUser",
                                "res",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        }
    }
},

Pipeline FunctionConfiguration:


"InvokeLambdaUser": {
    "Type": "AWS::AppSync::FunctionConfiguration",
    "Properties": {
        "ApiId": {
            "Ref": "AppSyncApiId"
        },
        "Name": "InvokeLambdaUser",
        "DataSourceName": "InvokeLambdaUser",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "InvokeLambdaUser",
                                "req",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        },
        "ResponseMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "InvokeLambdaUser",
                                "res",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        }
    },
    "DependsOn": "DatasourceInvokeLambdaUser"
},

Lambda as Datasource:


"DatasourceInvokeLambdaUser": {
    "Type": "AWS::AppSync::DataSource",
    "Properties": {
        "ApiId": {
            "Ref": "AppSyncApiId"
        },
        "Name": "DatasourceInvokeLambdaUser",
        "Type": "AWS_LAMBDA",
        "ServiceRoleArn": {
            "Fn::GetAtt": [
                "UserLambdaRole",
                "Arn"
            ]
        },
        "LambdaConfig": {
            "LambdaFunctionArn": {
                "Fn::If": [
                    "HasEnvironmentParameter",
                    {
                        "Fn::Sub": [
                            "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:LambdaUser-${env}",
                            {
                                "env": {
                                    "Ref": "env"
                                }
                            }
                        ]
                    },
                    {
                        "Fn::Sub": [
                            "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:LambdaUser",
                            {}
                        ]
                    }
                ]
            }
        }
    },
    "DependsOn": "UserLambdaRole"
},
josefaidt commented 2 years ago

Hey @mgarabedian :wave: apologies for the delay! I was able to successfully reproduce this behavior.

CustomResources.json ```json { "AWSTemplateFormatVersion": "2010-09-09", "Description": "An auto-generated nested stack.", "Metadata": {}, "Parameters": { "AppSyncApiId": { "Type": "String", "Description": "The id of the AppSync API associated with this project." }, "AppSyncApiName": { "Type": "String", "Description": "The name of the AppSync API", "Default": "AppSyncSimpleTransform" }, "env": { "Type": "String", "Description": "The environment name. e.g. Dev, Test, or Production", "Default": "NONE" }, "S3DeploymentBucket": { "Type": "String", "Description": "The S3 bucket containing all deployment assets for the project." }, "S3DeploymentRootKey": { "Type": "String", "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." } }, "Resources": { "EmptyResource": { "Type": "Custom::EmptyResource", "Condition": "AlwaysFalse" }, "TodoDatasourceLambdaRole": { "Type": "AWS::IAM::Role", "Properties": { "RoleName": { "Fn::Sub": [ "TodoDatasourceLambdaRole-${env}", { "env": { "Ref": "env" } } ] }, "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Policies": [ { "PolicyName": "InvokeLambdaFunction", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["lambda:invokeFunction"], "Resource": [ { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:todo-${env}", { "env": { "Ref": "env" } } ] }, { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:todosettings-${env}", { "env": { "Ref": "env" } } ] } ] } ] } } ] } }, "CreateTodoResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "FieldName": "createTodo", "TypeName": "Mutation", "Kind": "PIPELINE", "PipelineConfig": { "Functions": [ { "Fn::GetAtt": ["InvokeLambdaTodo", "FunctionId"] } ] }, "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [".", ["Mutation", "createTodo", "req", "vtl"]] } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [".", ["Mutation", "createTodo", "res", "vtl"]] } } ] } } }, "CreateTodoSettingsResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "FieldName": "createTodoSettings", "TypeName": "Mutation", "Kind": "PIPELINE", "PipelineConfig": { "Functions": [ { "Fn::GetAtt": ["InvokeLambdaTodoSettings", "FunctionId"] } ] }, "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [ ".", ["Mutation", "createTodoSettings", "req", "vtl"] ] } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [ ".", ["Mutation", "createTodoSettings", "res", "vtl"] ] } } ] } } }, "DatasourceInvokeLambdaTodo": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "DatasourceInvokeLambdaTodo", "Type": "AWS_LAMBDA", "ServiceRoleArn": { "Fn::GetAtt": ["TodoDatasourceLambdaRole", "Arn"] }, "LambdaConfig": { "LambdaFunctionArn": { "Fn::If": [ "HasEnvironmentParameter", { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:todo-${env}", { "env": { "Ref": "env" } } ] }, { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:todo", {} ] } ] } } } }, "DatasourceInvokeLambdaTodoSettings": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "DatasourceInvokeLambdaTodoSettings", "Type": "AWS_LAMBDA", "ServiceRoleArn": { "Fn::GetAtt": ["TodoDatasourceLambdaRole", "Arn"] }, "LambdaConfig": { "LambdaFunctionArn": { "Fn::If": [ "HasEnvironmentParameter", { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:todosettings-${env}", { "env": { "Ref": "env" } } ] }, { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:todo", {} ] } ] } } } }, "InvokeLambdaTodo": { "Type": "AWS::AppSync::FunctionConfiguration", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "InvokeLambdaTodo", "DataSourceName": "DatasourceInvokeLambdaTodo", "FunctionVersion": "2018-05-29", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [".", ["InvokeLambdaTodo", "req", "vtl"]] } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [".", ["InvokeLambdaTodo", "res", "vtl"]] } } ] } }, "DependsOn": "DatasourceInvokeLambdaTodo" }, "InvokeLambdaTodoSettings": { "Type": "AWS::AppSync::FunctionConfiguration", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "InvokeLambdaTodoSettings", "DataSourceName": "DatasourceInvokeLambdaTodoSettings", "FunctionVersion": "2018-05-29", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [".", ["InvokeLambdaTodoSettings", "req", "vtl"]] } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }, "ResolverFileName": { "Fn::Join": [".", ["InvokeLambdaTodoSettings", "res", "vtl"]] } } ] } }, "DependsOn": "DatasourceInvokeLambdaTodoSettings" } }, "Conditions": { "HasEnvironmentParameter": { "Fn::Not": [ { "Fn::Equals": [ { "Ref": "env" }, "NONE" ] } ] }, "AlwaysFalse": { "Fn::Equals": ["true", "false"] } }, "Outputs": { "EmptyOutput": { "Description": "An empty output. You may delete this if you have at least one resource above.", "Value": "" } } } ```
GraphQL Schema ```graphql type Todo @model { id: ID! name: String! description: String settings: TodoSettings @hasOne } type TodoSettings @model { id: ID! name: String } ```

Where our InvokeLambdaTodo*.req.vtl templates are:

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "field": "createTodo",
        "arguments":  $utils.toJson($context.arguments)
    }
}

And InvokeLambdaTodo*.res.vtl

$util.toJson($ctx.prev.result)

Finally, with our two Lambda functions todo and todosettings we can add a console.log statement:

exports.handler = async (event) => {
  console.log('hello from todo')
  const payload = {
    id: '1234',
    name: 'John Doe',
  }
  const response = {
    statusCode: 200,
    body: JSON.stringify(payload),
  }
  return response
}

And upon running the mock server with amplify mock api and attempting to run mutation createTodoSettings..., we can see the console.log statement from the todo lambda in the console.

Marking as a bug šŸ™‚