diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a614f78968..6a41e0d945c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,6 @@ repos: hooks: - id: check-merge-conflict - id: trailing-whitespace - - id: end-of-file-fixer - id: check-toml - repo: local hooks: diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 6d8f441d661..6943d6ed9bb 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -387,7 +387,7 @@ You can instruct API Gateway handler to use a custom serializer to best suit you As you grow the number of routes a given Lambda function should handle, it is natural to split routes 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 `split_route_module.py`, this is how you'd use the `Router` feature. +Let's assume you have `split_route.py` as your Lambda function entrypoint and routes in `split_route_module.py`. This is how you'd use the `Router` feature. === "split_route_module.py" diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index f3203e37834..4d28b41a81f 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -5,7 +5,7 @@ description: Core utility Event handler for AWS AppSync Direct Lambda Resolver and Amplify GraphQL Transformer. -### Key Features +## Key Features * Automatically parse API arguments to function arguments * Choose between strictly match a GraphQL field name or all of them to a function @@ -30,144 +30,16 @@ This is the sample infrastructure we are using for the initial examples with a A ???+ tip "Tip: Designing GraphQL Schemas for the first time?" Visit [AWS AppSync schema documentation](https://docs.aws.amazon.com/appsync/latest/devguide/designing-your-schema.html){target="_blank"} for understanding how to define types, nesting, and pagination. -=== "schema.graphql" +=== "getting_started_schema.graphql" ```typescript - --8<-- "docs/shared/getting_started_schema.graphql" + --8<-- "examples/event_handler_graphql/src/getting_started_schema.graphql" ``` === "template.yml" - ```yaml hl_lines="37-42 50-55 61-62 78-91 96-120" - AWSTemplateFormatVersion: '2010-09-09' - Transform: AWS::Serverless-2016-10-31 - Description: Hello world Direct Lambda Resolver - - Globals: - Function: - Timeout: 5 - Runtime: python3.8 - Tracing: Active - Environment: - Variables: - # Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/latest/#environment-variables - LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true - POWERTOOLS_SERVICE_NAME: sample_resolver - - Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - Handler: app.lambda_handler - CodeUri: hello_world - Description: Sample Lambda Powertools Direct Lambda Resolver - Tags: - SOLUTION: LambdaPowertoolsPython - - # IAM Permissions and Roles - - AppSyncServiceRole: - 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: - - !GetAtt HelloWorldFunction.Arn - Roles: - - !Ref AppSyncServiceRole - - # GraphQL API - - HelloWorldApi: - Type: "AWS::AppSync::GraphQLApi" - Properties: - Name: HelloWorldApi - AuthenticationType: "API_KEY" - XrayEnabled: true - - HelloWorldApiKey: - Type: AWS::AppSync::ApiKey - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - - HelloWorldApiSchema: - Type: "AWS::AppSync::GraphQLSchema" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - Definition: | - schema { - query:Query - } - - type Query { - getTodo(id: ID!): Todo - listTodos: [Todo] - } - - type Todo { - id: ID! - title: String - description: String - done: Boolean - } - - # Lambda Direct Data Source and Resolver - - HelloWorldFunctionDataSource: - Type: "AWS::AppSync::DataSource" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - Name: "HelloWorldLambdaDirectResolver" - Type: "AWS_LAMBDA" - ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn - LambdaConfig: - LambdaFunctionArn: !GetAtt HelloWorldFunction.Arn - - ListTodosResolver: - Type: "AWS::AppSync::Resolver" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - TypeName: "Query" - FieldName: "listTodos" - DataSourceName: !GetAtt HelloWorldFunctionDataSource.Name - - GetTodoResolver: - Type: "AWS::AppSync::Resolver" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - TypeName: "Query" - FieldName: "getTodo" - DataSourceName: !GetAtt HelloWorldFunctionDataSource.Name - - - Outputs: - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldAPI: - Value: !GetAtt HelloWorldApi.Arn + ```yaml hl_lines="59-60 71-72 94-95 104-105 112-113" + --8<-- "examples/event_handler_graphql/sam/template.yaml" ``` ### Resolver decorator @@ -176,248 +48,86 @@ You can define your functions to match GraphQL types and fields with the `app.re 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. +???+ important + GraphQL arguments are passed as function keyword arguments. -=== "app.py" + **Example** - ```python hl_lines="3-5 9 31-32 39-40 47" - from aws_lambda_powertools import Logger, Tracer + The GraphQL Query `getTodo(id: "todo_id_value")` will + call `get_todo` as `get_todo(id="todo_id_value")`. - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils +=== "getting_started_graphql_api_resolver.py" - tracer = Tracer(service="sample_resolver") - logger = Logger(service="sample_resolver") - app = AppSyncResolver() + ```python hl_lines="7 13 23 25-26 35 37 48" + --8<-- "examples/event_handler_graphql/src/getting_started_graphql_api_resolver.py" + ``` - # 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 stored - TODOS = [ - { - "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(), - }, - ] +=== "getting_started_schema.graphql" + ```typescript hl_lines="6-7" + --8<-- "examples/event_handler_graphql/src/getting_started_schema.graphql" + ``` - @app.resolver(type_name="Query", field_name="getTodo") - def get_todo(id: str = ""): - logger.info(f"Fetching Todo {id}") - todo = [todo for todo in TODOS if todo["id"] == id] +=== "getting_started_get_todo.json" - return todo + ```json hl_lines="2-3" + --8<-- "examples/event_handler_graphql/src/getting_started_get_todo.json" + ``` +=== "getting_started_list_todos.json" - @app.resolver(type_name="Query", field_name="listTodos") - def list_todos(): - return TODOS + ```json hl_lines="2 40 42" + --8<-- "examples/event_handler_graphql/src/getting_started_list_todos.json" + ``` +### Scalar functions - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` +When working with [AWS AppSync Scalar types](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html){target="_blank"}, you might want to generate the same values for data validation purposes. -=== "schema.graphql" +For convenience, the most commonly used values are available as functions within `scalar_types_utils` module. - ```typescript - --8<-- "docs/shared/getting_started_schema.graphql" - ``` +```python hl_lines="1-6" title="Creating key scalar values with scalar_types_utils" +--8<-- "examples/event_handler_graphql/src/scalar_functions.py" +``` -=== "getTodo_event.json" - - ```json - { - "arguments": { - "id": "7e362732-c8cd-4405-b090-144ac9b38960" - }, - "identity": null, - "source": null, - "request": { - "headers": { - "x-forwarded-for": "1.2.3.4, 5.6.7.8", - "accept-encoding": "gzip, deflate, br", - "cloudfront-viewer-country": "NL", - "cloudfront-is-tablet-viewer": "false", - "referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1", - "via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)", - "cloudfront-forwarded-proto": "https", - "origin": "https://eu-west-1.console.aws.amazon.com", - "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq", - "content-type": "application/json", - "x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494", - "x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==", - "content-length": "114", - "x-amz-user-agent": "AWS-Console-AppSync/", - "x-forwarded-proto": "https", - "host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com", - "accept-language": "en-US,en;q=0.5", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0", - "cloudfront-is-desktop-viewer": "true", - "cloudfront-is-mobile-viewer": "false", - "accept": "*/*", - "x-forwarded-port": "443", - "cloudfront-is-smarttv-viewer": "false" - } - }, - "prev": null, - "info": { - "parentTypeName": "Query", - "selectionSetList": [ - "title", - "id" - ], - "selectionSetGraphQL": "{\n title\n id\n}", - "fieldName": "getTodo", - "variables": {} - }, - "stash": {} - } - ``` +Here's a table with their related scalar as a quick reference: -=== "listTodos_event.json" - - ```json - { - "arguments": {}, - "identity": null, - "source": null, - "request": { - "headers": { - "x-forwarded-for": "1.2.3.4, 5.6.7.8", - "accept-encoding": "gzip, deflate, br", - "cloudfront-viewer-country": "NL", - "cloudfront-is-tablet-viewer": "false", - "referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1", - "via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)", - "cloudfront-forwarded-proto": "https", - "origin": "https://eu-west-1.console.aws.amazon.com", - "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq", - "content-type": "application/json", - "x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494", - "x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==", - "content-length": "114", - "x-amz-user-agent": "AWS-Console-AppSync/", - "x-forwarded-proto": "https", - "host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com", - "accept-language": "en-US,en;q=0.5", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0", - "cloudfront-is-desktop-viewer": "true", - "cloudfront-is-mobile-viewer": "false", - "accept": "*/*", - "x-forwarded-port": "443", - "cloudfront-is-smarttv-viewer": "false" - } - }, - "prev": null, - "info": { - "parentTypeName": "Query", - "selectionSetList": [ - "id", - "title" - ], - "selectionSetGraphQL": "{\n id\n title\n}", - "fieldName": "listTodos", - "variables": {} - }, - "stash": {} - } - ``` +| Scalar type | Scalar function | Sample value | +| ---------------- | ---------------------------------- | -------------------------------------- | +| **ID** | `scalar_types_utils.make_id` | `e916c84d-48b6-484c-bef3-cee3e4d86ebf` | +| **AWSDate** | `scalar_types_utils.aws_date` | `2022-07-08Z` | +| **AWSTime** | `scalar_types_utils.aws_time` | `15:11:00.189Z` | +| **AWSDateTime** | `scalar_types_utils.aws_datetime` | `2022-07-08T15:11:00.189Z` | +| **AWSTimestamp** | `scalar_types_utils.aws_timestamp` | `1657293060` | ## Advanced ### Nested mappings -You can nest `app.resolver()` decorator multiple times when resolving fields with the same return. - -=== "nested_mappings.py" - - ```python hl_lines="4 8 10-12 18" - from aws_lambda_powertools import Logger, Tracer +???+ note - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver + The following examples use a more advanced schema. These schemas differ from [initial sample infrastructure we used earlier](#required-resources). - tracer = Tracer(service="sample_resolver") - logger = Logger(service="sample_resolver") - app = AppSyncResolver() +You can nest `app.resolver()` decorator multiple times when resolving fields with the same return value. - @app.resolver(field_name="listLocations") - @app.resolver(field_name="locations") - def get_locations(name: str, description: str = ""): - return name + description +=== "nested_mappings.py" - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="4 10 20-21 23 30" + --8<-- "examples/event_handler_graphql/src/nested_mappings.py" ``` -=== "schema.graphql" +=== "nested_mappings_schema.graphql" ```typescript hl_lines="6 20" - schema { - query: Query - } - - type Query { - listLocations: [Location] - } - - type Location { - id: ID! - name: String! - description: String - address: String - } - - type Merchant { - id: String! - name: String! - description: String - locations: [Location] - } + --8<-- "examples/event_handler_graphql/src/nested_mappings_schema.graphql" ``` ### Async functions For Lambda Python3.8+ runtime, this utility supports async functions when you use in conjunction with `asyncio.run`. -```python hl_lines="5 9 11-13 21" title="Resolving GraphQL resolvers async" -import asyncio -from aws_lambda_powertools import Logger, Tracer - -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import AppSyncResolver - -tracer = Tracer(service="sample_resolver") -logger = Logger(service="sample_resolver") -app = AppSyncResolver() - -@app.resolver(type_name="Query", field_name="listTodos") -async def list_todos(): - todos = await some_async_io_call() - return todos - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) -@tracer.capture_lambda_handler -def lambda_handler(event, context): - result = app.resolve(event, context) - - return asyncio.run(result) +```python hl_lines="7 14 24-25 34 36" title="Resolving GraphQL resolvers async" +--8<-- "examples/event_handler_graphql/src/async_resolvers.py" ``` ### Amplify GraphQL Transformer @@ -427,29 +137,7 @@ Assuming you have [Amplify CLI installed](https://docs.amplify.aws/cli/start/ins ```typescript hl_lines="7 15 20 22" title="Example GraphQL Schema" -@model -type Merchant { - id: String! - name: String! - description: String - # Resolves to `common_field` - commonField: String @function(name: "merchantInfo-${env}") -} - -type Location { - id: ID! - name: String! - address: String - # Resolves to `common_field` - commonField: String @function(name: "merchantInfo-${env}") -} - -type Query { - # List of locations resolves to `list_locations` - listLocations(page: Int, size: Int): [Location] @function(name: "merchantInfo-${env}") - # List of locations resolves to `list_locations` - findMerchant(search: str): [Merchant] @function(name: "searchMerchant-${env}") -} +--8<-- "examples/event_handler_graphql/src/amplify_graphql_transformer_schema.graphql" ``` [Create two new basic Python functions](https://docs.amplify.aws/cli/function#set-up-a-function){target="_blank"} via `amplify add function`. @@ -457,257 +145,60 @@ type Query { ???+ note 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`. +Within your function's folder, add Powertools as a dependency with `pipenv install aws-lambda-powertools`. Use the following code for `merchantInfo` and `searchMerchant` functions respectively. -=== "merchantInfo/src/app.py" - - ```python hl_lines="4-5 9 11-12 15-16 23" - from aws_lambda_powertools import Logger, Tracer - - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils - - tracer = Tracer(service="sample_graphql_transformer_resolver") - logger = Logger(service="sample_graphql_transformer_resolver") - app = AppSyncResolver() +=== "graphql_transformer_merchant_info.py" - @app.resolver(type_name="Query", field_name="listLocations") - def list_locations(page: int = 0, size: int = 10): - return [{"id": 100, "name": "Smooth Grooves"}] + ```python hl_lines="4 6 22-23 27-28 36" + --8<-- "examples/event_handler_graphql/src/graphql_transformer_merchant_info.py" + ``` - @app.resolver(field_name="commonField") - def common_field(): - # Would match all fieldNames matching 'commonField' - return scalar_types_utils.make_id() +=== "graphql_transformer_search_merchant.py" - @tracer.capture_lambda_handler - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - def lambda_handler(event, context): - app.resolve(event, context) - ``` -=== "searchMerchant/src/app.py" - - ```python hl_lines="1 4 6-7" - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils - - app = AppSyncResolver() - - @app.resolver(type_name="Query", field_name="findMerchant") - def find_merchant(search: str): - return [ - { - "id": scalar_types_utils.make_id(), - "name": "Brewer Brewing", - "description": "Mike Brewer's IPA brewing place" - }, - { - "id": scalar_types_utils.make_id(), - "name": "Serverlessa's Bakery", - "description": "Lessa's sourdough place" - }, - ] + ```python hl_lines="4 6 21-22 36 42" + --8<-- "examples/event_handler_graphql/src/graphql_transformer_search_merchant.py" ``` -**Example AppSync GraphQL Transformer Function resolver events** - -=== "Query.listLocations event" +=== "graphql_transformer_list_locations.json" ```json hl_lines="2-7" - { - "typeName": "Query", - "fieldName": "listLocations", - "arguments": { - "page": 2, - "size": 1 - }, - "identity": { - "claims": { - "iat": 1615366261 - ... - }, - "username": "mike", - ... - }, - "request": { - "headers": { - "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", - "x-forwarded-for": "127.0.0.1" - ... - } - }, - ... - } + --8<-- "examples/event_handler_graphql/src/graphql_transformer_list_locations.json" ``` -=== "*.commonField event" +=== "graphql_transformer_common_field.json" ```json hl_lines="2 3" - { - "typeName": "Merchant", - "fieldName": "commonField", - "arguments": { - }, - "identity": { - "claims": { - "iat": 1615366261 - ... - }, - "username": "mike", - ... - }, - "request": { - "headers": { - "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", - "x-forwarded-for": "127.0.0.1" - ... - } - }, - ... - } + --8<-- "examples/event_handler_graphql/src/graphql_transformer_common_field.json" ``` -=== "Query.findMerchant event" +=== "graphql_transformer_find_merchant.json" ```json hl_lines="2-6" - { - "typeName": "Query", - "fieldName": "findMerchant", - "arguments": { - "search": "Brewers Coffee" - }, - "identity": { - "claims": { - "iat": 1615366261 - ... - }, - "username": "mike", - ... - }, - "request": { - "headers": { - "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", - "x-forwarded-for": "127.0.0.1" - ... - } - }, - ... - } + --8<-- "examples/event_handler_graphql/src/graphql_transformer_find_merchant.json" ``` ### Custom data models -You can subclass `AppSyncResolverEvent` to bring your own set of methods to handle incoming events, by using `data_model` param in the `resolve` method. - -=== "custom_model.py" - - ```python hl_lines="12-15 20 27" - from aws_lambda_powertools import Logger, Tracer - - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent - - tracer = Tracer(service="sample_resolver") - logger = Logger(service="sample_resolver") - app = AppSyncResolver() +You can subclass [AppSyncResolverEvent](../../utilities/data_classes.md#appsync-resolver){target="_blank"} to bring your own set of methods to handle incoming events, by using `data_model` param in the `resolve` method. +=== "custom_models.py.py" - class MyCustomModel(AppSyncResolverEvent): - @property - def country_viewer(self) -> str: - return self.request_headers.get("cloudfront-viewer-country") - - @app.resolver(field_name="listLocations") - @app.resolver(field_name="locations") - def get_locations(name: str, description: str = ""): - if app.current_event.country_viewer == "US": - ... - return name + description - - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context, data_model=MyCustomModel) + ```python hl_lines="4 7 23-25 28-29 36 43" + --8<-- "examples/event_handler_graphql/src/custom_models.py" ``` -=== "schema.graphql" +=== "nested_mappings_schema.graphql" ```typescript hl_lines="6 20" - schema { - query: Query - } - - type Query { - listLocations: [Location] - } - - type Location { - id: ID! - name: String! - description: String - address: String - } - - type Merchant { - id: String! - name: String! - description: String - locations: [Location] - } + --8<-- "examples/event_handler_graphql/src/nested_mappings_schema.graphql" ``` -=== "listLocations_event.json" - - ```json - { - "arguments": {}, - "identity": null, - "source": null, - "request": { - "headers": { - "x-forwarded-for": "1.2.3.4, 5.6.7.8", - "accept-encoding": "gzip, deflate, br", - "cloudfront-viewer-country": "NL", - "cloudfront-is-tablet-viewer": "false", - "referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1", - "via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)", - "cloudfront-forwarded-proto": "https", - "origin": "https://eu-west-1.console.aws.amazon.com", - "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq", - "content-type": "application/json", - "x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494", - "x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==", - "content-length": "114", - "x-amz-user-agent": "AWS-Console-AppSync/", - "x-forwarded-proto": "https", - "host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com", - "accept-language": "en-US,en;q=0.5", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0", - "cloudfront-is-desktop-viewer": "true", - "cloudfront-is-mobile-viewer": "false", - "accept": "*/*", - "x-forwarded-port": "443", - "cloudfront-is-smarttv-viewer": "false" - } - }, - "prev": null, - "info": { - "parentTypeName": "Query", - "selectionSetList": [ - "id", - "name", - "description" - ], - "selectionSetGraphQL": "{\n id\n name\n description\n}", - "fieldName": "listLocations", - "variables": {} - }, - "stash": {} - } +=== "graphql_transformer_list_locations.json" + + ```json hl_lines="18-19" + --8<-- "examples/event_handler_graphql/src/graphql_transformer_list_locations.json" ``` ### Split operations with Router @@ -715,59 +206,24 @@ You can subclass `AppSyncResolverEvent` to bring your own set of methods to hand ???+ tip Read the **[considerations section for trade-offs between monolithic and micro functions](./api_gateway.md#considerations){target="_blank"}**, as it's also applicable here. -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. +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 when the `Router` feature comes handy. -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. +Let's assume you have `split_operation.py` as your Lambda function entrypoint and routes in `split_operation_module.py`. This is how you'd use the `Router` feature. -=== "resolvers/location.py" +=== "split_operation_module.py" We import **Router** instead of **AppSyncResolver**; syntax wise is exactly the same. - ```python hl_lines="4 7 10 15" - from typing import Any, Dict, List - - from aws_lambda_powertools import Logger - from aws_lambda_powertools.event_handler.appsync import Router - - logger = Logger(child=True) - router = Router() - - - @router.resolver(type_name="Query", field_name="listLocations") - def list_locations(merchant_id: str) -> List[Dict[str, Any]]: - return [{"name": "Location name", "merchant_id": merchant_id}] - + ```python hl_lines="4 8 18-19" + --8<-- "examples/event_handler_graphql/src/split_operation_module.py" + ``` - @router.resolver(type_name="Location", field_name="status") - def resolve_status(merchant_id: str) -> str: - logger.debug(f"Resolve status for merchant_id: {merchant_id}") - return "FOO" - ``` +=== "split_operation.py" -=== "app.py" + We use `include_router` method and include all `location` operations registered in the `router` global object. - We use `include_router` method and include all `location` operations registered in the `router` global object. - - ```python hl_lines="8 13" - from typing import Dict - - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.logging.correlation_paths import APPSYNC_RESOLVER - from aws_lambda_powertools.utilities.typing import LambdaContext - - from resolvers import location - - tracer = Tracer() - logger = Logger() - app = AppSyncResolver() - app.include_router(location.router) - - - @tracer.capture_lambda_handler - @logger.inject_lambda_context(correlation_id_path=APPSYNC_RESOLVER) - def lambda_handler(event: Dict, context: LambdaContext): - app.resolve(event, context) + ```python hl_lines="1 11" + --8<-- "examples/event_handler_graphql/src/split_operation.py" ``` ## Testing your code @@ -778,89 +234,43 @@ 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: -=== "test_resolver.py" - - ```python - import json - import pytest - from pathlib import Path +=== "assert_graphql_response.py" - from src.index import app # import the instance of AppSyncResolver from your code - - def test_direct_resolver(): - # Load mock event from a file - json_file_path = Path("appSyncDirectResolver.json") - with open(json_file_path) as json_file: - mock_event = json.load(json_file) - - # Call the implicit handler - result = app(mock_event, {}) - - assert result == "created this value" + ```python hl_lines="6 26 29" + --8<-- "examples/event_handler_graphql/src/assert_graphql_response.py" ``` -=== "src/index.py" - - ```python - - from aws_lambda_powertools.event_handler import AppSyncResolver - - app = AppSyncResolver() - - @app.resolver(field_name="createSomething") - def create_something(): - return "created this value" +=== "assert_graphql_response_module.py" + ```python hl_lines="10" + --8<-- "examples/event_handler_graphql/src/assert_graphql_response_module.py" ``` -=== "appSyncDirectResolver.json" +=== "assert_graphql_response.json" - ```json - --8<-- "tests/events/appSyncDirectResolver.json" + ```json hl_lines="5" + --8<-- "examples/event_handler_graphql/src/assert_graphql_response.json" ``` -And an example for testing asynchronous resolvers. Note that this requires the `pytest-asyncio` package: - -=== "test_async_resolver.py" +And an example for testing asynchronous resolvers. Note that this requires the `pytest-asyncio` package. This tests a specific async GraphQL operation. - ```python - import json - import pytest - from pathlib import Path - - from src.index import app # import the instance of AppSyncResolver from your code - - @pytest.mark.asyncio - async def test_direct_resolver(): - # Load mock event from a file - json_file_path = Path("appSyncDirectResolver.json") - with open(json_file_path) as json_file: - mock_event = json.load(json_file) +???+ note + Alternatively, you can continue call `lambda_handler` function synchronously as it'd run `asyncio.run` to await for the coroutine to complete. - # Call the implicit handler - result = await app(mock_event, {}) +=== "assert_async_graphql_response.py" - assert result == "created this value" + ```python hl_lines="27" + --8<-- "examples/event_handler_graphql/src/assert_async_graphql_response.py" ``` -=== "src/index.py" - - ```python - import asyncio - - from aws_lambda_powertools.event_handler import AppSyncResolver - - app = AppSyncResolver() - - @app.resolver(field_name="createSomething") - async def create_something_async(): - await asyncio.sleep(1) # Do async stuff - return "created this value" +=== "assert_async_graphql_response_module.py" + ```python hl_lines="14" + --8<-- "examples/event_handler_graphql/src/assert_async_graphql_response_module.py" ``` -=== "appSyncDirectResolver.json" +=== "assert_async_graphql_response.json" - ```json - --8<-- "tests/events/appSyncDirectResolver.json" + ```json hl_lines="3 4" + --8<-- "examples/event_handler_graphql/src/assert_async_graphql_response.json" ``` diff --git a/examples/event_handler_graphql/sam/template.yaml b/examples/event_handler_graphql/sam/template.yaml new file mode 100644 index 00000000000..3e2ab60ab10 --- /dev/null +++ b/examples/event_handler_graphql/sam/template.yaml @@ -0,0 +1,124 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Hello world Direct Lambda Resolver + +Globals: + Function: + Timeout: 5 + Runtime: python3.9 + Tracing: Active + Environment: + Variables: + # Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/latest/#environment-variables + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_SERVICE_NAME: example + +Resources: + TodosFunction: + Type: AWS::Serverless::Function + Properties: + Handler: getting_started_graphql_api_resolver.lambda_handler + CodeUri: ../src + Description: Sample Direct Lambda Resolver + + # IAM Permissions and Roles + + AppSyncServiceRole: + 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: + - !GetAtt TodosFunction.Arn + Roles: + - !Ref AppSyncServiceRole + + # GraphQL API + + TodosApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: TodosApi + AuthenticationType: "API_KEY" + XrayEnabled: true + + TodosApiKey: + Type: AWS::AppSync::ApiKey + Properties: + ApiId: !GetAtt TodosApi.ApiId + + TodosApiSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt TodosApi.ApiId + Definition: | + schema { + query:Query + } + + type Query { + getTodo(id: ID!): Todo + listTodos: [Todo] + } + + type Todo { + id: ID! + userId: String + title: String + completed: Boolean + } + + # Lambda Direct Data Source and Resolver + + TodosFunctionDataSource: + Type: "AWS::AppSync::DataSource" + Properties: + ApiId: !GetAtt TodosApi.ApiId + Name: "HelloWorldLambdaDirectResolver" + Type: "AWS_LAMBDA" + ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn + LambdaConfig: + LambdaFunctionArn: !GetAtt TodosFunction.Arn + + ListTodosResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt TodosApi.ApiId + TypeName: "Query" + FieldName: "listTodos" + DataSourceName: !GetAtt TodosFunctionDataSource.Name + + GetTodoResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt TodosApi.ApiId + TypeName: "Query" + FieldName: "getTodo" + DataSourceName: !GetAtt TodosFunctionDataSource.Name + +Outputs: + TodosFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt TodosFunction.Arn + + TodosApi: + Value: !GetAtt TodosApi.Arn diff --git a/examples/event_handler_graphql/src/amplify_graphql_transformer_schema.graphql b/examples/event_handler_graphql/src/amplify_graphql_transformer_schema.graphql new file mode 100644 index 00000000000..0bd6949cb91 --- /dev/null +++ b/examples/event_handler_graphql/src/amplify_graphql_transformer_schema.graphql @@ -0,0 +1,23 @@ +@model +type Merchant { + id: String! + name: String! + description: String + # Resolves to `common_field` + commonField: String @function(name: "merchantInfo-${env}") +} + +type Location { + id: ID! + name: String! + address: String + # Resolves to `common_field` + commonField: String @function(name: "merchantInfo-${env}") +} + +type Query { + # List of locations resolves to `list_locations` + listLocations(page: Int, size: Int): [Location] @function(name: "merchantInfo-${env}") + # List of locations resolves to `list_locations` + findMerchant(search: str): [Merchant] @function(name: "searchMerchant-${env}") +} diff --git a/examples/event_handler_graphql/src/assert_async_graphql_response.json b/examples/event_handler_graphql/src/assert_async_graphql_response.json new file mode 100644 index 00000000000..e22d4e741cd --- /dev/null +++ b/examples/event_handler_graphql/src/assert_async_graphql_response.json @@ -0,0 +1,43 @@ +{ + "typeName": "Query", + "fieldName": "listTodos", + "arguments": {}, + "selectionSetList": [ + "id", + "userId", + "completed" + ], + "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" + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1", + "cloudfront-viewer-country": "NL", + "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq" + } + } +} \ No newline at end of file diff --git a/examples/event_handler_graphql/src/assert_async_graphql_response.py b/examples/event_handler_graphql/src/assert_async_graphql_response.py new file mode 100644 index 00000000000..22eceb1c5d0 --- /dev/null +++ b/examples/event_handler_graphql/src/assert_async_graphql_response.py @@ -0,0 +1,34 @@ +import json +from dataclasses import dataclass +from pathlib import Path + +import pytest +from assert_async_graphql_response_module import Location, app # instance of AppSyncResolver + + +@pytest.fixture +def lambda_context(): + @dataclass + class LambdaContext: + function_name: str = "test" + memory_limit_in_mb: int = 128 + invoked_function_arn: str = "arn:aws:lambda:eu-west-1:123456789012:function:test" + aws_request_id: str = "da658bd3-2d6f-4e7b-8ec2-937234644fdc" + + return LambdaContext() + + +@pytest.mark.asyncio +async def test_async_direct_resolver(lambda_context): + # GIVEN + fake_event = json.loads(Path("assert_async_graphql_response.json").read_text()) + + # WHEN + result: list[Location] = await app(fake_event, lambda_context) + # alternatively, you can also run a sync test against `lambda_handler` + # since `lambda_handler` awaits the coroutine to complete + + # THEN + assert result[0]["userId"] == 1 + assert result[0]["id"] == 1 + assert result[0]["completed"] is False diff --git a/examples/event_handler_graphql/src/assert_async_graphql_response_module.py b/examples/event_handler_graphql/src/assert_async_graphql_response_module.py new file mode 100644 index 00000000000..892da71fb0f --- /dev/null +++ b/examples/event_handler_graphql/src/assert_async_graphql_response_module.py @@ -0,0 +1,37 @@ +import asyncio +from typing import TypedDict + +import aiohttp + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.tracing import aiohttp_trace_config +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Todo(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + userId: str + title: str + completed: bool + + +@app.resolver(type_name="Query", field_name="listTodos") +async def list_todos() -> list[Todo]: + async with aiohttp.ClientSession(trace_configs=[aiohttp_trace_config()]) as session: + async with session.get("https://jsonplaceholder.typicode.com/todos") as resp: + # first two results to demo assertion + return await resp.json()[:2] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + result = app.resolve(event, context) + + return asyncio.run(result) diff --git a/examples/event_handler_graphql/src/assert_graphql_response.json b/examples/event_handler_graphql/src/assert_graphql_response.json new file mode 100644 index 00000000000..7d5fe1be12e --- /dev/null +++ b/examples/event_handler_graphql/src/assert_graphql_response.json @@ -0,0 +1,45 @@ +{ + "typeName": "Query", + "fieldName": "listLocations", + "arguments": { + "name": "Perkins-Reed", + "description": "Nulla sed amet. Earum libero qui sunt perspiciatis. Non aliquid accusamus." + }, + "selectionSetList": [ + "id", + "name" + ], + "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" + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1", + "cloudfront-viewer-country": "NL", + "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq" + } + } +} \ No newline at end of file diff --git a/examples/event_handler_graphql/src/assert_graphql_response.py b/examples/event_handler_graphql/src/assert_graphql_response.py new file mode 100644 index 00000000000..548aece15e0 --- /dev/null +++ b/examples/event_handler_graphql/src/assert_graphql_response.py @@ -0,0 +1,29 @@ +import json +from dataclasses import dataclass +from pathlib import Path + +import pytest +from assert_graphql_response_module import Location, app # instance of AppSyncResolver + + +@pytest.fixture +def lambda_context(): + @dataclass + class LambdaContext: + function_name: str = "test" + memory_limit_in_mb: int = 128 + invoked_function_arn: str = "arn:aws:lambda:eu-west-1:123456789012:function:test" + aws_request_id: str = "da658bd3-2d6f-4e7b-8ec2-937234644fdc" + + return LambdaContext() + + +def test_direct_resolver(lambda_context): + # GIVEN + fake_event = json.loads(Path("assert_graphql_response.json").read_text()) + + # WHEN + result: list[Location] = app(fake_event, lambda_context) + + # THEN + assert result[0]["name"] == "Perkins-Reed" diff --git a/examples/event_handler_graphql/src/assert_graphql_response_module.py b/examples/event_handler_graphql/src/assert_graphql_response_module.py new file mode 100644 index 00000000000..2f9c8ac3c41 --- /dev/null +++ b/examples/event_handler_graphql/src/assert_graphql_response_module.py @@ -0,0 +1,30 @@ +from typing import TypedDict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Location(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + name: str + description: str + address: str + + +@app.resolver(field_name="listLocations") +@app.resolver(field_name="locations") +@tracer.capture_method +def get_locations(name: str, description: str = "") -> list[Location]: # match GraphQL Query arguments + return [{"name": name, "description": description}] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_graphql/src/async_resolvers.py b/examples/event_handler_graphql/src/async_resolvers.py new file mode 100644 index 00000000000..229e015c886 --- /dev/null +++ b/examples/event_handler_graphql/src/async_resolvers.py @@ -0,0 +1,36 @@ +import asyncio +from typing import TypedDict + +import aiohttp + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.tracing import aiohttp_trace_config +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Todo(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + userId: str + title: str + completed: bool + + +@app.resolver(type_name="Query", field_name="listTodos") +async def list_todos() -> list[Todo]: + async with aiohttp.ClientSession(trace_configs=[aiohttp_trace_config()]) as session: + async with session.get("https://jsonplaceholder.typicode.com/todos") as resp: + return await resp.json() + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + result = app.resolve(event, context) + + return asyncio.run(result) diff --git a/examples/event_handler_graphql/src/custom_models.py b/examples/event_handler_graphql/src/custom_models.py new file mode 100644 index 00000000000..92763ca3401 --- /dev/null +++ b/examples/event_handler_graphql/src/custom_models.py @@ -0,0 +1,43 @@ +from typing import TypedDict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Location(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + name: str + description: str + address: str + commonField: str + + +class MyCustomModel(AppSyncResolverEvent): + @property + def country_viewer(self) -> str: + return self.get_header_value(name="cloudfront-viewer-country", default_value="", case_sensitive=False) + + @property + def api_key(self) -> str: + return self.get_header_value(name="x-api-key", default_value="", case_sensitive=False) + + +@app.resolver(type_name="Query", field_name="listLocations") +def list_locations(page: int = 0, size: int = 10) -> list[Location]: + # additional properties/methods will now be available under current_event + logger.debug(f"Request country origin: {app.current_event.country_viewer}") + return [{"id": scalar_types_utils.make_id(), "name": "Perry, James and Carroll"}] + + +@tracer.capture_lambda_handler +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +def lambda_handler(event: dict, context: LambdaContext) -> dict: + app.resolve(event, context, data_model=MyCustomModel) diff --git a/examples/event_handler_graphql/src/getting_started_get_todo.json b/examples/event_handler_graphql/src/getting_started_get_todo.json new file mode 100644 index 00000000000..6cbf15ba36c --- /dev/null +++ b/examples/event_handler_graphql/src/getting_started_get_todo.json @@ -0,0 +1,46 @@ +{ + "arguments": { + "id": "7e362732-c8cd-4405-b090-144ac9b38960" + }, + "identity": null, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.2.3.4, 5.6.7.8", + "accept-encoding": "gzip, deflate, br", + "cloudfront-viewer-country": "NL", + "cloudfront-is-tablet-viewer": "false", + "referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1", + "via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://eu-west-1.console.aws.amazon.com", + "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq", + "content-type": "application/json", + "x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494", + "x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==", + "content-length": "114", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-forwarded-proto": "https", + "host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com", + "accept-language": "en-US,en;q=0.5", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "accept": "*/*", + "x-forwarded-port": "443", + "cloudfront-is-smarttv-viewer": "false" + } + }, + "prev": null, + "info": { + "parentTypeName": "Query", + "selectionSetList": [ + "title", + "id" + ], + "selectionSetGraphQL": "{\n title\n id\n}", + "fieldName": "getTodo", + "variables": {} + }, + "stash": {} +} \ No newline at end of file diff --git a/examples/event_handler_graphql/src/getting_started_graphql_api_resolver.py b/examples/event_handler_graphql/src/getting_started_graphql_api_resolver.py new file mode 100644 index 00000000000..4e42bd42f58 --- /dev/null +++ b/examples/event_handler_graphql/src/getting_started_graphql_api_resolver.py @@ -0,0 +1,48 @@ +from typing import TypedDict + +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Todo(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + userId: str + title: str + completed: bool + + +@app.resolver(type_name="Query", field_name="getTodo") +@tracer.capture_method +def get_todo( + id: str = "", # noqa AA03 VNE003 shadows built-in id to match query argument, e.g., getTodo(id: "some_id") +) -> Todo: + logger.info(f"Fetching Todo {id}") + todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{id}") + todos.raise_for_status() + + return todos.json() + + +@app.resolver(type_name="Query", field_name="listTodos") +@tracer.capture_method +def list_todos() -> list[Todo]: + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return todos.json()[:10] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_graphql/src/getting_started_list_todos.json b/examples/event_handler_graphql/src/getting_started_list_todos.json new file mode 100644 index 00000000000..5be5094cf94 --- /dev/null +++ b/examples/event_handler_graphql/src/getting_started_list_todos.json @@ -0,0 +1,44 @@ +{ + "arguments": {}, + "identity": null, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.2.3.4, 5.6.7.8", + "accept-encoding": "gzip, deflate, br", + "cloudfront-viewer-country": "NL", + "cloudfront-is-tablet-viewer": "false", + "referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1", + "via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://eu-west-1.console.aws.amazon.com", + "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq", + "content-type": "application/json", + "x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494", + "x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==", + "content-length": "114", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-forwarded-proto": "https", + "host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com", + "accept-language": "en-US,en;q=0.5", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "accept": "*/*", + "x-forwarded-port": "443", + "cloudfront-is-smarttv-viewer": "false" + } + }, + "prev": null, + "info": { + "parentTypeName": "Query", + "selectionSetList": [ + "id", + "title" + ], + "selectionSetGraphQL": "{\n id\n title\n}", + "fieldName": "listTodos", + "variables": {} + }, + "stash": {} +} \ No newline at end of file diff --git a/docs/shared/getting_started_schema.graphql b/examples/event_handler_graphql/src/getting_started_schema.graphql similarity index 76% rename from docs/shared/getting_started_schema.graphql rename to examples/event_handler_graphql/src/getting_started_schema.graphql index c738156bd73..b8ef8f995d0 100644 --- a/docs/shared/getting_started_schema.graphql +++ b/examples/event_handler_graphql/src/getting_started_schema.graphql @@ -9,7 +9,7 @@ type Query { type Todo { id: ID! + userId: String title: String - description: String - done: Boolean + completed: Boolean } diff --git a/examples/event_handler_graphql/src/graphql_transformer_common_field.json b/examples/event_handler_graphql/src/graphql_transformer_common_field.json new file mode 100644 index 00000000000..6b8b47b8172 --- /dev/null +++ b/examples/event_handler_graphql/src/graphql_transformer_common_field.json @@ -0,0 +1,17 @@ +{ + "typeName": "Merchant", + "fieldName": "commonField", + "arguments": {}, + "identity": { + "claims": { + "iat": 1615366261 + }, + "username": "marieellis" + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + } + }, +} \ No newline at end of file diff --git a/examples/event_handler_graphql/src/graphql_transformer_find_merchant.json b/examples/event_handler_graphql/src/graphql_transformer_find_merchant.json new file mode 100644 index 00000000000..8186ebc110e --- /dev/null +++ b/examples/event_handler_graphql/src/graphql_transformer_find_merchant.json @@ -0,0 +1,19 @@ +{ + "typeName": "Query", + "fieldName": "findMerchant", + "arguments": { + "search": "Parry-Wood" + }, + "identity": { + "claims": { + "iat": 1615366261 + }, + "username": "wwilliams" + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + } + }, +} \ No newline at end of file diff --git a/examples/event_handler_graphql/src/graphql_transformer_list_locations.json b/examples/event_handler_graphql/src/graphql_transformer_list_locations.json new file mode 100644 index 00000000000..b8f24aa70b6 --- /dev/null +++ b/examples/event_handler_graphql/src/graphql_transformer_list_locations.json @@ -0,0 +1,22 @@ +{ + "typeName": "Query", + "fieldName": "listLocations", + "arguments": { + "page": 2, + "size": 1 + }, + "identity": { + "claims": { + "iat": 1615366261 + }, + "username": "treid" + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1", + "cloudfront-viewer-country": "NL", + "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq" + } + } +} \ No newline at end of file diff --git a/examples/event_handler_graphql/src/graphql_transformer_merchant_info.py b/examples/event_handler_graphql/src/graphql_transformer_merchant_info.py new file mode 100644 index 00000000000..272f119f3b8 --- /dev/null +++ b/examples/event_handler_graphql/src/graphql_transformer_merchant_info.py @@ -0,0 +1,36 @@ +from typing import TypedDict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Location(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + name: str + description: str + address: str + commonField: str + + +@app.resolver(type_name="Query", field_name="listLocations") +def list_locations(page: int = 0, size: int = 10) -> list[Location]: + return [{"id": scalar_types_utils.make_id(), "name": "Smooth Grooves"}] + + +@app.resolver(field_name="commonField") +def common_field() -> str: + # Would match all fieldNames matching 'commonField' + return scalar_types_utils.make_id() + + +@tracer.capture_lambda_handler +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +def lambda_handler(event: dict, context: LambdaContext) -> dict: + app.resolve(event, context) diff --git a/examples/event_handler_graphql/src/graphql_transformer_search_merchant.py b/examples/event_handler_graphql/src/graphql_transformer_search_merchant.py new file mode 100644 index 00000000000..e2adb566f93 --- /dev/null +++ b/examples/event_handler_graphql/src/graphql_transformer_search_merchant.py @@ -0,0 +1,42 @@ +from typing import TypedDict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = AppSyncResolver() +tracer = Tracer() +logger = Logger() + + +class Merchant(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + name: str + description: str + commonField: str + + +@app.resolver(type_name="Query", field_name="findMerchant") +def find_merchant(search: str) -> list[Merchant]: + merchants: list[Merchant] = [ + { + "id": scalar_types_utils.make_id(), + "name": "Parry-Wood", + "description": "Possimus doloremque tempora harum deleniti eum.", + }, + { + "id": scalar_types_utils.make_id(), + "name": "Shaw, Owen and Jones", + "description": "Aliquam iste architecto suscipit in.", + }, + ] + + return next((merchant for merchant in merchants if search == merchant["name"]), [{}]) + + +@tracer.capture_lambda_handler +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +def lambda_handler(event: dict, context: LambdaContext) -> dict: + app.resolve(event, context) diff --git a/examples/event_handler_graphql/src/nested_mappings.py b/examples/event_handler_graphql/src/nested_mappings.py new file mode 100644 index 00000000000..2f9c8ac3c41 --- /dev/null +++ b/examples/event_handler_graphql/src/nested_mappings.py @@ -0,0 +1,30 @@ +from typing import TypedDict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() + + +class Location(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + name: str + description: str + address: str + + +@app.resolver(field_name="listLocations") +@app.resolver(field_name="locations") +@tracer.capture_method +def get_locations(name: str, description: str = "") -> list[Location]: # match GraphQL Query arguments + return [{"name": name, "description": description}] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_graphql/src/nested_mappings_schema.graphql b/examples/event_handler_graphql/src/nested_mappings_schema.graphql new file mode 100644 index 00000000000..23a9ae468b1 --- /dev/null +++ b/examples/event_handler_graphql/src/nested_mappings_schema.graphql @@ -0,0 +1,21 @@ +schema { + query: Query +} + +type Query { + listLocations: [Location] +} + +type Location { + id: ID! + name: String! + description: String + address: String +} + +type Merchant { + id: String! + name: String! + description: String + locations: [Location] +} diff --git a/examples/event_handler_graphql/src/scalar_functions.py b/examples/event_handler_graphql/src/scalar_functions.py new file mode 100644 index 00000000000..0d8fa98b7b3 --- /dev/null +++ b/examples/event_handler_graphql/src/scalar_functions.py @@ -0,0 +1,15 @@ +from aws_lambda_powertools.utilities.data_classes.appsync.scalar_types_utils import ( + aws_date, + aws_datetime, + aws_time, + aws_timestamp, + make_id, +) + +# Scalars: https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html + +_: str = make_id() # Scalar: ID! +_: str = aws_date() # Scalar: AWSDate +_: str = aws_time() # Scalar: AWSTime +_: str = aws_datetime() # Scalar: AWSDateTime +_: int = aws_timestamp() # Scalar: AWSTimestamp diff --git a/examples/event_handler_graphql/src/split_operation.py b/examples/event_handler_graphql/src/split_operation.py new file mode 100644 index 00000000000..5704181d78c --- /dev/null +++ b/examples/event_handler_graphql/src/split_operation.py @@ -0,0 +1,17 @@ +import split_operation_module + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() +app.include_router(split_operation_module.router) + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_graphql/src/split_operation_module.py b/examples/event_handler_graphql/src/split_operation_module.py new file mode 100644 index 00000000000..43c413672b6 --- /dev/null +++ b/examples/event_handler_graphql/src/split_operation_module.py @@ -0,0 +1,22 @@ +from typing import TypedDict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler.appsync import Router + +tracer = Tracer() +logger = Logger() +router = Router() + + +class Location(TypedDict, total=False): + id: str # noqa AA03 VNE003, required due to GraphQL Schema + name: str + description: str + address: str + + +@router.resolver(field_name="listLocations") +@router.resolver(field_name="locations") +@tracer.capture_method +def get_locations(name: str, description: str = "") -> list[Location]: # match GraphQL Query arguments + return [{"name": name, "description": description}] diff --git a/examples/event_handler_rest/sam/template.yaml b/examples/event_handler_rest/sam/template.yaml index f9837e729a5..513e6196f13 100644 --- a/examples/event_handler_rest/sam/template.yaml +++ b/examples/event_handler_rest/sam/template.yaml @@ -13,7 +13,7 @@ Globals: - "*~1*" # converts to */* for any binary type Function: Timeout: 5 - Runtime: python3.8 + Runtime: python3.9 Tracing: Active Environment: Variables: @@ -26,8 +26,8 @@ Resources: ApiFunction: Type: AWS::Serverless::Function Properties: - Handler: app.lambda_handler - CodeUri: api_handler/ + Handler: getting_started_rest_api_resolver.lambda_handler + CodeUri: ../src Description: API handler function Events: AnyApiEvent: