diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index 8d6be2f40e0..7ea8da2dc22 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -27,6 +27,9 @@ RequestContextV2AuthorizerJwt, RequestContextV2Http, ) +from .appsync import ( + AppSyncResolverEventModel, +) from .bedrock_agent import ( BedrockAgentEventModel, BedrockAgentModel, @@ -137,6 +140,7 @@ "AlbModel", "AlbRequestContext", "AlbRequestContextData", + "AppSyncResolverEventModel", "DynamoDBStreamModel", "EventBridgeModel", "DynamoDBStreamChangedRecordModel", diff --git a/aws_lambda_powertools/utilities/parser/models/appsync.py b/aws_lambda_powertools/utilities/parser/models/appsync.py new file mode 100644 index 00000000000..fe65d932332 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/models/appsync.py @@ -0,0 +1,70 @@ +from typing import Optional, List, Dict, Union, Any +from pydantic import BaseModel + +class AppSyncIamIdentity(BaseModel): + accountId: str + cognitoIdentityPoolId: Optional[str] + cognitoIdentityId: Optional[str] + sourceIp: List[str] + username: str + userArn: str + cognitoIdentityAuthType: Optional[str] + cognitoIdentityAuthProvider: Optional[str] + + +class AppSyncCognitoIdentity(BaseModel): + sub: str + issuer: str + username: str + claims: Dict[str, Any] + sourceIp: List[str] + defaultAuthStrategy: str + groups: Optional[List[str]] + + +class AppSyncOidcIdentity(BaseModel): + claims: Dict[str, Any] + issuer: str + sub: str + + +class AppSyncLambdaIdentity(BaseModel): + resolverContext: Dict[str, Any] + + +AppSyncIdentity = Union[ + AppSyncIamIdentity, + AppSyncCognitoIdentity, + AppSyncOidcIdentity, + AppSyncLambdaIdentity, +] + + +class AppSyncRequestModel(BaseModel): + domainName: Optional[str] + headers: Dict[str, str] + + +class AppSyncInfoModel(BaseModel): + selectionSetList: List[str] + selectionSetGraphQL: str + parentTypeName: str + fieldName: str + variables: Dict[str, Any] + + +class AppSyncPrevModel(BaseModel): + result: Dict[str, Any] + + +class AppSyncResolverEventModel(BaseModel): + arguments: Dict[str, Any] + identity: Optional[AppSyncIdentity] + source: Optional[Dict[str, Any]] + request: AppSyncRequestModel + info: AppSyncInfoModel + prev: Optional[AppSyncPrevModel] + stash: Dict[str, Any] + + +AppSyncBatchResolverEventModel = List[AppSyncResolverEventModel] \ No newline at end of file diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index b6abbe965e1..4cdf0d452f2 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -111,6 +111,7 @@ The example above uses `SqsModel`. Other built-in models can be found below. | **APIGatewayWebSocketMessageEventModel** | Lambda Event Source payload for Amazon API Gateway WebSocket API message body | | **APIGatewayWebSocketConnectEventModel** | Lambda Event Source payload for Amazon API Gateway WebSocket API $connect message | | **APIGatewayWebSocketDisconnectEventModel** | Lambda Event Source payload for Amazon API Gateway WebSocket API $disconnect message | +| **AppSyncResolverEventModel** | Lambda Event Source payload for AWS AppSync Resolver | | **BedrockAgentEventModel** | Lambda Event Source payload for Bedrock Agents | | **CloudFormationCustomResourceCreateModel** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation | | **CloudFormationCustomResourceUpdateModel** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation | diff --git a/tests/events/appsync_resolver_event.json b/tests/events/appsync_resolver_event.json new file mode 100644 index 00000000000..1b56d4dc93c --- /dev/null +++ b/tests/events/appsync_resolver_event.json @@ -0,0 +1,78 @@ +{ + "typeName": "Merchant", + "fieldName": "locations", + "arguments": { + "page": 2, + "size": 1, + "name": "value" + }, + "identity": { + "claims": { + "sub": "07920713-4526-4642-9c88-2953512de441", + "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "aud": "58rc9bf5kkti90ctmvioppukm9", + "event_id": "7f4c9383-abf6-48b7-b821-91643968b755", + "token_use": "id", + "auth_time": 1615366261, + "name": "Michael Brewer", + "exp": 1615369861, + "iat": 1615366261 + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "sourceIp": ["11.215.2.22"], + "sub": "07920713-4526-4642-9c88-2953512de441", + "username": "mike" + }, + "source": { + "name": "Value", + "nested": { + "name": "value", + "list": [] + } + }, + "request": { + "headers": { + "x-forwarded-for": "11.215.2.22, 64.44.173.11", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 SOMETHING.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://console.aws.amazon.com", + "content-length": "156", + "accept-language": "en-US,en;q=0.9", + "host": "SOMETHING.appsync-api.us-east-1.amazonaws.com", + "x-forwarded-proto": "https", + "sec-gpc": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://console.aws.amazon.com/", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "Fo5VIuvP6V6anIEt62WzFDCK45mzM4yEdpt5BYxOl9OFqafd-WR0cA==", + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "authorization": "AUTH-HEADER", + "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" + }, + "domainName": "SOMETHING.appsync-api.us-east-1.amazonaws.com" + }, + "prev": { + "result": {} + }, + "info": { + "selectionSetList": ["id", "field1", "field2"], + "selectionSetGraphQL": "{\n id\n field1\n field2\n}", + "parentTypeName": "Merchant", + "fieldName": "locations", + "variables": {} + }, + "stash": {} + } \ No newline at end of file diff --git a/tests/unit/parser/_pydantic/test_appsync.py b/tests/unit/parser/_pydantic/test_appsync.py new file mode 100644 index 00000000000..b8a57eaa7c3 --- /dev/null +++ b/tests/unit/parser/_pydantic/test_appsync.py @@ -0,0 +1,27 @@ +import pytest + +from aws_lambda_powertools.utilities.parser import parse, ValidationError +from aws_lambda_powertools.utilities.parser.models import AppSyncResolverEventModel +from tests.functional.utils import load_event + +def test_appsync_event_model_parses_successfully(): + """ + Validate that a valid AppSync resolver event is correctly parsed by the model. + """ + event = load_event("appsync_resolver_event.json") + parsed_event = parse(event=event, model=AppSyncResolverEventModel) + + assert parsed_event.arguments["page"] == 2 + assert parsed_event.identity.username == "mike" + assert parsed_event.request.headers["host"].endswith("appsync-api.us-east-1.amazonaws.com") + assert parsed_event.info.fieldName == "locations" + assert parsed_event.info.parentTypeName == "Merchant" + + +def test_appsync_event_model_invalid_payload_raises(): + """ + Validate that parsing an invalid AppSync resolver event payload raises a ValidationError. + """ + invalid_event = {"invalid": "event"} + with pytest.raises(ValidationError): + parse(event=invalid_event, model=AppSyncResolverEventModel) \ No newline at end of file