Direct Lambda Resolver. A custom AppSync Resolver to bypass the use of Apache Velocity Template (VTL) and automatically map your function's response to a GraphQL field.
Amplify GraphQL Transformer. Custom GraphQL directives to define your application's data model using Schema Definition Language (SDL). Amplify CLI uses these directives to convert GraphQL SDL into full descriptive AWS CloudFormation templates.
You must have an existing AppSync GraphQL API and IAM permissions to invoke your Lambda function. That said, there is no additional permissions to use this utility.
This is the sample infrastructure we are using for the initial examples with a AppSync Direct Lambda Resolver.
Tip: Designing GraphQL Schemas for the first time?
AWSTemplateFormatVersion:'2010-09-09'Transform:AWS::Serverless-2016-10-31Description:Hello world Direct Lambda ResolverGlobals:Function:Timeout:5Runtime:python3.8Tracing:ActiveEnvironment:Variables:# Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/latest/#environment-variablesLOG_LEVEL:INFOPOWERTOOLS_LOGGER_SAMPLE_RATE:0.1POWERTOOLS_LOGGER_LOG_EVENT:truePOWERTOOLS_SERVICE_NAME:sample_resolverResources:HelloWorldFunction:Type:AWS::Serverless::FunctionProperties:Handler:app.lambda_handlerCodeUri:hello_worldDescription:Sample Lambda Powertools Direct Lambda ResolverTags:SOLUTION:LambdaPowertoolsPython# IAM Permissions and RolesAppSyncServiceRole:Type:"AWS::IAM::Role"Properties:AssumeRolePolicyDocument:Version:"2012-10-17"Statement:-Effect:"Allow"Principal:Service:-"appsync.amazonaws.com"Action:-"sts:AssumeRole"InvokeLambdaResolverPolicy:Type:"AWS::IAM::Policy"Properties:PolicyName:"DirectAppSyncLambda"PolicyDocument:Version:"2012-10-17"Statement:-Effect:"Allow"Action:"lambda:invokeFunction"Resource:-!GetAttHelloWorldFunction.ArnRoles:-!RefAppSyncServiceRole# GraphQL APIHelloWorldApi:Type:"AWS::AppSync::GraphQLApi"Properties:Name:HelloWorldApiAuthenticationType:"API_KEY"XrayEnabled:trueHelloWorldApiKey:Type:AWS::AppSync::ApiKeyProperties:ApiId:!GetAttHelloWorldApi.ApiIdHelloWorldApiSchema:Type:"AWS::AppSync::GraphQLSchema"Properties:ApiId:!GetAttHelloWorldApi.ApiIdDefinition:|schema {query:Query}type Query {getTodo(id: ID!): TodolistTodos: [Todo]}type Todo {id: ID!title: Stringdescription: Stringdone: Boolean}# Lambda Direct Data Source and ResolverHelloWorldFunctionDataSource:Type:"AWS::AppSync::DataSource"Properties:ApiId:!GetAttHelloWorldApi.ApiIdName:"HelloWorldLambdaDirectResolver"Type:"AWS_LAMBDA"ServiceRoleArn:!GetAttAppSyncServiceRole.ArnLambdaConfig:LambdaFunctionArn:!GetAttHelloWorldFunction.ArnListTodosResolver:Type:"AWS::AppSync::Resolver"Properties:ApiId:!GetAttHelloWorldApi.ApiIdTypeName:"Query"FieldName:"listTodos"DataSourceName:!GetAttHelloWorldFunctionDataSource.NameGetTodoResolver:Type:"AWS::AppSync::Resolver"Properties:ApiId:!GetAttHelloWorldApi.ApiIdTypeName:"Query"FieldName:"getTodo"DataSourceName:!GetAttHelloWorldFunctionDataSource.NameOutputs:HelloWorldFunction:Description:"HelloWorldLambdaFunctionARN"Value:!GetAttHelloWorldFunction.ArnHelloWorldAPI:Value:!GetAttHelloWorldApi.Arn
You can define your functions to match GraphQL types and fields with the app.resolver() decorator.
Here's an example where we have two separate functions to resolve getTodo and listTodos fields within the Query type. For completion, we use Scalar type utilities to generate the right output based on our schema definition.
Info
GraphQL arguments are passed as function arguments.
fromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.event_handlerimportAppSyncResolverfromaws_lambda_powertools.utilities.data_classes.appsyncimportscalar_types_utilstracer=Tracer(service="sample_resolver")logger=Logger(service="sample_resolver")app=AppSyncResolver()# Note that `creation_time` isn't available in the schema# This utility also takes into account what info you make available at API level vs what's storedTODOS=[{"id":scalar_types_utils.make_id(),# type ID or String"title":"First task","description":"String","done":False,"creation_time":scalar_types_utils.aws_datetime(),# type AWSDateTime},{"id":scalar_types_utils.make_id(),"title":"Second task","description":"String","done":True,"creation_time":scalar_types_utils.aws_datetime(),},]@app.resolver(type_name="Query",field_name="getTodo")defget_todo(id:str=""):logger.info(f"Fetching Todo {id}")todo=[todofortodoinTODOSiftodo["id"]==id]returntodo@app.resolver(type_name="Query",field_name="listTodos")deflist_todos():returnTODOS@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER)@tracer.capture_lambda_handlerdeflambda_handler(event,context):returnapp.resolve(event,context)
Amplify CLI generated functions use Pipenv as a dependency manager. Your function source code is located at amplify/backend/function/your-function-name.
Within your function's folder, add Lambda Powertools as a dependency with pipenv install aws-lambda-powertools.
Use the following code for merchantInfo and searchMerchant functions respectively.
1 2 3 4 5 6 7 8 91011121314151617181920212223
fromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.event_handlerimportAppSyncResolverfromaws_lambda_powertools.utilities.data_classes.appsyncimportscalar_types_utilstracer=Tracer(service="sample_graphql_transformer_resolver")logger=Logger(service="sample_graphql_transformer_resolver")app=AppSyncResolver()@app.resolver(type_name="Query",field_name="listLocations")deflist_locations(page:int=0,size:int=10):return[{"id":100,"name":"Smooth Grooves"}]@app.resolver(field_name="commonField")defcommon_field():# Would match all fieldNames matching 'commonField'returnscalar_types_utils.make_id()@tracer.capture_lambda_handler@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER)deflambda_handler(event,context):app.resolve(event,context)
As you grow the number of related GraphQL operations a given Lambda function should handle, it is natural to split them into separate files to ease maintenance - That's where the Router feature is useful.
Let's assume you have app.py as your Lambda function entrypoint and routes in location.py, this is how you'd use the Router feature.
We import Router instead of AppSyncResolver; syntax wise is exactly the same.
1 2 3 4 5 6 7 8 9101112131415161718
fromtypingimportAny,Dict,Listfromaws_lambda_powertoolsimportLoggerfromaws_lambda_powertools.event_handler.appsyncimportRouterlogger=Logger(child=True)router=Router()@router.resolver(type_name="Query",field_name="listLocations")deflist_locations(merchant_id:str)->List[Dict[str,Any]]:return[{"name":"Location name","merchant_id":merchant_id}]@router.resolver(type_name="Location",field_name="status")defresolve_status(merchant_id:str)->str:logger.debug(f"Resolve status for merchant_id: {merchant_id}")return"FOO"
We use include_router method and include all location operations registered in the router global object.
You can test your resolvers by passing a mocked or actual AppSync Lambda event that you're expecting.
You can use either app.resolve(event, context) or simply app(event, context).
Here's an example of how you can test your synchronous resolvers:
1 2 3 4 5 6 7 8 910111213141516
importjsonimportpytestfrompathlibimportPathfromsrc.indeximportapp# import the instance of AppSyncResolver from your codedeftest_direct_resolver():# Load mock event from a filejson_file_path=Path("appSyncDirectResolver.json")withopen(json_file_path)asjson_file:mock_event=json.load(json_file)# Call the implicit handlerresult=app(mock_event,{})assertresult=="created this value"
1234567
fromaws_lambda_powertools.event_handlerimportAppSyncResolverapp=AppSyncResolver()@app.resolver(field_name="createSomething")defcreate_something():return"created this value"
{"arguments":{"id":"my identifier"},"identity":{"claims":{"sub":"192879fc-a240-4bf1-ab5a-d6a00f3063f9","email_verified":true,"iss":"https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx","phone_number_verified":false,"cognito:username":"jdoe","aud":"7471s60os7h0uu77i1tk27sp9n","event_id":"bc334ed8-a938-4474-b644-9547e304e606","token_use":"id","auth_time":1599154213,"phone_number":"+19999999999","exp":1599157813,"iat":1599154213,"email":"jdoe@email.com"},"defaultAuthStrategy":"ALLOW","groups":null,"issuer":"https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx","sourceIp":["1.1.1.1"],"sub":"192879fc-a240-4bf1-ab5a-d6a00f3063f9","username":"jdoe"},"source":null,"request":{"headers":{"x-forwarded-for":"1.1.1.1, 2.2.2.2","cloudfront-viewer-country":"US","cloudfront-is-tablet-viewer":"false","via":"2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)","cloudfront-forwarded-proto":"https","origin":"https://us-west-1.console.aws.amazon.com","content-length":"217","accept-language":"en-US,en;q=0.9","host":"xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com","x-forwarded-proto":"https","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36","accept":"*/*","cloudfront-is-mobile-viewer":"false","cloudfront-is-smarttv-viewer":"false","accept-encoding":"gzip, deflate, br","referer":"https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1","content-type":"application/json","sec-fetch-mode":"cors","x-amz-cf-id":"3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==","x-amzn-trace-id":"Root=1-5f512f51-fac632066c5e848ae714","authorization":"eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...","sec-fetch-dest":"empty","x-amz-user-agent":"AWS-Console-AppSync/","cloudfront-is-desktop-viewer":"true","sec-fetch-site":"cross-site","x-forwarded-port":"443"}},"prev":null,"info":{"selectionSetList":["id","field1","field2"],"selectionSetGraphQL":"{\n id\n field1\n field2\n}","parentTypeName":"Mutation","fieldName":"createSomething","variables":{}},"stash":{}}
And an example for testing asynchronous resolvers. Note that this requires the pytest-asyncio package:
1 2 3 4 5 6 7 8 91011121314151617
importjsonimportpytestfrompathlibimportPathfromsrc.indeximportapp# import the instance of AppSyncResolver from your code@pytest.mark.asyncioasyncdeftest_direct_resolver():# Load mock event from a filejson_file_path=Path("appSyncDirectResolver.json")withopen(json_file_path)asjson_file:mock_event=json.load(json_file)# Call the implicit handlerresult=awaitapp(mock_event,{})assertresult=="created this value"
1 2 3 4 5 6 7 8 910
importasynciofromaws_lambda_powertools.event_handlerimportAppSyncResolverapp=AppSyncResolver()@app.resolver(field_name="createSomething")asyncdefcreate_something_async():awaitasyncio.sleep(1)# Do async stuffreturn"created this value"