From 2e754129146954a61e0b220bac1c2f7a061d4cb3 Mon Sep 17 00:00:00 2001 From: George Bearden Date: Mon, 23 Jan 2023 13:43:02 -0500 Subject: [PATCH 1/9] Add optional request templates for non-default content-types. --- .../aws-apigateway-sqs/lib/index.ts | 50 +++++++++------ .../test/apigateway-sqs.test.ts | 63 +++++++++++++++++++ .../core/lib/apigateway-helper.ts | 10 ++- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts index 6cb1d3c32..27908be14 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts @@ -69,11 +69,19 @@ export interface ApiGatewayToSqsProps { */ readonly allowCreateOperation?: boolean; /** - * API Gateway Request template for Create method, if allowCreateOperation set to true + * API Gateway Request template for Create method for the default `application/json` content-type, + * if the `allowCreateOperation` property is set to true * - * @default - None + * @default - 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")' */ readonly createRequestTemplate?: string; + /** + * Optional Create request templates for content-types other than `application/json`. + * Use the `createRequestTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; /** * Whether to deploy an API Gateway Method for Read operations on the queue (i.e. sqs:ReceiveMessage). * @@ -86,6 +94,13 @@ export interface ApiGatewayToSqsProps { * @default - "Action=ReceiveMessage" */ readonly readRequestTemplate?: string; + /** + * Optional Read request templates for content-types other than `application/json`. + * Use the `readRequestTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; /** * Whether to deploy an API Gateway Method for Delete operations on the queue (i.e. sqs:DeleteMessage). * @@ -98,6 +113,13 @@ export interface ApiGatewayToSqsProps { * @default - "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))" */ readonly deleteRequestTemplate?: string; + /** + * Optional Delete request templates for content-types other than `application/json`. + * Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalDeleteRequestTemplates?: { [contentType: string]: string; }; /** * User provided props to override the default props for the CloudWatchLogs LogGroup. * @@ -179,12 +201,7 @@ export class ApiGatewayToSqs extends Construct { const apiGatewayResource = this.apiGateway.root.addResource('message'); // Create - let createRequestTemplate = "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")"; - - if (props.createRequestTemplate) { - createRequestTemplate = props.createRequestTemplate; - } - + const createRequestTemplate = props.createRequestTemplate ?? 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")'; if (props.allowCreateOperation && props.allowCreateOperation === true) { this.addActionToPolicy("sqs:SendMessage"); defaults.addProxyMethodToApiResource({ @@ -194,17 +211,13 @@ export class ApiGatewayToSqs extends Construct { apiMethod: "POST", apiResource: this.apiGateway.root, requestTemplate: createRequestTemplate, + additionalRequestTemplates: props.additionalCreateRequestTemplates, contentType: "'application/x-www-form-urlencoded'" }); } // Read - let readRequestTemplate = "Action=ReceiveMessage"; - - if (props.readRequestTemplate) { - readRequestTemplate = props.readRequestTemplate; - } - + const readRequestTemplate = props.readRequestTemplate ?? "Action=ReceiveMessage"; if (props.allowReadOperation === undefined || props.allowReadOperation === true) { this.addActionToPolicy("sqs:ReceiveMessage"); defaults.addProxyMethodToApiResource({ @@ -214,17 +227,13 @@ export class ApiGatewayToSqs extends Construct { apiMethod: "GET", apiResource: this.apiGateway.root, requestTemplate: readRequestTemplate, + additionalRequestTemplates: props.additionalReadRequestTemplates, contentType: "'application/x-www-form-urlencoded'" }); } // Delete - let deleteRequestTemplate = "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))"; - - if (props.deleteRequestTemplate) { - deleteRequestTemplate = props.deleteRequestTemplate; - } - + const deleteRequestTemplate = props.deleteRequestTemplate ?? "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))"; if (props.allowDeleteOperation && props.allowDeleteOperation === true) { this.addActionToPolicy("sqs:DeleteMessage"); defaults.addProxyMethodToApiResource({ @@ -234,6 +243,7 @@ export class ApiGatewayToSqs extends Construct { apiMethod: "DELETE", apiResource: apiGatewayResource, requestTemplate: deleteRequestTemplate, + additionalRequestTemplates: props.additionalDeleteRequestTemplates, contentType: "'application/x-www-form-urlencoded'" }); } diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts index 4f5cb5263..f4a1b97d8 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts @@ -261,4 +261,67 @@ test('Queue is encrypted with customer managed KMS Key when enable encryption fl ] } }); +}); + +test('Construct accepts additional read request templates', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + enableEncryptionWithCustomerManagedKey: true, + additionalReadRequestTemplates: { + 'text/plain': 'Hello' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + RequestTemplates: { + 'application/json': 'Action=ReceiveMessage', + 'text/plain': 'Hello' + } + } + }); +}); + +test('Construct accepts additional create request templates', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + enableEncryptionWithCustomerManagedKey: true, + allowCreateOperation: true, + additionalCreateRequestTemplates: { + 'text/plain': 'Hello' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + RequestTemplates: { + 'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")', + 'text/plain': 'Hello' + } + } + }); +}); + +test('Construct can override default create request template type', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + enableEncryptionWithCustomerManagedKey: true, + allowCreateOperation: true, + createRequestTemplate: 'Hello', + additionalCreateRequestTemplates: { + 'text/plain': 'Goodbye' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + RequestTemplates: { + 'application/json': 'Hello', + 'text/plain': 'Goodbye' + } + } + }); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts index 35d5d00d4..74c77c39f 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts @@ -240,6 +240,7 @@ export interface AddProxyMethodToApiResourceInputParams { readonly apiMethod: string, readonly apiGatewayRole: IRole, readonly requestTemplate: string, + readonly additionalRequestTemplates?: { [contentType: string]: string; }, readonly contentType?: string, readonly requestValidator?: api.IRequestValidator, readonly requestModel?: { [contentType: string]: api.IModel; }, @@ -248,6 +249,11 @@ export interface AddProxyMethodToApiResourceInputParams { } export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceInputParams): api.Method { + const requestTemplates = { + "application/json": params.requestTemplate, + ...params.additionalRequestTemplates + }; + let baseProps: api.AwsIntegrationProps = { service: params.service, integrationHttpMethod: "POST", @@ -257,9 +263,7 @@ export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceI requestParameters: { "integration.request.header.Content-Type": params.contentType ? params.contentType : "'application/json'" }, - requestTemplates: { - "application/json": params.requestTemplate - }, + requestTemplates, integrationResponses: [ { statusCode: "200" From 1491eab664f33a318a5dd5276d63bae1b186002e Mon Sep 17 00:00:00 2001 From: George Bearden Date: Tue, 31 Jan 2023 15:30:46 -0500 Subject: [PATCH 2/9] Add optional request templates properties to apigateway constructs. --- .../aws-apigateway-dynamodb/lib/index.ts | 123 ++-- .../test/apigateway-dynamodb.test.ts | 79 +++ ...additional-request-templates.expected.json | 408 +++++++++++ .../integ.additional-request-templates.ts | 55 ++ .../lib/index.ts | 37 +- .../test/apigateway-kinesis.test.ts | 36 + ...additional-request-templates.expected.json | 632 +++++++++++++++++ .../integ.additional-request-templates.ts | 31 + .../lib/index.ts | 11 +- .../test/apigateway-sagemakerendpoint.test.ts | 22 + ...additional-request-templates.expected.json | 396 +++++++++++ .../integ.additional-request-templates.ts | 49 ++ .../aws-apigateway-sqs/lib/index.ts | 22 +- .../test/apigateway-sqs.test.ts | 23 +- ...additional-request-templates.expected.json | 646 ++++++++++++++++++ .../integ.additional-request-templates.ts | 32 + 16 files changed, 2546 insertions(+), 56 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts index 3266455dc..eca732cd6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts @@ -29,77 +29,122 @@ export interface ApiGatewayToDynamoDBProps { * * @default - Default props are used */ - readonly dynamoTableProps?: dynamodb.TableProps, + readonly dynamoTableProps?: dynamodb.TableProps; /** * Existing instance of DynamoDB table object, providing both this and `dynamoTableProps` will cause an error. * * @default - None */ - readonly existingTableObj?: dynamodb.Table, + readonly existingTableObj?: dynamodb.Table; /** * Optional user-provided props to override the default props for the API Gateway. * * @default - Default properties are used. */ - readonly apiGatewayProps?: api.RestApiProps, + readonly apiGatewayProps?: api.RestApiProps; /** - * Whether to deploy API Gateway Method for Create operation on DynamoDB table. + * Whether to deploy an API Gateway Method for POST HTTP operations on the DynamoDB table (i.e. dynamodb:PutItem). * * @default - false */ - readonly allowCreateOperation?: boolean, + readonly allowCreateOperation?: boolean; /** - * API Gateway Request template for Create method, required if allowCreateOperation set to true + * API Gateway Request Template for the create method for the default `application/json` content-type. + * This property is required if the `allowCreateOperation` property is set to true. * * @default - None */ - readonly createRequestTemplate?: string, + readonly createRequestTemplate?: string; /** - * Whether to deploy API Gateway Method for Read operation on DynamoDB table. + * Optional Create Request Templates for content-types other than `application/json`. + * Use the `createRequestTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; + /** + * Whether to deploy an API Gateway Method for GET HTTP operations on DynamoDB table (i.e. dynamodb:Query). * * @default - true */ - readonly allowReadOperation?: boolean, + readonly allowReadOperation?: boolean; /** - * Optional API Gateway Request template for Read method, it will use the default template if - * allowReadOperation is true and readRequestTemplate is not provided. + * API Gateway Request Template for the read method for the default `application/json` content-type, + * if the `allowReadOperation` property is set to true. + * * The default template only supports a partition key and not partition + sort keys. * + * @default - `{ \ + * "TableName": "DYNAMODB_TABLE_NAME", \ + * "KeyConditionExpression": "PARTITION_KEY_NAME = :v1", \ + * "ExpressionAttributeValues": { \ + * ":v1": { \ + * "S": "$input.params('PARTITION_KEY_NAME')" \ + * } \ + * } \ + * }` + */ + readonly readRequestTemplate?: string; + /** + * Optional Read Request Templates for content-types other than `application/json`. + * Use the `readRequestTemplate` property to set the request template for the `application/json` content-type. + * * @default - None */ - readonly readRequestTemplate?: string, + readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; /** - * Whether to deploy API Gateway Method for Update operation on DynamoDB table. + * Whether to deploy API Gateway Method for PUT HTTP operations on DynamoDB table (i.e. dynamodb:UpdateItem). * * @default - false */ - readonly allowUpdateOperation?: boolean, + readonly allowUpdateOperation?: boolean; + /** + * API Gateway Request Template for the update method, required if the `allowUpdateOperation` property is set to true. + * + * @default - None + */ + readonly updateRequestTemplate?: string; /** - * API Gateway Request template for Update method, required if allowUpdateOperation set to true + * Optional Update Request Templates for content-types other than `application/json`. + * Use the `updateRequestTemplate` property to set the request template for the `application/json` content-type. * * @default - None */ - readonly updateRequestTemplate?: string, + readonly additionalUpdateRequestTemplates?: { [contentType: string]: string; }; /** - * Whether to deploy API Gateway Method for Delete operation on DynamoDB table. + * Whether to deploy API Gateway Method for DELETE HTTP operations on DynamoDB table (i.e. dynamodb:DeleteItem). * * @default - false */ - readonly allowDeleteOperation?: boolean, + readonly allowDeleteOperation?: boolean; /** - * Optional API Gateway Request template for Delete method, it will use the default template if - * allowDeleteOperation is true and deleteRequestTemplate is not provided. - * The default template only supports a partition key and not partition + sort keys. + * API Gateway Request Template for the delete method for the default `application/json` content-type, + * if the `allowDeleteOperation` property is set to true. + * + * @default - `{ \ + * "TableName": "DYNAMODB_TABLE_NAME", \ + * "Key": { \ + * "${partitionKeyName}": { \ + * "S": "$input.params('PARTITION_KEY_NAME')" \ + * } \ + * }, \ + * "ReturnValues": "ALL_OLD" \ + * }` + */ + readonly deleteRequestTemplate?: string; + /** + * Optional Delete request templates for content-types other than `application/json`. + * Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type. * * @default - None */ - readonly deleteRequestTemplate?: string, + readonly additionalDeleteRequestTemplates?: { [contentType: string]: string; }; /** * User provided props to override the default props for the CloudWatchLogs LogGroup. * * @default - Default props are used */ - readonly logGroupProps?: logs.LogGroupProps + readonly logGroupProps?: logs.LogGroupProps; } /** @@ -162,18 +207,14 @@ export class ApiGatewayToDynamoDB extends Construct { apiGatewayRole: this.apiGatewayRole, apiMethod: "POST", apiResource: this.apiGateway.root, - requestTemplate: createRequestTemplate + requestTemplate: createRequestTemplate, + additionalRequestTemplates: props.additionalCreateRequestTemplates }); } // Read if (props.allowReadOperation === undefined || props.allowReadOperation === true) { - let readRequestTemplate; - - if (props.readRequestTemplate) { - readRequestTemplate = props.readRequestTemplate; - } else { - readRequestTemplate = - `{ \ + const readRequestTemplate = props.readRequestTemplate ?? + `{ \ "TableName": "${this.dynamoTable.tableName}", \ "KeyConditionExpression": "${partitionKeyName} = :v1", \ "ExpressionAttributeValues": { \ @@ -182,7 +223,6 @@ export class ApiGatewayToDynamoDB extends Construct { } \ } \ }`; - } this.addActionToPolicy("dynamodb:Query"); defaults.addProxyMethodToApiResource({ @@ -191,7 +231,8 @@ export class ApiGatewayToDynamoDB extends Construct { apiGatewayRole: this.apiGatewayRole, apiMethod: "GET", apiResource: apiGatewayResource, - requestTemplate: readRequestTemplate + requestTemplate: readRequestTemplate, + additionalRequestTemplates: props.additionalReadRequestTemplates }); } // Update @@ -204,18 +245,14 @@ export class ApiGatewayToDynamoDB extends Construct { apiGatewayRole: this.apiGatewayRole, apiMethod: "PUT", apiResource: apiGatewayResource, - requestTemplate: updateRequestTemplate + requestTemplate: updateRequestTemplate, + additionalRequestTemplates: props.additionalUpdateRequestTemplates }); } // Delete if (props.allowDeleteOperation && props.allowDeleteOperation === true) { - let deleteRequestTemplate; - - if (props.deleteRequestTemplate) { - deleteRequestTemplate = props.deleteRequestTemplate; - } else { - deleteRequestTemplate = - `{ \ + const deleteRequestTemplate = props.deleteRequestTemplate ?? + `{ \ "TableName": "${this.dynamoTable.tableName}", \ "Key": { \ "${partitionKeyName}": { \ @@ -224,7 +261,6 @@ export class ApiGatewayToDynamoDB extends Construct { }, \ "ReturnValues": "ALL_OLD" \ }`; - } this.addActionToPolicy("dynamodb:DeleteItem"); defaults.addProxyMethodToApiResource({ @@ -233,7 +269,8 @@ export class ApiGatewayToDynamoDB extends Construct { apiGatewayRole: this.apiGatewayRole, apiMethod: "DELETE", apiResource: apiGatewayResource, - requestTemplate: deleteRequestTemplate + requestTemplate: deleteRequestTemplate, + additionalRequestTemplates: props.additionalDeleteRequestTemplates }); } } diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts index e45b61c46..8d645d1a1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts @@ -250,4 +250,83 @@ test("check setting allowReadOperation=false for dynamodb", () => { // Expect only one APIG Method (DELETE) for allowDeleteOperation expect(stack2).toCountResources("AWS::ApiGateway::Method", 1); +}); + +test('Construct accepts additional create request templates', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowCreateOperation: true, + createRequestTemplate: 'create-me', + additionalCreateRequestTemplates: { + 'text/plain': 'Hello' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + RequestTemplates: { + 'application/json': 'create-me', + 'text/plain': 'Hello' + } + } + }); +}); + +test('Construct accepts additional read request templates', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + additionalReadRequestTemplates: { + 'text/plain': 'Hello' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + RequestTemplates: { + 'text/plain': 'Hello' + } + } + }); +}); + +test('Construct accepts additional update request templates', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowUpdateOperation: true, + updateRequestTemplate: 'default-update-template', + additionalUpdateRequestTemplates: { + 'text/plain': 'additional-update-template' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'PUT', + Integration: { + RequestTemplates: { + 'application/json': 'default-update-template', + 'text/plain': 'additional-update-template' + } + } + }); +}); + +test('Construct accepts additional delete request templates', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowDeleteOperation: true, + additionalDeleteRequestTemplates: { + 'text/plain': 'DeleteMe' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + RequestTemplates: { + 'text/plain': 'DeleteMe' + } + } + }); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json new file mode 100644 index 000000000..cf90c8719 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json @@ -0,0 +1,408 @@ +{ + "Description": "Integration Test for aws-apigateway-dynamodb", + "Resources": { + "existingtableE51CCC93": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + }, + "TableName": "test-table" + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaydynamodbadditionalrequesttemplatesApiAccessLogGroupAF75D750": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47748f8e2ffb1e74060b1bf8f54a7cd2009": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidGET05129D15", + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidA77CCE90" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeploymentStageprod33ED5D23": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesApiAccessLogGroupAF75D750", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47748f8e2ffb1e74060b1bf8f54a7cd2009" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidA77CCE90": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C", + "RootResourceId" + ] + }, + "PathPart": "{id}", + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidGET05129D15": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiidA77CCE90" + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleFDAECAC6", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{ \"TableName\": \"", + { + "Ref": "existingtableE51CCC93" + }, + "\", \"KeyConditionExpression\": \"id = :v1\", \"ExpressionAttributeValues\": { \":v1\": { \"S\": \"$input.params('id')\" } } }" + ] + ] + }, + "text/plain": "{ \"TableName\": \"test-table\", \"KeyConditionExpression\": \"id = :v1\", \"ExpressionAttributeValues\": { \":v1\": { \"S\": \"$input.params('id')\" } } }" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/Query" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiUsagePlan905D10C7": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "Stage": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeploymentStageprod33ED5D23" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesLambdaRestApiCloudWatchRole51265771": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesLambdaRestApiAccount8891474D": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesLambdaRestApiCloudWatchRole51265771", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + ] + }, + "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleFDAECAC6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleDefaultPolicy4C47B35E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:Query", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "existingtableE51CCC93", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleDefaultPolicy4C47B35E", + "Roles": [ + { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleFDAECAC6" + } + ] + } + } + }, + "Outputs": { + "testapigatewaydynamodbadditionalrequesttemplatesRestApiEndpoint855E7762": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeploymentStageprod33ED5D23" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts new file mode 100644 index 000000000..0189f23c7 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts @@ -0,0 +1,55 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "aws-cdk-lib"; +import { ApiGatewayToDynamoDB } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-dynamodb'; + +const tableName = 'test-table'; +const partitionKeyName = 'id'; + +const existingTableObj = new dynamodb.Table(stack, 'existing-table', { + tableName, + partitionKey: { + name: partitionKeyName, + type: dynamodb.AttributeType.STRING, + }, + pointInTimeRecovery: true, + encryption: dynamodb.TableEncryption.AWS_MANAGED, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST +}); + +new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-additional-request-templates', { + existingTableObj, + additionalReadRequestTemplates: { + 'text/plain': `{ \ + "TableName": "${tableName}", \ + "KeyConditionExpression": "${partitionKeyName} = :v1", \ + "ExpressionAttributeValues": { \ + ":v1": { \ + "S": "$input.params('${partitionKeyName}')" \ + } \ + } \ + }` + } +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts index 22f8c0de3..20c175a78 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts @@ -39,6 +39,13 @@ export interface ApiGatewayToKinesisStreamsProps { * "PartitionKey": "$input.path('$.partitionKey')" } */ readonly putRecordRequestTemplate?: string; + /** + * Optional PutRecord Request Templates for content-types other than `application/json`. + * Use the `putRecordRequestTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalPutRecordRequestTemplates?: { [contentType: string]: string; }; /** * API Gateway request model for the PutRecord action. * If not provided, a default one will be created. @@ -48,13 +55,20 @@ export interface ApiGatewayToKinesisStreamsProps { */ readonly putRecordRequestModel?: api.ModelOptions; /** - * API Gateway request template for the PutRecords action. + * API Gateway request template for the PutRecords action for the default `application/json` content-type. * If not provided, a default one will be used. * * @default - { "StreamName": "${this.kinesisStream.streamName}", "Records": [ #foreach($elem in $input.path('$.records')) * { "Data": "$util.base64Encode($elem.data)", "PartitionKey": "$elem.partitionKey"}#if($foreach.hasNext),#end #end ] } */ readonly putRecordsRequestTemplate?: string; + /** + * Optional PutRecords Request Templates for content-types other than `application/json`. + * Use the `putRecordsRequestTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalPutRecordsRequestTemplates?: { [contentType: string]: string; }; /** * API Gateway request model for the PutRecords action. * If not provided, a default one will be created. @@ -144,6 +158,7 @@ export class ApiGatewayToKinesisStreams extends Construct { apiMethod: 'POST', apiResource: putRecordResource, requestTemplate: this.getPutRecordTemplate(props.putRecordRequestTemplate), + additionalRequestTemplates: this.getAdditionalPutRecordTemplates(props.additionalPutRecordRequestTemplates), contentType: "'x-amz-json-1.1'", requestValidator, requestModel: { 'application/json': this.getPutRecordModel(props.putRecordRequestModel) } @@ -158,6 +173,7 @@ export class ApiGatewayToKinesisStreams extends Construct { apiMethod: 'POST', apiResource: putRecordsResource, requestTemplate: this.getPutRecordsTemplate(props.putRecordsRequestTemplate), + additionalRequestTemplates: this.getAdditionalPutRecordTemplates(props.additionalPutRecordsRequestTemplates), contentType: "'x-amz-json-1.1'", requestValidator, requestModel: { 'application/json': this.getPutRecordsModel(props.putRecordsRequestModel) } @@ -169,6 +185,25 @@ export class ApiGatewayToKinesisStreams extends Construct { } } + /** + * This method transforms the value of each request template by replacing the stream name placeholder value with the + * actual name of the stream resource + * + * @param templates The additional request templates to transform. + */ + private getAdditionalPutRecordTemplates(templates?: { [contentType: string]: string; }): { [contentType: string]: string; } { + + const transformedTemplates: { [contentType: string]: string; } = {}; + + for (const key in templates) { + if (templates[key] !== undefined) { + transformedTemplates[key] = templates[key].replace("${StreamName}", this.kinesisStream.streamName); + } + } + + return transformedTemplates; + } + private getPutRecordTemplate(putRecordTemplate?: string): string { if (putRecordTemplate !== undefined) { return putRecordTemplate.replace("${StreamName}", this.kinesisStream.streamName); diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts index 23a5966f7..555b38d19 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts @@ -111,3 +111,39 @@ test('Test deployment w/ existing stream', () => { // Since createCloudWatchAlars is set to false, no Alarm should exist expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm'); }); + +test('Construct accepts additional PutRecord request templates', () => { + const stack = new Stack(); + new ApiGatewayToKinesisStreams(stack, 'api-gateway-kinesis-streamsĀ ', { + additionalPutRecordRequestTemplates: { + 'text/plain': 'custom-template' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + RequestTemplates: { + 'text/plain': 'custom-template' + } + } + }); +}); + +test('Construct accepts additional PutRecords request templates', () => { + const stack = new Stack(); + new ApiGatewayToKinesisStreams(stack, 'api-gateway-kinesis-streamsĀ ', { + additionalPutRecordsRequestTemplates: { + 'text/plain': 'custom-template' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + RequestTemplates: { + 'text/plain': 'custom-template' + } + } + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.expected.json new file mode 100644 index 000000000..140df25ef --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.expected.json @@ -0,0 +1,632 @@ +{ + "Description": "Integration Test for aws-apigateway-kinesis", + "Resources": { + "testapigatewaykinesisadditionalrequesttemplatesApiAccessLogGroup9C079B68": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiDeployment5A447E3D3f205cdf3c053c5a1187e7b2a0f3474d": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordPOST307FC87D", + "testapigatewaykinesisadditionalrequesttemplatesRestApirecord01520200", + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordsPOST5F6260A2", + "testapigatewaykinesisadditionalrequesttemplatesRestApirecords37B412D1", + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordModel1A75CC15", + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordsModel49E0CAB9", + "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiDeploymentStageprodD274025B": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesApiAccessLogGroup9C079B68", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiDeployment5A447E3D3f205cdf3c053c5a1187e7b2a0f3474d" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecord01520200": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7", + "RootResourceId" + ] + }, + "PathPart": "record", + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordPOST307FC87D": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirecord01520200" + }, + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'x-amz-json-1.1'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{ \"StreamName\": \"", + { + "Ref": "KinesisStream46752A3E" + }, + "\", \"Data\": \"$util.base64Encode($input.json('$.data'))\", \"PartitionKey\": \"$input.path('$.partitionKey')\" }" + ] + ] + }, + "text/plain": { + "Fn::Join": [ + "", + [ + "{ \"StreamName\": \"", + { + "Ref": "KinesisStream46752A3E" + }, + "\", \"Data\": \"$util.base64Encode($input.json('$.foo'))\", \"PartitionKey\": \"$input.path('$.bar')\" }" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":kinesis:action/PutRecord" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ], + "RequestModels": { + "application/json": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordModel1A75CC15" + } + }, + "RequestValidatorId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecords37B412D1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7", + "RootResourceId" + ] + }, + "PathPart": "records", + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordsPOST5F6260A2": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirecords37B412D1" + }, + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'x-amz-json-1.1'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{ \"StreamName\": \"", + { + "Ref": "KinesisStream46752A3E" + }, + "\", \"Records\": [ #foreach($elem in $input.path('$.records')) { \"Data\": \"$util.base64Encode($elem.data)\", \"PartitionKey\": \"$elem.partitionKey\"}#if($foreach.hasNext),#end #end ] }" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":kinesis:action/PutRecords" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ], + "RequestModels": { + "application/json": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordsModel49E0CAB9" + } + }, + "RequestValidatorId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiUsagePlan68D85E30": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "Stage": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiDeploymentStageprodD274025B" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE": { + "Type": "AWS::ApiGateway::RequestValidator", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "Name": "request-body-validator", + "ValidateRequestBody": true + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordModel1A75CC15": { + "Type": "AWS::ApiGateway::Model", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "ContentType": "application/json", + "Description": "PutRecord proxy single-record payload", + "Name": "PutRecordModel", + "Schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "PutRecord proxy single-record payload", + "type": "object", + "required": [ + "data", + "partitionKey" + ], + "properties": { + "data": { + "type": "string" + }, + "partitionKey": { + "type": "string" + } + } + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordsModel49E0CAB9": { + "Type": "AWS::ApiGateway::Model", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "ContentType": "application/json", + "Description": "PutRecords proxy payload data", + "Name": "PutRecordsModel", + "Schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "PutRecords proxy payload data", + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "object", + "required": [ + "data", + "partitionKey" + ], + "properties": { + "data": { + "type": "string" + }, + "partitionKey": { + "type": "string" + } + } + } + } + } + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesLambdaRestApiCloudWatchRole4FA3BA8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaykinesisadditionalrequesttemplatesLambdaRestApiAccount9A5A772A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesLambdaRestApiCloudWatchRole4FA3BA8A", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + ] + }, + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleDefaultPolicy5D60DBDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:ListShards", + "kinesis:PutRecord", + "kinesis:PutRecords" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KinesisStream46752A3E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleDefaultPolicy5D60DBDF", + "Roles": [ + { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D" + } + ] + } + }, + "testapigatewaykinesisadditionalrequesttemplatesKinesisStreamGetRecordsIteratorAgeAlarm05247CB0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Consumer Record Processing Falling Behind, there is risk for data loss due to record expiration.", + "MetricName": "GetRecords.IteratorAgeMilliseconds", + "Namespace": "AWS/Kinesis", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 43200000 + } + }, + "testapigatewaykinesisadditionalrequesttemplatesKinesisStreamReadProvisionedThroughputExceededAlarmE49197EC": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Consumer Application is Reading at a Slower Rate Than Expected.", + "MetricName": "ReadProvisionedThroughputExceeded", + "Namespace": "AWS/Kinesis", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } + }, + "KinesisStream46752A3E": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": { + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" + }, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + } + } + } + }, + "Outputs": { + "testapigatewaykinesisadditionalrequesttemplatesRestApiEndpointE192AA9B": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiDeploymentStageprodD274025B" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.ts new file mode 100644 index 000000000..d0fd95dc5 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.additional-request-templates.ts @@ -0,0 +1,31 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from 'aws-cdk-lib'; +import { ApiGatewayToKinesisStreams } from '../lib'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-kinesis'; + +new ApiGatewayToKinesisStreams(stack, 'test-apigateway-kinesis-additional-request-templates', { + additionalPutRecordRequestTemplates: { + 'text/plain': `{ "StreamName": "\${StreamName}", "Data": "$util.base64Encode($input.json('$.foo'))", "PartitionKey": "$input.path('$.bar')" }` + } +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts index 4d20bdbb8..1206ebd25 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts @@ -54,11 +54,19 @@ export interface ApiGatewayToSageMakerEndpointProps { */ readonly resourcePath: string, /** - * Mapping template to convert GET requests received on the REST API to POST requests expected by the SageMaker endpoint. + * Mapping template to convert GET requests for the default `application/json` content-type received + * on the REST API to POST requests expected by the SageMaker endpoint. * * @default - None. */ readonly requestMappingTemplate: string, + /** + * Optional Create Request Templates for content-types other than `application/json`. + * Use the `requestMappingTemplate` property to set the request template for the `application/json` content-type. + * + * @default - None + */ + readonly additionalRequestTemplates?: { [contentType: string]: string; }; /** * Optional mapping template to convert responses received from the SageMaker endpoint. * @@ -170,6 +178,7 @@ export class ApiGatewayToSageMakerEndpoint extends Construct { apiResource, requestValidator, requestTemplate: props.requestMappingTemplate, + additionalRequestTemplates: props.additionalRequestTemplates, awsIntegrationProps: { options: { integrationResponses: integResponses } }, diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/apigateway-sagemakerendpoint.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/apigateway-sagemakerendpoint.test.ts index 4631500cf..862b569c3 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/apigateway-sagemakerendpoint.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/apigateway-sagemakerendpoint.test.ts @@ -125,3 +125,25 @@ test('Test deployment w/ overwritten properties', () => { Description: 'existing role for SageMaker integration' }); }); + +test('Construct accepts additional read request templates', () => { + const stack = new Stack(); + new ApiGatewayToSageMakerEndpoint(stack, 'api-gateway-sagemaker-endpoint', { + endpointName: 'my-endpoint', + resourcePath: '{my_param}', + requestMappingTemplate: 'my-request-vtl-template', + additionalRequestTemplates: { + 'text/plain': 'additional-request-template' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + RequestTemplates: { + 'application/json': 'my-request-vtl-template', + 'text/plain': 'additional-request-template' + } + } + }); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.expected.json new file mode 100644 index 000000000..fb8b6a00d --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.expected.json @@ -0,0 +1,396 @@ +{ + "Description": "Integration Test for aws-apigateway-sagemakerendpoint", + "Resources": { + "testapigatewaysagemakerendpointdefaultApiAccessLogGroupAD5E1ADF": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaysagemakerendpointdefaultRestApiDeployment04BFEB63ab5dc870083af2a47a41af0f4ee69fff": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaysagemakerendpointdefaultRestApiuseridGETB3BB79AA", + "testapigatewaysagemakerendpointdefaultRestApiuserid9952BA11", + "testapigatewaysagemakerendpointdefaultRestApirequestvalidator1A23C251" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaysagemakerendpointdefaultRestApiDeploymentStageprodFD1743A7": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaysagemakerendpointdefaultApiAccessLogGroupAD5E1ADF", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApiDeployment04BFEB63ab5dc870083af2a47a41af0f4ee69fff" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaysagemakerendpointdefaultRestApiuserid9952BA11": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B", + "RootResourceId" + ] + }, + "PathPart": "{user_id}", + "RestApiId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + } + } + }, + "testapigatewaysagemakerendpointdefaultRestApiuseridGETB3BB79AA": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApiuserid9952BA11" + }, + "RestApiId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysagemakerendpointdefaultapigatewayrole8EA61BE4", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "SelectionPattern": "5\\d{2}", + "StatusCode": "500" + }, + { + "SelectionPattern": "4\\d{2}", + "StatusCode": "400" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": "{\n \"instances\": [\n#set( $user_id = $input.params(\"user_id\") )\n#set( $items = $input.params(\"items\") )\n#foreach( $item in $items.split(\",\") )\n {\"in0\": [$user_id], \"in1\": [$item]}#if( $foreach.hasNext ),#end\n $esc.newline\n#end\n ]\n}", + "text/plain": "{\n \"instances\": [\n#set( $user_id = $input.params(\"user_id\") )\n#set( $items = $input.params(\"items\") )\n#foreach( $item in $items.split(\",\") )\n {\"in0\": [$user_id], \"in1\": [$item]}#if( $foreach.hasNext ),#end\n $esc.newline\n#end\n ]\n}" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":runtime.sagemaker:path/endpoints/my-endpoint/invocations" + ] + ] + } + }, + "MethodResponses": [ + { + "StatusCode": "200" + }, + { + "StatusCode": "500" + }, + { + "StatusCode": "400" + } + ], + "RequestValidatorId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApirequestvalidator1A23C251" + } + } + }, + "testapigatewaysagemakerendpointdefaultRestApiUsagePlan7C5B0716": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + }, + "Stage": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApiDeploymentStageprodFD1743A7" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaysagemakerendpointdefaultRestApirequestvalidator1A23C251": { + "Type": "AWS::ApiGateway::RequestValidator", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + }, + "Name": "request-param-validator", + "ValidateRequestParameters": true + } + }, + "testapigatewaysagemakerendpointdefaultLambdaRestApiCloudWatchRole56EE67C8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaysagemakerendpointdefaultLambdaRestApiAccount6B3C7FDD": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaysagemakerendpointdefaultLambdaRestApiCloudWatchRole56EE67C8", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + ] + }, + "testapigatewaysagemakerendpointdefaultapigatewayrole8EA61BE4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaysagemakerendpointdefaultInvokeEndpointPolicyB835D2B2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sagemaker:InvokeEndpoint", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sagemaker:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":endpoint/my-endpoint" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaysagemakerendpointdefaultInvokeEndpointPolicyB835D2B2", + "Roles": [ + { + "Ref": "testapigatewaysagemakerendpointdefaultapigatewayrole8EA61BE4" + } + ] + } + } + }, + "Outputs": { + "testapigatewaysagemakerendpointdefaultRestApiEndpoint1EFF6760": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaysagemakerendpointdefaultRestApi7D1DA11B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaysagemakerendpointdefaultRestApiDeploymentStageprodFD1743A7" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.ts new file mode 100644 index 000000000..4fd4d8d63 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/test/integ.additional-request-templates.ts @@ -0,0 +1,49 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from 'aws-cdk-lib'; +import { ApiGatewayToSageMakerEndpoint, ApiGatewayToSageMakerEndpointProps } from '../lib'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-sagemakerendpoint'; + +// Definitions +const requestTemplate = +`{ + "instances": [ +#set( $user_id = $input.params("user_id") ) +#set( $items = $input.params("items") ) +#foreach( $item in $items.split(",") ) + {"in0": [$user_id], "in1": [$item]}#if( $foreach.hasNext ),#end + $esc.newline +#end + ] +}`; + +const props: ApiGatewayToSageMakerEndpointProps = { + endpointName: 'my-endpoint', + resourcePath: '{user_id}', + requestMappingTemplate: requestTemplate, + additionalRequestTemplates: { + 'text/plain': requestTemplate + } +}; + +new ApiGatewayToSageMakerEndpoint(stack, 'test-apigateway-sagemakerendpoint-default', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts index 27908be14..4b51d9f22 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts @@ -63,52 +63,54 @@ export interface ApiGatewayToSqsProps { */ readonly maxReceiveCount?: number; /** - * Whether to deploy an API Gateway Method for Create operations on the queue (i.e. sqs:SendMessage). + * Whether to deploy an API Gateway Method for POST HTTP operations on the queue (i.e. sqs:SendMessage). * * @default - false */ readonly allowCreateOperation?: boolean; /** - * API Gateway Request template for Create method for the default `application/json` content-type, - * if the `allowCreateOperation` property is set to true + * API Gateway Request Template for the create method for the default `application/json` content-type, + * if the `allowCreateOperation` property is set to true. * * @default - 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")' */ readonly createRequestTemplate?: string; /** - * Optional Create request templates for content-types other than `application/json`. + * Optional Create Request Templates for content-types other than `application/json`. * Use the `createRequestTemplate` property to set the request template for the `application/json` content-type. * * @default - None */ readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; /** - * Whether to deploy an API Gateway Method for Read operations on the queue (i.e. sqs:ReceiveMessage). + * Whether to deploy an API Gateway Method for GET HTTP operations on the queue (i.e. sqs:ReceiveMessage). * - * @default - "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")" + * @default - false */ readonly allowReadOperation?: boolean; /** - * API Gateway Request template for Get method, if allowReadOperation set to true + * API Gateway Request Template for the read method for the default `application/json` content-type, + * if the `allowReadOperation` property is set to true. * * @default - "Action=ReceiveMessage" */ readonly readRequestTemplate?: string; /** - * Optional Read request templates for content-types other than `application/json`. + * Optional Read Request Templates for content-types other than `application/json`. * Use the `readRequestTemplate` property to set the request template for the `application/json` content-type. * * @default - None */ readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; /** - * Whether to deploy an API Gateway Method for Delete operations on the queue (i.e. sqs:DeleteMessage). + * Whether to deploy an API Gateway Method for HTTP DELETE operations on the queue (i.e. sqs:DeleteMessage). * * @default - false */ readonly allowDeleteOperation?: boolean; /** - * API Gateway Request template for Delete method, if allowDeleteOperation set to true + * API Gateway Request Template for THE delete method for the default `application/json` content-type, + * if the `allowDeleteOperation` property is set to true. * * @default - "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))" */ diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts index f4a1b97d8..44e4a9fb2 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts @@ -311,7 +311,7 @@ test('Construct can override default create request template type', () => { allowCreateOperation: true, createRequestTemplate: 'Hello', additionalCreateRequestTemplates: { - 'text/plain': 'Goodbye' + 'text/plain': 'Goodbye', } }); @@ -324,4 +324,25 @@ test('Construct can override default create request template type', () => { } } }); +}); + +test('Construct accepts additional delete request templates', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + enableEncryptionWithCustomerManagedKey: true, + allowDeleteOperation: true, + additionalDeleteRequestTemplates: { + 'text/plain': 'DeleteMe' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + RequestTemplates: { + 'application/json': `Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))`, + 'text/plain': 'DeleteMe' + } + } + }); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.expected.json new file mode 100644 index 000000000..32f5d3a15 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.expected.json @@ -0,0 +1,646 @@ +{ + "Description": "Integration Test for aws-apigateway-sqs", + "Resources": { + "testapigatewaysqsdefaultdeadLetterQueue24467CAD": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testapigatewaysqsdefaultdeadLetterQueuePolicyEF507332": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage", + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:RemovePermission", + "sqs:AddPermission", + "sqs:SetQueueAttributes" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultdeadLetterQueue24467CAD", + "Arn" + ] + }, + "Sid": "QueueOwnerOnlyAccess" + }, + { + "Action": "SQS:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultdeadLetterQueue24467CAD", + "Arn" + ] + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "testapigatewaysqsdefaultdeadLetterQueue24467CAD" + } + ] + } + }, + "testapigatewaysqsdefaultqueueCAC098BE": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs", + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultdeadLetterQueue24467CAD", + "Arn" + ] + }, + "maxReceiveCount": 15 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testapigatewaysqsdefaultqueuePolicy529DEC31": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage", + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:RemovePermission", + "sqs:AddPermission", + "sqs:SetQueueAttributes" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultqueueCAC098BE", + "Arn" + ] + }, + "Sid": "QueueOwnerOnlyAccess" + }, + { + "Action": "SQS:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultqueueCAC098BE", + "Arn" + ] + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "testapigatewaysqsdefaultqueueCAC098BE" + } + ] + } + }, + "testapigatewaysqsdefaultApiAccessLogGroup16132600": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaysqsdefaultRestApi554243C3": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaysqsdefaultRestApiDeploymentFB9688F5aa398c65c945d66dd485b50d59cc7a25": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaysqsdefaultRestApiGET733E6394", + "testapigatewaysqsdefaultRestApimessage41073D7F", + "testapigatewaysqsdefaultRestApiPOSTD8ACD1CB" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaysqsdefaultRestApiDeploymentStageprod600FEEE2": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultApiAccessLogGroup16132600", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaysqsdefaultRestApiDeploymentFB9688F5aa398c65c945d66dd485b50d59cc7a25" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaysqsdefaultRestApimessage41073D7F": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultRestApi554243C3", + "RootResourceId" + ] + }, + "PathPart": "message", + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + } + } + }, + "testapigatewaysqsdefaultRestApiPOSTD8ACD1CB": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultRestApi554243C3", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultapigatewayrole080B85EC", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")", + "text/plain": "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultqueueCAC098BE", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsdefaultRestApiGET733E6394": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultRestApi554243C3", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultapigatewayrole080B85EC", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=ReceiveMessage" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultqueueCAC098BE", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsdefaultRestApiUsagePlan3475CA67": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "Stage": { + "Ref": "testapigatewaysqsdefaultRestApiDeploymentStageprod600FEEE2" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaysqsdefaultLambdaRestApiCloudWatchRole8EA3C5EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaysqsdefaultLambdaRestApiAccountF7D19F4F": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultLambdaRestApiCloudWatchRole8EA3C5EC", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaysqsdefaultRestApi554243C3" + ] + }, + "testapigatewaysqsdefaultapigatewayrole080B85EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaysqsdefaultapigatewayroleDefaultPolicyFF253592": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultqueueCAC098BE", + "Arn" + ] + } + }, + { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultqueueCAC098BE", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaysqsdefaultapigatewayroleDefaultPolicyFF253592", + "Roles": [ + { + "Ref": "testapigatewaysqsdefaultapigatewayrole080B85EC" + } + ] + } + } + }, + "Outputs": { + "testapigatewaysqsdefaultRestApiEndpointE6DCCE4E": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaysqsdefaultRestApiDeploymentStageprod600FEEE2" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.ts new file mode 100644 index 000000000..dd9c2494f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.additional-request-templates.ts @@ -0,0 +1,32 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "aws-cdk-lib"; +import { ApiGatewayToSqs } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-sqs'; + +new ApiGatewayToSqs(stack, 'test-api-gateway-sqs-default', { + allowCreateOperation: true, + additionalCreateRequestTemplates: { + 'text/plain': 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")' + } +}); + +// Synth +app.synth(); \ No newline at end of file From e0a8ed6d32eed7db596a8aefb48bb556a53f0d6d Mon Sep 17 00:00:00 2001 From: George Bearden Date: Tue, 31 Jan 2023 16:12:19 -0500 Subject: [PATCH 3/9] Update READMEs to include new optional request template properties. --- .../aws-apigateway-dynamodb/README.md | 22 ++++++++++++------- ...additional-request-templates.expected.json | 20 ++++++++++++----- .../integ.additional-request-templates.ts | 4 +--- .../aws-apigateway-kinesisstreams/README.md | 2 ++ .../README.md | 3 ++- .../lib/index.ts | 2 +- .../aws-apigateway-sqs/README.md | 16 +++++++++----- 7 files changed, 45 insertions(+), 24 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md index 6f6d12ea3..59781b066 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md @@ -65,14 +65,20 @@ new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatew |dynamoTableProps?|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table.| |existingTableObj?|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.Table.html)|Existing instance of DynamoDB table object, providing both this and `dynamoTableProps` will cause an error.| |apiGatewayProps?|[`api.RestApiProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| -|allowCreateOperation?|`boolean`|Whether to deploy API Gateway Method for Create operation on DynamoDB table.| -|createRequestTemplate?|`string`|API Gateway Request template for Create method, required if `allowCreateOperation` set to true.| -|allowReadOperation?|`boolean`|Whether to deploy API Gateway Method for Read operation on DynamoDB table.| -|readRequestTemplate?|`string`|Optional API Gateway Request template for Read method, it will use the default template if `allowReadOperation` is true and `readRequestTemplate` is not provided. The default template only supports a partition key and not partition + sort keys.| -|allowUpdateOperation?|`boolean`|Whether to deploy API Gateway Method for Update operation on DynamoDB table.| -|updateRequestTemplate?|`string`|API Gateway Request template for Update method, required if `allowUpdateOperation` set to true.| -|allowDeleteOperation?|`boolean`|Whether to deploy API Gateway Method for Delete operation on DynamoDB table.| -|deleteRequestTemplate?|`string`|Optional API Gateway Request template for Delete method, it will use the default template if `allowDeleteOperation` is true and `deleteRequestTemplate` is not provided. The default template only supports a partition key and not partition + sort keys.| + +|allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the DynamoDB table (i.e. dynamodb:PutItem).| +|createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type. This property is required if the `allowCreateOperation` property is set to true.| +|additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| +|allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for GET HTTP operations on DynamoDB table (i.e. dynamodb:Query).| +|readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type, if the `allowReadOperation` property is set to true. The default template only supports a partition key and not partition + sort keys.| +|additionalReadRequestTemplates?|`{ [contentType: string]: string; }`|Optional Read Request Templates for content-types other than `application/json`. Use the `readRequestTemplate` property to set the request template for the `application/json` content-type.| +|allowUpdateOperation?|`boolean`|Whether to deploy API Gateway Method for PUT HTTP operations on DynamoDB table (i.e. dynamodb:UpdateItem).| +|updateRequestTemplate?|`string`|API Gateway Request Template for the update method, required if the `allowUpdateOperation` property is set to true.| +|additionalUpdateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Update Request Templates for content-types other than `application/json`. Use the `updateRequestTemplate` property to set the request template for the `application/json` content-type.| +|allowDeleteOperation?|`boolean`|Whether to deploy API Gateway Method for DELETE HTTP operations on DynamoDB table (i.e. dynamodb:DeleteItem).| +|deleteRequestTemplate?|`string`|API Gateway Request Template for the delete method for the default `application/json` content-type, if the `allowDeleteOperation` property is set to true.| +|additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type.| + |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| ## Pattern Properties diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json index cf90c8719..fd2c4df63 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.expected.json @@ -22,8 +22,7 @@ }, "SSESpecification": { "SSEEnabled": true - }, - "TableName": "test-table" + } }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" @@ -58,7 +57,7 @@ "Name": "RestApi" } }, - "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47748f8e2ffb1e74060b1bf8f54a7cd2009": { + "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47741477f52c0bb6da128d2999e75c543fc": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -97,7 +96,7 @@ "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" }, "DeploymentId": { - "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47748f8e2ffb1e74060b1bf8f54a7cd2009" + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47741477f52c0bb6da128d2999e75c543fc" }, "MethodSettings": [ { @@ -174,7 +173,18 @@ ] ] }, - "text/plain": "{ \"TableName\": \"test-table\", \"KeyConditionExpression\": \"id = :v1\", \"ExpressionAttributeValues\": { \":v1\": { \"S\": \"$input.params('id')\" } } }" + "text/plain": { + "Fn::Join": [ + "", + [ + "{ \"TableName\": \"", + { + "Ref": "existingtableE51CCC93" + }, + "\", \"KeyConditionExpression\": \"id = :v1\", \"ExpressionAttributeValues\": { \":v1\": { \"S\": \"$input.params('id')\" } } }" + ] + ] + } }, "Type": "AWS", "Uri": { diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts index 0189f23c7..87a7427ae 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.additional-request-templates.ts @@ -22,11 +22,9 @@ const app = new App(); const stack = new Stack(app, generateIntegStackName(__filename)); stack.templateOptions.description = 'Integration Test for aws-apigateway-dynamodb'; -const tableName = 'test-table'; const partitionKeyName = 'id'; const existingTableObj = new dynamodb.Table(stack, 'existing-table', { - tableName, partitionKey: { name: partitionKeyName, type: dynamodb.AttributeType.STRING, @@ -40,7 +38,7 @@ new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-additional-request-te existingTableObj, additionalReadRequestTemplates: { 'text/plain': `{ \ - "TableName": "${tableName}", \ + "TableName": "${existingTableObj.tableName}", \ "KeyConditionExpression": "${partitionKeyName} = :v1", \ "ExpressionAttributeValues": { \ ":v1": { \ diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md index cb6ce60e1..83968755a 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md @@ -61,8 +61,10 @@ new ApiGatewayToKinesisStreams(this, "test-apigw-kinesis", new ApiGatewayToKines |:-------------|:----------------|-----------------| |apiGatewayProps?|[`api.RestApiProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| |putRecordRequestTemplate?|`string`|API Gateway request template for the PutRecord action. If not provided, a default one will be used.| +|additionalPutRecordRequestTemplates?|`{ [contentType: string]: string; }`|Optional PutRecord Request Templates for content-types other than `application/json`. Use the `putRecordRequestTemplate` property to set the request template for the `application/json` content-type.| |putRecordRequestModel?|[`api.ModelOptions`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.ModelOptions.html)|API Gateway request model for the PutRecord action. If not provided, a default one will be created.| |putRecordsRequestTemplate?|`string`|API Gateway request template for the PutRecords action. If not provided, a default one will be used.| +|additionalPutRecordsRequestTemplates?|`{ [contentType: string]: string; }`|Optional PutRecords Request Templates for content-types other than `application/json`. Use the `putRecordsRequestTemplate` property to set the request template for the `application/json` content-type.| |putRecordsRequestModel?|[`api.ModelOptions`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.ModelOptions.html)|API Gateway request model for the PutRecords action. If not provided, a default one will be created.| |existingStreamObj?|[`kinesis.Stream`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kinesis.Stream.html)|Existing instance of Kinesis Stream, providing both this and `kinesisStreamProps` will cause an error.| |kinesisStreamProps?|[`kinesis.StreamProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kinesis.StreamProps.html)|Optional user-provided props to override the default props for the Kinesis stream.| diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md index 8e9bd5000..2ddda8d88 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md @@ -123,7 +123,8 @@ new ApiGatewayToSageMakerEndpoint(this, "ApiGatewayToSageMakerEndpointPattern", |endpointName|`string`|Name of the deployed SageMaker inference endpoint.| |resourceName?|`string`|Optional resource name where the GET method will be available.| |resourcePath|`string`|Resource path for the GET method. The variable defined here can be referenced in `requestMappingTemplate`.| -|requestMappingTemplate|`string`|Mapping template to convert GET requests received on the REST API to POST requests expected by the SageMaker endpoint.| +|requestMappingTemplate|`string`|Mapping template to convert GET requests for the default `application/json` content-type received on the REST API to POST requests expected by the SageMaker endpoint.| +|additionalRequestTemplates|`{ [contentType: string]: string; }`|Optional Request Templates for content-types other than `application/json`. Use the `requestMappingTemplate` property to set the request template for the `application/json` content-type.| |responseMappingTemplate?|`string`|Optional mapping template to convert responses received from the SageMaker endpoint.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts index 1206ebd25..a40992851 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/lib/index.ts @@ -61,7 +61,7 @@ export interface ApiGatewayToSageMakerEndpointProps { */ readonly requestMappingTemplate: string, /** - * Optional Create Request Templates for content-types other than `application/json`. + * Optional Request Templates for content-types other than `application/json`. * Use the `requestMappingTemplate` property to set the request template for the `application/json` content-type. * * @default - None diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md index 9e9d998a5..400c9aec7 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md @@ -62,12 +62,16 @@ new ApiGatewayToSqs(this, "ApiGatewayToSqsPattern", new ApiGatewayToSqsProps.Bui |queueProps?|[`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.QueueProps.html)|Optional user-provided props to override the default props for the queue.| |deployDeadLetterQueue?|`boolean`|Whether to deploy a secondary queue to be used as a dead letter queue. Defaults to `true`.| |maxReceiveCount|`number`|The number of times a message can be unsuccessfully dequeued before being moved to the dead-letter queue.| -|allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for Create operations on the queue (i.e. sqs:SendMessage).| -|createRequestTemplate?|`string`|Override the default API Gateway Request template for Create method, if allowCreateOperation set to true.| -|allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for Read operations on the queue (i.e. sqs:ReceiveMessage).| -|readRequestTemplate?|`string`|Override the default API Gateway Request template for Read method, if allowReadOperation set to true.| -|allowDeleteOperation?|`boolean`|Whether to deploy an API Gateway Method for Delete operations on the queue (i.e. sqs:DeleteMessage).| -|deleteRequestTemplate?|`string`|Override the default API Gateway Request template for Delete method, if allowDeleteOperation set to true.| + +|allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the queue (i.e. sqs:SendMessage).| +|createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type, if the `allowCreateOperation` property is set to true.| +|additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| +|allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for GET HTTP operations on the queue (i.e. sqs:ReceiveMessage).| +|readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type, if the `allowReadOperation` property is set to true.| +|additionalReadRequestTemplates?|`{ [contentType: string]: string; }`|Optional Read Request Templates for content-types other than `application/json`. Use the `readRequestTemplate` property to set the request template for the `application/json` content-type.| +|allowDeleteOperation?|`boolean`|Whether to deploy an API Gateway Method for HTTP DELETE operations on the queue (i.e. sqs:DeleteMessage).| +|deleteRequestTemplate?|`string`|API Gateway Request Template for THE delete method for the default `application/json` content-type, if the `allowDeleteOperation` property is set to true.| +|additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| |enableEncryptionWithCustomerManagedKey?|`boolean`|If no key is provided, this flag determines whether the queue is encrypted with a new CMK or an AWS managed key. This flag is ignored if any of the following are defined: queueProps.encryptionMasterKey, encryptionKey or encryptionKeyProps.| |encryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html)|An optional, imported encryption key to encrypt the SQS Queue with.| From cd97159bd5326e41c680dcdcb278d20870a2ea8c Mon Sep 17 00:00:00 2001 From: George Bearden Date: Wed, 1 Feb 2023 10:00:19 -0500 Subject: [PATCH 4/9] remove a blank line. --- .../@aws-solutions-constructs/aws-apigateway-dynamodb/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md index 59781b066..63e41f7ba 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md @@ -65,7 +65,6 @@ new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatew |dynamoTableProps?|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table.| |existingTableObj?|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.Table.html)|Existing instance of DynamoDB table object, providing both this and `dynamoTableProps` will cause an error.| |apiGatewayProps?|[`api.RestApiProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| - |allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the DynamoDB table (i.e. dynamodb:PutItem).| |createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type. This property is required if the `allowCreateOperation` property is set to true.| |additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| From 2349d37c137fec1cf51cb86a16d1841b9ea3c600 Mon Sep 17 00:00:00 2001 From: George Bearden Date: Wed, 1 Feb 2023 10:07:28 -0500 Subject: [PATCH 5/9] Add unit test for new additionalRequestTemplate property on apigateway-helper --- .../core/test/apigateway-helper.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts index 23fa9bd94..e5cbbbb91 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts @@ -650,4 +650,42 @@ test('Test for ApiKey creation using lambdaApiProps', () => { expect(stack).toHaveResourceLike("AWS::ApiGateway::UsagePlanKey", { KeyType: "API_KEY" }); +}); + +test('Test addMethodToApiResource with action', () => { + const stack = new Stack(); + const [ restApi ] = defaults.GlobalRestApi(stack); + + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = restApi.root.addResource('api-gateway-resource'); + const requestTemplate = '{}'; + const additionalRequestTemplates = { + 'text/plain': 'additional-request-template' + }; + + // Add Method + defaults.addProxyMethodToApiResource({ + action: "Query", + service: "dynamodb", + apiResource: apiGatewayResource, + apiGatewayRole, + apiMethod: "GET", + requestTemplate, + additionalRequestTemplates + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + RequestTemplates: { + 'application/json': `{}`, + 'text/plain': 'additional-request-template' + } + } + }); }); \ No newline at end of file From e687a2c7a7a603b2a69be746aa6135db9f14e8a9 Mon Sep 17 00:00:00 2001 From: George Bearden Date: Wed, 1 Feb 2023 17:23:47 -0500 Subject: [PATCH 6/9] Add optional integration response properties to the apigateway-based constructs. --- .../aws-apigateway-dynamodb/README.md | 5 +- .../aws-apigateway-dynamodb/lib/index.ts | 37 +- .../test/apigateway-dynamodb.test.ts | 202 ++++++ ...custom-integration-responses.expected.json | 402 ++++++++++++ .../integ.custom-integration-responses.ts | 50 ++ .../aws-apigateway-kinesisstreams/README.md | 5 +- .../lib/index.ts | 18 +- .../test/apigateway-kinesis.test.ts | 79 +++ ...custom-integration-responses.expected.json | 616 ++++++++++++++++++ .../integ.custom-integration-responses.ts | 36 + .../aws-apigateway-sqs/README.md | 4 +- .../aws-apigateway-sqs/lib/index.ts | 27 +- .../test/apigateway-sqs.test.ts | 150 +++++ ...custom-integration-responses.expected.json | 545 ++++++++++++++++ .../integ.custom-integration-responses.ts | 37 ++ .../core/lib/apigateway-helper.ts | 28 +- .../core/test/apigateway-helper.test.ts | 87 +++ 17 files changed, 2304 insertions(+), 24 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md index 63e41f7ba..623727cdd 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md @@ -68,16 +68,19 @@ new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatew |allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the DynamoDB table (i.e. dynamodb:PutItem).| |createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type. This property is required if the `allowCreateOperation` property is set to true.| |additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| +|createIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true.| |allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for GET HTTP operations on DynamoDB table (i.e. dynamodb:Query).| |readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type, if the `allowReadOperation` property is set to true. The default template only supports a partition key and not partition + sort keys.| |additionalReadRequestTemplates?|`{ [contentType: string]: string; }`|Optional Read Request Templates for content-types other than `application/json`. Use the `readRequestTemplate` property to set the request template for the `application/json` content-type.| +|readIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true.| |allowUpdateOperation?|`boolean`|Whether to deploy API Gateway Method for PUT HTTP operations on DynamoDB table (i.e. dynamodb:UpdateItem).| |updateRequestTemplate?|`string`|API Gateway Request Template for the update method, required if the `allowUpdateOperation` property is set to true.| |additionalUpdateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Update Request Templates for content-types other than `application/json`. Use the `updateRequestTemplate` property to set the request template for the `application/json` content-type.| +|updateIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the update method, if the `allowUpdateOperation` property is set to true.| |allowDeleteOperation?|`boolean`|Whether to deploy API Gateway Method for DELETE HTTP operations on DynamoDB table (i.e. dynamodb:DeleteItem).| |deleteRequestTemplate?|`string`|API Gateway Request Template for the delete method for the default `application/json` content-type, if the `allowDeleteOperation` property is set to true.| |additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type.| - +|deleteIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| ## Pattern Properties diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts index eca732cd6..41983a818 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts @@ -16,6 +16,7 @@ import * as iam from 'aws-cdk-lib/aws-iam'; import * as defaults from '@aws-solutions-constructs/core'; // Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate import { Construct } from 'constructs'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import { getPartitionKeyNameFromTable } from '@aws-solutions-constructs/core'; import * as logs from 'aws-cdk-lib/aws-logs'; @@ -62,6 +63,12 @@ export interface ApiGatewayToDynamoDBProps { * @default - None */ readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly createIntegrationResponses?: apigateway.IntegrationResponse[]; /** * Whether to deploy an API Gateway Method for GET HTTP operations on DynamoDB table (i.e. dynamodb:Query). * @@ -92,6 +99,12 @@ export interface ApiGatewayToDynamoDBProps { * @default - None */ readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly readIntegrationResponses?: apigateway.IntegrationResponse[]; /** * Whether to deploy API Gateway Method for PUT HTTP operations on DynamoDB table (i.e. dynamodb:UpdateItem). * @@ -111,6 +124,12 @@ export interface ApiGatewayToDynamoDBProps { * @default - None */ readonly additionalUpdateRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the update method, if the `allowUpdateOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly updateIntegrationResponses?: apigateway.IntegrationResponse[]; /** * Whether to deploy API Gateway Method for DELETE HTTP operations on DynamoDB table (i.e. dynamodb:DeleteItem). * @@ -139,6 +158,12 @@ export interface ApiGatewayToDynamoDBProps { * @default - None */ readonly additionalDeleteRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly deleteIntegrationResponses?: apigateway.IntegrationResponse[]; /** * User provided props to override the default props for the CloudWatchLogs LogGroup. * @@ -208,7 +233,8 @@ export class ApiGatewayToDynamoDB extends Construct { apiMethod: "POST", apiResource: this.apiGateway.root, requestTemplate: createRequestTemplate, - additionalRequestTemplates: props.additionalCreateRequestTemplates + additionalRequestTemplates: props.additionalCreateRequestTemplates, + integrationResponses: props.createIntegrationResponses }); } // Read @@ -232,7 +258,8 @@ export class ApiGatewayToDynamoDB extends Construct { apiMethod: "GET", apiResource: apiGatewayResource, requestTemplate: readRequestTemplate, - additionalRequestTemplates: props.additionalReadRequestTemplates + additionalRequestTemplates: props.additionalReadRequestTemplates, + integrationResponses: props.readIntegrationResponses }); } // Update @@ -246,7 +273,8 @@ export class ApiGatewayToDynamoDB extends Construct { apiMethod: "PUT", apiResource: apiGatewayResource, requestTemplate: updateRequestTemplate, - additionalRequestTemplates: props.additionalUpdateRequestTemplates + additionalRequestTemplates: props.additionalUpdateRequestTemplates, + integrationResponses: props.updateIntegrationResponses }); } // Delete @@ -270,7 +298,8 @@ export class ApiGatewayToDynamoDB extends Construct { apiMethod: "DELETE", apiResource: apiGatewayResource, requestTemplate: deleteRequestTemplate, - additionalRequestTemplates: props.additionalDeleteRequestTemplates + additionalRequestTemplates: props.additionalDeleteRequestTemplates, + integrationResponses: props.deleteIntegrationResponses }); } } diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts index 8d645d1a1..804e014f6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts @@ -329,4 +329,206 @@ test('Construct accepts additional delete request templates', () => { } } }); +}); + +test('Construct uses default integration responses', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowCreateOperation: true, + allowReadOperation: true, + allowUpdateOperation: true, + allowDeleteOperation: true, + createRequestTemplate: 'create', + updateRequestTemplate: 'update' + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'PUT', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); +}); + +test('Construct uses custom createIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowCreateOperation: true, + createRequestTemplate: 'OK', + createIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); +}); + +test('Construct uses custom readIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowReadOperation: true, + readIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); +}); + +test('Construct uses custom updateIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowUpdateOperation: true, + updateRequestTemplate: 'OK', + updateIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'PUT', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); +}); + +test('Construct uses custom deleteIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowDeleteOperation: true, + deleteIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json new file mode 100644 index 000000000..8fa2766ed --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json @@ -0,0 +1,402 @@ +{ + "Description": "Integration Test for aws-apigateway-dynamodb", + "Resources": { + "existingtableE51CCC93": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaydynamodbadditionalrequesttemplatesApiAccessLogGroupAF75D750": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C477ebd2134b237add637a36f3483536b1fd": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidGET05129D15", + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidA77CCE90" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeploymentStageprod33ED5D23": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesApiAccessLogGroupAF75D750", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C477ebd2134b237add637a36f3483536b1fd" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidA77CCE90": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C", + "RootResourceId" + ] + }, + "PathPart": "{id}", + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiidGET05129D15": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiidA77CCE90" + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleFDAECAC6", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "text/html": "OK" + }, + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{ \"TableName\": \"", + { + "Ref": "existingtableE51CCC93" + }, + "\", \"KeyConditionExpression\": \"id = :v1\", \"ExpressionAttributeValues\": { \":v1\": { \"S\": \"$input.params('id')\" } } }" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/Query" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesRestApiUsagePlan905D10C7": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + "Stage": { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeploymentStageprod33ED5D23" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesLambdaRestApiCloudWatchRole51265771": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesLambdaRestApiAccount8891474D": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbadditionalrequesttemplatesLambdaRestApiCloudWatchRole51265771", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + ] + }, + "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleFDAECAC6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleDefaultPolicy4C47B35E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:Query", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "existingtableE51CCC93", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleDefaultPolicy4C47B35E", + "Roles": [ + { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesapigatewayroleFDAECAC6" + } + ] + } + } + }, + "Outputs": { + "testapigatewaydynamodbadditionalrequesttemplatesRestApiEndpoint855E7762": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApi03F6484C" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeploymentStageprod33ED5D23" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts new file mode 100644 index 000000000..ad28eb6e1 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts @@ -0,0 +1,50 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "aws-cdk-lib"; +import { ApiGatewayToDynamoDB } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-dynamodb'; + +const partitionKeyName = 'id'; + +const existingTableObj = new dynamodb.Table(stack, 'existing-table', { + partitionKey: { + name: partitionKeyName, + type: dynamodb.AttributeType.STRING, + }, + pointInTimeRecovery: true, + encryption: dynamodb.TableEncryption.AWS_MANAGED, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST +}); + +new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-additional-request-templates', { + existingTableObj, + readIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md index 83968755a..ab85b9a29 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md @@ -63,9 +63,12 @@ new ApiGatewayToKinesisStreams(this, "test-apigw-kinesis", new ApiGatewayToKines |putRecordRequestTemplate?|`string`|API Gateway request template for the PutRecord action. If not provided, a default one will be used.| |additionalPutRecordRequestTemplates?|`{ [contentType: string]: string; }`|Optional PutRecord Request Templates for content-types other than `application/json`. Use the `putRecordRequestTemplate` property to set the request template for the `application/json` content-type.| |putRecordRequestModel?|[`api.ModelOptions`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.ModelOptions.html)|API Gateway request model for the PutRecord action. If not provided, a default one will be created.| +|putRecordIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the PutRecord action.| |putRecordsRequestTemplate?|`string`|API Gateway request template for the PutRecords action. If not provided, a default one will be used.| |additionalPutRecordsRequestTemplates?|`{ [contentType: string]: string; }`|Optional PutRecords Request Templates for content-types other than `application/json`. Use the `putRecordsRequestTemplate` property to set the request template for the `application/json` content-type.| -|putRecordsRequestModel?|[`api.ModelOptions`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.ModelOptions.html)|API Gateway request model for the PutRecords action. If not provided, a default one will be created.| +|putRecordsRequestModel?|[`api.ModelOptions`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.ModelOptions.html)| +API Gateway request model for the PutRecords action. If not provided, a default one will be created.| +|putRecordsIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the PutRecords action.| |existingStreamObj?|[`kinesis.Stream`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kinesis.Stream.html)|Existing instance of Kinesis Stream, providing both this and `kinesisStreamProps` will cause an error.| |kinesisStreamProps?|[`kinesis.StreamProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kinesis.StreamProps.html)|Optional user-provided props to override the default props for the Kinesis stream.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts index 20c175a78..44bb47a02 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts @@ -54,6 +54,12 @@ export interface ApiGatewayToKinesisStreamsProps { * "required":["data","partitionKey"],"properties":{"data":{"type":"string"},"partitionKey":{"type":"string"}}} */ readonly putRecordRequestModel?: api.ModelOptions; + /** + * Optional, custom API Gateway Integration Response for the PutRecord action. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly putRecordIntegrationResponses?: api.IntegrationResponse[]; /** * API Gateway request template for the PutRecords action for the default `application/json` content-type. * If not provided, a default one will be used. @@ -78,6 +84,12 @@ export interface ApiGatewayToKinesisStreamsProps { * "required":["data","partitionKey"],"properties":{"data":{"type":"string"},"partitionKey":{"type":"string"}}}}}} */ readonly putRecordsRequestModel?: api.ModelOptions; + /** + * Optional, custom API Gateway Integration Response for the PutRecord action. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly putRecordsIntegrationResponses?: api.IntegrationResponse[]; /** * Existing instance of Kinesis Stream, providing both this and `kinesisStreamProps` will cause an error. * @@ -161,7 +173,8 @@ export class ApiGatewayToKinesisStreams extends Construct { additionalRequestTemplates: this.getAdditionalPutRecordTemplates(props.additionalPutRecordRequestTemplates), contentType: "'x-amz-json-1.1'", requestValidator, - requestModel: { 'application/json': this.getPutRecordModel(props.putRecordRequestModel) } + requestModel: { 'application/json': this.getPutRecordModel(props.putRecordRequestModel) }, + integrationResponses: props.putRecordIntegrationResponses }); // PutRecords @@ -176,7 +189,8 @@ export class ApiGatewayToKinesisStreams extends Construct { additionalRequestTemplates: this.getAdditionalPutRecordTemplates(props.additionalPutRecordsRequestTemplates), contentType: "'x-amz-json-1.1'", requestValidator, - requestModel: { 'application/json': this.getPutRecordsModel(props.putRecordsRequestModel) } + requestModel: { 'application/json': this.getPutRecordsModel(props.putRecordsRequestModel) }, + integrationResponses: props.putRecordsIntegrationResponses }); if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts index 555b38d19..435d9b489 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts @@ -146,4 +146,83 @@ test('Construct accepts additional PutRecords request templates', () => { } } }); +}); + +test('Construct uses default integration responses', () => { + const stack = new Stack(); + new ApiGatewayToKinesisStreams(stack, 'api-gateway-kinesis-streamsĀ ', {}); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); +}); + +test('Construct uses custom putRecordIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToKinesisStreams(stack, 'api-gateway-kinesis-streamsĀ ', { + putRecordIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); +}); + +test('Construct uses custom putRecordsIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToKinesisStreams(stack, 'api-gateway-kinesis-streamsĀ ', { + putRecordsIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.expected.json new file mode 100644 index 000000000..32f03b4dd --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.expected.json @@ -0,0 +1,616 @@ +{ + "Description": "Integration Test for aws-apigateway-kinesis", + "Resources": { + "testapigatewaykinesisadditionalrequesttemplatesApiAccessLogGroup9C079B68": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiDeployment5A447E3D68efb2650de4064374902887dee80c33": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordPOST307FC87D", + "testapigatewaykinesisadditionalrequesttemplatesRestApirecord01520200", + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordsPOST5F6260A2", + "testapigatewaykinesisadditionalrequesttemplatesRestApirecords37B412D1", + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordModel1A75CC15", + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordsModel49E0CAB9", + "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiDeploymentStageprodD274025B": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesApiAccessLogGroup9C079B68", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiDeployment5A447E3D68efb2650de4064374902887dee80c33" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecord01520200": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7", + "RootResourceId" + ] + }, + "PathPart": "record", + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordPOST307FC87D": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirecord01520200" + }, + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "text/html": "OK" + }, + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'x-amz-json-1.1'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{ \"StreamName\": \"", + { + "Ref": "KinesisStream46752A3E" + }, + "\", \"Data\": \"$util.base64Encode($input.json('$.data'))\", \"PartitionKey\": \"$input.path('$.partitionKey')\" }" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":kinesis:action/PutRecord" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ], + "RequestModels": { + "application/json": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordModel1A75CC15" + } + }, + "RequestValidatorId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecords37B412D1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7", + "RootResourceId" + ] + }, + "PathPart": "records", + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirecordsPOST5F6260A2": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirecords37B412D1" + }, + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'x-amz-json-1.1'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{ \"StreamName\": \"", + { + "Ref": "KinesisStream46752A3E" + }, + "\", \"Records\": [ #foreach($elem in $input.path('$.records')) { \"Data\": \"$util.base64Encode($elem.data)\", \"PartitionKey\": \"$elem.partitionKey\"}#if($foreach.hasNext),#end #end ] }" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":kinesis:action/PutRecords" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ], + "RequestModels": { + "application/json": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordsModel49E0CAB9" + } + }, + "RequestValidatorId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiUsagePlan68D85E30": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "Stage": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiDeploymentStageprodD274025B" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApirequestvalidator69E589CE": { + "Type": "AWS::ApiGateway::RequestValidator", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "Name": "request-body-validator", + "ValidateRequestBody": true + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordModel1A75CC15": { + "Type": "AWS::ApiGateway::Model", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "ContentType": "application/json", + "Description": "PutRecord proxy single-record payload", + "Name": "PutRecordModel", + "Schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "PutRecord proxy single-record payload", + "type": "object", + "required": [ + "data", + "partitionKey" + ], + "properties": { + "data": { + "type": "string" + }, + "partitionKey": { + "type": "string" + } + } + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesRestApiPutRecordsModel49E0CAB9": { + "Type": "AWS::ApiGateway::Model", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + "ContentType": "application/json", + "Description": "PutRecords proxy payload data", + "Name": "PutRecordsModel", + "Schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "PutRecords proxy payload data", + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "object", + "required": [ + "data", + "partitionKey" + ], + "properties": { + "data": { + "type": "string" + }, + "partitionKey": { + "type": "string" + } + } + } + } + } + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesLambdaRestApiCloudWatchRole4FA3BA8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaykinesisadditionalrequesttemplatesLambdaRestApiAccount9A5A772A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaykinesisadditionalrequesttemplatesLambdaRestApiCloudWatchRole4FA3BA8A", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + ] + }, + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleDefaultPolicy5D60DBDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:ListShards", + "kinesis:PutRecord", + "kinesis:PutRecords" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KinesisStream46752A3E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleDefaultPolicy5D60DBDF", + "Roles": [ + { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesapigatewayroleAC79617D" + } + ] + } + }, + "testapigatewaykinesisadditionalrequesttemplatesKinesisStreamGetRecordsIteratorAgeAlarm05247CB0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Consumer Record Processing Falling Behind, there is risk for data loss due to record expiration.", + "MetricName": "GetRecords.IteratorAgeMilliseconds", + "Namespace": "AWS/Kinesis", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 43200000 + } + }, + "testapigatewaykinesisadditionalrequesttemplatesKinesisStreamReadProvisionedThroughputExceededAlarmE49197EC": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Consumer Application is Reading at a Slower Rate Than Expected.", + "MetricName": "ReadProvisionedThroughputExceeded", + "Namespace": "AWS/Kinesis", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } + }, + "KinesisStream46752A3E": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": { + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" + }, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + } + } + } + }, + "Outputs": { + "testapigatewaykinesisadditionalrequesttemplatesRestApiEndpointE192AA9B": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiCCD096E7" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaykinesisadditionalrequesttemplatesRestApiDeploymentStageprodD274025B" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.ts new file mode 100644 index 000000000..926d84165 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.custom-integration-responses.ts @@ -0,0 +1,36 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from 'aws-cdk-lib'; +import { ApiGatewayToKinesisStreams } from '../lib'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-kinesis'; + +new ApiGatewayToKinesisStreams(stack, 'test-apigateway-kinesis-additional-request-templates', { + putRecordIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md index 400c9aec7..1e0744b0c 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md @@ -62,16 +62,18 @@ new ApiGatewayToSqs(this, "ApiGatewayToSqsPattern", new ApiGatewayToSqsProps.Bui |queueProps?|[`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.QueueProps.html)|Optional user-provided props to override the default props for the queue.| |deployDeadLetterQueue?|`boolean`|Whether to deploy a secondary queue to be used as a dead letter queue. Defaults to `true`.| |maxReceiveCount|`number`|The number of times a message can be unsuccessfully dequeued before being moved to the dead-letter queue.| - |allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the queue (i.e. sqs:SendMessage).| |createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type, if the `allowCreateOperation` property is set to true.| |additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| +|createIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true.| |allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for GET HTTP operations on the queue (i.e. sqs:ReceiveMessage).| |readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type, if the `allowReadOperation` property is set to true.| |additionalReadRequestTemplates?|`{ [contentType: string]: string; }`|Optional Read Request Templates for content-types other than `application/json`. Use the `readRequestTemplate` property to set the request template for the `application/json` content-type.| +|readIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true.| |allowDeleteOperation?|`boolean`|Whether to deploy an API Gateway Method for HTTP DELETE operations on the queue (i.e. sqs:DeleteMessage).| |deleteRequestTemplate?|`string`|API Gateway Request Template for THE delete method for the default `application/json` content-type, if the `allowDeleteOperation` property is set to true.| |additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type.| +|deleteIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| |enableEncryptionWithCustomerManagedKey?|`boolean`|If no key is provided, this flag determines whether the queue is encrypted with a new CMK or an AWS managed key. This flag is ignored if any of the following are defined: queueProps.encryptionMasterKey, encryptionKey or encryptionKeyProps.| |encryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html)|An optional, imported encryption key to encrypt the SQS Queue with.| diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts index 4b51d9f22..16652bc55 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts @@ -82,6 +82,12 @@ export interface ApiGatewayToSqsProps { * @default - None */ readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly createIntegrationResponses?: api.IntegrationResponse[]; /** * Whether to deploy an API Gateway Method for GET HTTP operations on the queue (i.e. sqs:ReceiveMessage). * @@ -102,6 +108,12 @@ export interface ApiGatewayToSqsProps { * @default - None */ readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly readIntegrationResponses?: api.IntegrationResponse[]; /** * Whether to deploy an API Gateway Method for HTTP DELETE operations on the queue (i.e. sqs:DeleteMessage). * @@ -122,6 +134,12 @@ export interface ApiGatewayToSqsProps { * @default - None */ readonly additionalDeleteRequestTemplates?: { [contentType: string]: string; }; + /** + * Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true. + * + * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] + */ + readonly deleteIntegrationResponses?: api.IntegrationResponse[]; /** * User provided props to override the default props for the CloudWatchLogs LogGroup. * @@ -214,7 +232,8 @@ export class ApiGatewayToSqs extends Construct { apiResource: this.apiGateway.root, requestTemplate: createRequestTemplate, additionalRequestTemplates: props.additionalCreateRequestTemplates, - contentType: "'application/x-www-form-urlencoded'" + contentType: "'application/x-www-form-urlencoded'", + integrationResponses: props.createIntegrationResponses }); } @@ -230,7 +249,8 @@ export class ApiGatewayToSqs extends Construct { apiResource: this.apiGateway.root, requestTemplate: readRequestTemplate, additionalRequestTemplates: props.additionalReadRequestTemplates, - contentType: "'application/x-www-form-urlencoded'" + contentType: "'application/x-www-form-urlencoded'", + integrationResponses: props.readIntegrationResponses }); } @@ -246,7 +266,8 @@ export class ApiGatewayToSqs extends Construct { apiResource: apiGatewayResource, requestTemplate: deleteRequestTemplate, additionalRequestTemplates: props.additionalDeleteRequestTemplates, - contentType: "'application/x-www-form-urlencoded'" + contentType: "'application/x-www-form-urlencoded'", + integrationResponses: props.deleteIntegrationResponses }); } } diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts index 44e4a9fb2..dc57a1f6f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts @@ -345,4 +345,154 @@ test('Construct accepts additional delete request templates', () => { } } }); +}); + +test('Construct uses default integration responses', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowCreateOperation: true, + allowReadOperation: true, + allowDeleteOperation: true, + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); +}); + +test('Construct uses custom createIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowCreateOperation: true, + createIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); +}); + +test('Construct uses custom readIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowCreateOperation: true, + readIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); +}); + +test('Construct uses custom deleteIntegrationResponses property', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowDeleteOperation: true, + deleteIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + StatusCode: '200' + } + ] + } + }); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.expected.json new file mode 100644 index 000000000..1ba7f0556 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.expected.json @@ -0,0 +1,545 @@ +{ + "Description": "Integration Test for aws-apigateway-sqs", + "Resources": { + "testapigatewaysqsintegrationresponsesdeadLetterQueue22D4FBC4": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testapigatewaysqsintegrationresponsesdeadLetterQueuePolicyC05CEDC7": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage", + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:RemovePermission", + "sqs:AddPermission", + "sqs:SetQueueAttributes" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesdeadLetterQueue22D4FBC4", + "Arn" + ] + }, + "Sid": "QueueOwnerOnlyAccess" + }, + { + "Action": "SQS:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesdeadLetterQueue22D4FBC4", + "Arn" + ] + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "testapigatewaysqsintegrationresponsesdeadLetterQueue22D4FBC4" + } + ] + } + }, + "testapigatewaysqsintegrationresponsesqueueF9FA04EF": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs", + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesdeadLetterQueue22D4FBC4", + "Arn" + ] + }, + "maxReceiveCount": 15 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testapigatewaysqsintegrationresponsesqueuePolicy22D3DE99": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage", + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:RemovePermission", + "sqs:AddPermission", + "sqs:SetQueueAttributes" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesqueueF9FA04EF", + "Arn" + ] + }, + "Sid": "QueueOwnerOnlyAccess" + }, + { + "Action": "SQS:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesqueueF9FA04EF", + "Arn" + ] + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "testapigatewaysqsintegrationresponsesqueueF9FA04EF" + } + ] + } + }, + "testapigatewaysqsintegrationresponsesApiAccessLogGroup12574495": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testapigatewaysqsintegrationresponsesRestApi3BE7E402": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaysqsintegrationresponsesRestApiDeployment9749223507b4327bc896117bc13d1c395d6fa5e7": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaysqsintegrationresponsesRestApiGETD105D1F1", + "testapigatewaysqsintegrationresponsesRestApimessageC29A9FEB" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaysqsintegrationresponsesRestApiDeploymentStageprod07200D02": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesApiAccessLogGroup12574495", + "Arn" + ] + }, + "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" + }, + "DeploymentId": { + "Ref": "testapigatewaysqsintegrationresponsesRestApiDeployment9749223507b4327bc896117bc13d1c395d6fa5e7" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod", + "TracingEnabled": true + } + }, + "testapigatewaysqsintegrationresponsesRestApimessageC29A9FEB": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesRestApi3BE7E402", + "RootResourceId" + ] + }, + "PathPart": "message", + "RestApiId": { + "Ref": "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + } + } + }, + "testapigatewaysqsintegrationresponsesRestApiGETD105D1F1": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesRestApi3BE7E402", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesapigatewayrole6FC88B17", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "text/html": "OK" + }, + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=ReceiveMessage" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesqueueF9FA04EF", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsintegrationresponsesRestApiUsagePlan4172DBF8": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + }, + "Stage": { + "Ref": "testapigatewaysqsintegrationresponsesRestApiDeploymentStageprod07200D02" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaysqsintegrationresponsesLambdaRestApiCloudWatchRoleC95A1D3B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaysqsintegrationresponsesLambdaRestApiAccountEFC75D59": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesLambdaRestApiCloudWatchRoleC95A1D3B", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + ] + }, + "testapigatewaysqsintegrationresponsesapigatewayrole6FC88B17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaysqsintegrationresponsesapigatewayroleDefaultPolicyB4D5EFE9": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaysqsintegrationresponsesqueueF9FA04EF", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaysqsintegrationresponsesapigatewayroleDefaultPolicyB4D5EFE9", + "Roles": [ + { + "Ref": "testapigatewaysqsintegrationresponsesapigatewayrole6FC88B17" + } + ] + } + } + }, + "Outputs": { + "testapigatewaysqsintegrationresponsesRestApiEndpointEBC579CE": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaysqsintegrationresponsesRestApi3BE7E402" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaysqsintegrationresponsesRestApiDeploymentStageprod07200D02" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.ts new file mode 100644 index 000000000..653d14b38 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/integ.custom-integration-responses.ts @@ -0,0 +1,37 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "aws-cdk-lib"; +import { ApiGatewayToSqs } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test for aws-apigateway-sqs'; + +new ApiGatewayToSqs(stack, 'test-api-gateway-sqs-integration-responses', { + allowReadOperation: true, + readIntegrationResponses: [ + { + statusCode: '200', + responseTemplates: { + 'text/html': 'OK' + } + } + ] +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts index 74c77c39f..8076b45cb 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts @@ -241,6 +241,7 @@ export interface AddProxyMethodToApiResourceInputParams { readonly apiGatewayRole: IRole, readonly requestTemplate: string, readonly additionalRequestTemplates?: { [contentType: string]: string; }, + readonly integrationResponses?: cdk.aws_apigateway.IntegrationResponse[], readonly contentType?: string, readonly requestValidator?: api.IRequestValidator, readonly requestModel?: { [contentType: string]: api.IModel; }, @@ -254,6 +255,20 @@ export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceI ...params.additionalRequestTemplates }; + // Use user-provided integration responses, otherwise fallback to the default ones we provide. + const integrationResponses = params.integrationResponses ?? [ + { + statusCode: "200" + }, + { + statusCode: "500", + responseTemplates: { + "text/html": "Error" + }, + selectionPattern: "500" + } + ]; + let baseProps: api.AwsIntegrationProps = { service: params.service, integrationHttpMethod: "POST", @@ -264,18 +279,7 @@ export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceI "integration.request.header.Content-Type": params.contentType ? params.contentType : "'application/json'" }, requestTemplates, - integrationResponses: [ - { - statusCode: "200" - }, - { - statusCode: "500", - responseTemplates: { - "text/html": "Error" - }, - selectionPattern: "500" - } - ] + integrationResponses } }; diff --git a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts index e5cbbbb91..e60e7db28 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts @@ -688,4 +688,91 @@ test('Test addMethodToApiResource with action', () => { } } }); +}); + +test('Default integration responses are used on addMethodToApiResource method', () => { + const stack = new Stack(); + const [ restApi ] = defaults.GlobalRestApi(stack); + + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = restApi.root.addResource('api-gateway-resource'); + + // Add Method + defaults.addProxyMethodToApiResource({ + action: 'Query', + service: 'dynamodb', + apiResource: apiGatewayResource, + apiGatewayRole, + apiMethod: 'GET', + requestTemplate: '{}', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + IntegrationResponses: [ + { + StatusCode: '200' + }, + { + ResponseTemplates: { + 'text/html': 'Error' + }, + SelectionPattern: '500', + StatusCode: '500' + } + ] + } + }); +}); + +test('Can override integration responses on addMethodToApiResource method', () => { + const stack = new Stack(); + const [ restApi ] = defaults.GlobalRestApi(stack); + + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = restApi.root.addResource('api-gateway-resource'); + + // Add Method + defaults.addProxyMethodToApiResource({ + action: 'Query', + service: 'dynamodb', + apiResource: apiGatewayResource, + apiGatewayRole, + apiMethod: 'GET', + requestTemplate: '{}', + integrationResponses: [ + { + statusCode: "200", + responseTemplates: { + "text/html": "OK" + } + } + ] + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + IntegrationResponses: [ + { + ResponseTemplates: { + 'text/html': 'OK' + }, + SelectionPattern: '200', + StatusCode: '200' + } + ], + } + }); }); \ No newline at end of file From e284d2f3284fb85628d5cd27e50086a18f0e8b7c Mon Sep 17 00:00:00 2001 From: George Bearden Date: Wed, 1 Feb 2023 17:38:50 -0500 Subject: [PATCH 7/9] Fix failing unit test in apigateway-helper. --- .../core/test/apigateway-helper.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts index e60e7db28..553514428 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts @@ -769,7 +769,6 @@ test('Can override integration responses on addMethodToApiResource method', () = ResponseTemplates: { 'text/html': 'OK' }, - SelectionPattern: '200', StatusCode: '200' } ], From 7fa9ae96152257f0801df84cf3cc09903bbe6ce7 Mon Sep 17 00:00:00 2001 From: George Bearden Date: Thu, 2 Feb 2023 17:20:31 -0500 Subject: [PATCH 8/9] Address PR feedback. --- .../aws-apigateway-dynamodb/README.md | 27 ++- .../aws-apigateway-dynamodb/lib/index.ts | 45 ++++- .../test/apigateway-dynamodb.test.ts | 180 ++++++++++++++++++ ...custom-integration-responses.expected.json | 12 +- .../integ.custom-integration-responses.ts | 3 +- .../lib/index.ts | 22 +-- ...t.ts => apigateway-kinesisstreams.test.ts} | 0 .../aws-apigateway-sqs/README.md | 23 ++- .../aws-apigateway-sqs/lib/index.ts | 50 +++-- .../test/apigateway-sqs.test.ts | 140 ++++++++++++-- .../core/lib/apigateway-defaults.ts | 19 ++ .../core/lib/apigateway-helper.ts | 13 +- .../core/test/apigateway-helper.test.ts | 2 +- 13 files changed, 455 insertions(+), 81 deletions(-) rename source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/{apigateway-kinesis.test.ts => apigateway-kinesisstreams.test.ts} (100%) diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md index 623727cdd..416a840ec 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md @@ -67,20 +67,20 @@ new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatew |apiGatewayProps?|[`api.RestApiProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| |allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the DynamoDB table (i.e. dynamodb:PutItem).| |createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type. This property is required if the `allowCreateOperation` property is set to true.| -|additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| -|createIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true.| +|additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type. This property can only be specified if the `allowCreateOperation` property is set to true.| +|createIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the create method. This property can only be specified if the `allowCreateOperation` property is set to true.| |allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for GET HTTP operations on DynamoDB table (i.e. dynamodb:Query).| -|readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type, if the `allowReadOperation` property is set to true. The default template only supports a partition key and not partition + sort keys.| +|readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type. The default template only supports a partition key and not partition + sort keys.| |additionalReadRequestTemplates?|`{ [contentType: string]: string; }`|Optional Read Request Templates for content-types other than `application/json`. Use the `readRequestTemplate` property to set the request template for the `application/json` content-type.| -|readIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true.| +|readIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the read method.| |allowUpdateOperation?|`boolean`|Whether to deploy API Gateway Method for PUT HTTP operations on DynamoDB table (i.e. dynamodb:UpdateItem).| -|updateRequestTemplate?|`string`|API Gateway Request Template for the update method, required if the `allowUpdateOperation` property is set to true.| -|additionalUpdateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Update Request Templates for content-types other than `application/json`. Use the `updateRequestTemplate` property to set the request template for the `application/json` content-type.| -|updateIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the update method, if the `allowUpdateOperation` property is set to true.| +|updateRequestTemplate?|`string`|API Gateway Request Template for the update method. This property is required if the `allowUpdateOperation` property is set to true.| +|additionalUpdateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Update Request Templates for content-types other than `application/json`. Use the `updateRequestTemplate` property to set the request template for the `application/json` content-type. This property can only be specified if the `allowUpdateOperation` property is set to true.| +|updateIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the update method. This property can only be specified if the `allowUpdateOperation` property is set to true.| |allowDeleteOperation?|`boolean`|Whether to deploy API Gateway Method for DELETE HTTP operations on DynamoDB table (i.e. dynamodb:DeleteItem).| -|deleteRequestTemplate?|`string`|API Gateway Request Template for the delete method for the default `application/json` content-type, if the `allowDeleteOperation` property is set to true.| -|additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type.| -|deleteIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true.| +|deleteRequestTemplate?|`string`|API Gateway Request Template for the delete method for the default `application/json` content-type. | +|additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type. This property can only be specified if the `allowDeleteOperation` property is set to true.| +|deleteIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the delete method. This property can only be specified if the `allowDeleteOperation` property is set to true.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| ## Pattern Properties @@ -93,6 +93,13 @@ new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatew |apiGatewayCloudWatchRole?|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of the iam.Role created by the construct for API Gateway for CloudWatch access.| |apiGatewayLogGroup|[`logs.LogGroup`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroup.html)|Returns an instance of the LogGroup created by the construct for API Gateway access logging to CloudWatch.| +# API Gateway Request/Response Template Properties Overview +There are sets of properties corresponding to the various API operations this construct supports (CREATE/READ/UPDATE/DELETE). Each API operation has a property to enable it, followed by a set of properties used to specify request templates and integration responses. Taking the **CREATE** operation as an example: +* The API method is enabled by setting the `allowCreateOperation` property to true. +* Once set, the request template for the `application/json` content-type can be set using the `createRequestTemplate` property. +* Additional request templates for content-types other than `application/json` can be specified in the `additionalCreateRequestTemplates` property. +* Non-default integration responses can be specified in the `createIntegrationResponses` property. + ## Default settings Out of the box implementation of the Construct without any override will set the following defaults: diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts index 41983a818..7ff874700 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts @@ -59,12 +59,14 @@ export interface ApiGatewayToDynamoDBProps { /** * Optional Create Request Templates for content-types other than `application/json`. * Use the `createRequestTemplate` property to set the request template for the `application/json` content-type. + * This property can only be specified if the `allowCreateOperation` property is set to true. * * @default - None */ readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the create method. + * This property can only be specified if the `allowCreateOperation` property is set to true. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -76,8 +78,7 @@ export interface ApiGatewayToDynamoDBProps { */ readonly allowReadOperation?: boolean; /** - * API Gateway Request Template for the read method for the default `application/json` content-type, - * if the `allowReadOperation` property is set to true. + * API Gateway Request Template for the read method for the default `application/json` content-type. * * The default template only supports a partition key and not partition + sort keys. * @@ -100,7 +101,7 @@ export interface ApiGatewayToDynamoDBProps { */ readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the read method. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -112,7 +113,8 @@ export interface ApiGatewayToDynamoDBProps { */ readonly allowUpdateOperation?: boolean; /** - * API Gateway Request Template for the update method, required if the `allowUpdateOperation` property is set to true. + * API Gateway Request Template for the update method. + * This property is required if the `allowUpdateOperation` property is set to true. * * @default - None */ @@ -120,12 +122,14 @@ export interface ApiGatewayToDynamoDBProps { /** * Optional Update Request Templates for content-types other than `application/json`. * Use the `updateRequestTemplate` property to set the request template for the `application/json` content-type. + * This property can only be specified if the `allowUpdateOperation` property is set to true. * * @default - None */ readonly additionalUpdateRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the update method, if the `allowUpdateOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the update method. + * This property can only be specified if the `allowUpdateOperation` property is set to true. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -137,8 +141,8 @@ export interface ApiGatewayToDynamoDBProps { */ readonly allowDeleteOperation?: boolean; /** - * API Gateway Request Template for the delete method for the default `application/json` content-type, - * if the `allowDeleteOperation` property is set to true. + * API Gateway Request Template for the delete method for the default `application/json` content-type. + * This property can only be specified if the `allowDeleteOperation` property is set to true. * * @default - `{ \ * "TableName": "DYNAMODB_TABLE_NAME", \ @@ -154,12 +158,14 @@ export interface ApiGatewayToDynamoDBProps { /** * Optional Delete request templates for content-types other than `application/json`. * Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type. + * This property can only be specified if the `allowDeleteOperation` property is set to true. * * @default - None */ readonly additionalDeleteRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the delete method. + * This property can only be specified if the `allowDeleteOperation` property is set to true. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -193,6 +199,27 @@ export class ApiGatewayToDynamoDB extends Construct { super(scope, id); defaults.CheckProps(props); + if ((props.createRequestTemplate || props.additionalCreateRequestTemplates || props.createIntegrationResponses) + && props.allowCreateOperation !== true) { + throw new Error(`The 'allowCreateOperation' property must be set to true when setting any of the following: ` + + `'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'`); + } + if ((props.readRequestTemplate || props.additionalReadRequestTemplates || props.readIntegrationResponses) + && props.allowReadOperation === false) { + throw new Error(`The 'allowReadOperation' property must be set to true or undefined when setting any of the following: ` + + `'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'`); + } + if ((props.updateRequestTemplate || props.additionalUpdateRequestTemplates || props.updateIntegrationResponses) + && props.allowUpdateOperation !== true) { + throw new Error(`The 'allowUpdateOperation' property must be set to true when setting any of the following: ` + + `'updateRequestTemplate', 'additionalUpdateRequestTemplates', 'updateIntegrationResponses'`); + } + if ((props.deleteRequestTemplate || props.additionalDeleteRequestTemplates || props.deleteIntegrationResponses) + && props.allowDeleteOperation !== true) { + throw new Error(`The 'allowDeleteOperation' property must be set to true when setting any of the following: ` + + `'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'`); + } + // Set the default props for DynamoDB table const dynamoTableProps: dynamodb.TableProps = defaults.consolidateProps(defaults.DefaultTableProps, props.dynamoTableProps); let partitionKeyName = dynamoTableProps.partitionKey.name; diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts index 804e014f6..a20efc9ed 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts @@ -252,6 +252,74 @@ test("check setting allowReadOperation=false for dynamodb", () => { expect(stack2).toCountResources("AWS::ApiGateway::Method", 1); }); +test('Construct can override default create request template type', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowCreateOperation: true, + createRequestTemplate: 'ok', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + Integration: { + RequestTemplates: { + 'application/json': 'ok' + } + } + }); +}); + +test('Construct can override default read request template type', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowReadOperation: true, + readRequestTemplate: 'ok', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + RequestTemplates: { + 'application/json': 'ok' + } + } + }); +}); + +test('Construct can override default update request template type', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowUpdateOperation: true, + updateRequestTemplate: 'ok', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'PUT', + Integration: { + RequestTemplates: { + 'application/json': 'ok' + } + } + }); +}); + +test('Construct can override default delete request template type', () => { + const stack = new Stack(); + new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowDeleteOperation: true, + deleteRequestTemplate: 'ok', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + RequestTemplates: { + 'application/json': 'ok' + } + } + }); +}); + test('Construct accepts additional create request templates', () => { const stack = new Stack(); new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { @@ -276,6 +344,7 @@ test('Construct accepts additional create request templates', () => { test('Construct accepts additional read request templates', () => { const stack = new Stack(); new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowReadOperation: true, additionalReadRequestTemplates: { 'text/plain': 'Hello' } @@ -531,4 +600,115 @@ test('Construct uses custom deleteIntegrationResponses property', () => { ] } }); +}); + +test('Construct throws error when createRequestTemplate is set and allowCreateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + createRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowCreateOperation' property must be set to true when setting any of the following: 'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'/); +}); + +test('Construct throws error when additionalCreateRequestTemplates is set and allowCreateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + additionalCreateRequestTemplates: {} + }); + + expect(app).toThrowError(/The 'allowCreateOperation' property must be set to true when setting any of the following: 'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'/); +}); + +test('Construct throws error when createIntegrationResponses is set and allowCreateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + createIntegrationResponses: [] + }); + + expect(app).toThrowError(/The 'allowCreateOperation' property must be set to true when setting any of the following: 'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'/); +}); + +test('Construct throws error when readRequestTemplate is set and allowReadOperation is false', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowReadOperation: false, + readRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowReadOperation' property must be set to true or undefined when setting any of the following: 'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'/); +}); + +test('Construct throws error when additionalReadRequestTemplates is set and allowReadOperation is false', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowReadOperation: false, + additionalReadRequestTemplates: {}, + }); + + expect(app).toThrowError(/The 'allowReadOperation' property must be set to true or undefined when setting any of the following: 'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'/); +}); + +test('Construct throws error when readIntegrationResponses is set and allowReadOperation is false', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + allowReadOperation: false, + readIntegrationResponses: [], + }); + + expect(app).toThrowError(/The 'allowReadOperation' property must be set to true or undefined when setting any of the following: 'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'/); +}); + +test('Construct throws error when updateRequestTemplate is set and allowUpdateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + updateRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowUpdateOperation' property must be set to true when setting any of the following: 'updateRequestTemplate', 'additionalUpdateRequestTemplates', 'updateIntegrationResponses'/); +}); + +test('Construct throws error when additionalUpdateRequestTemplates is set and allowUpdateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + additionalUpdateRequestTemplates: {} + }); + + expect(app).toThrowError(/The 'allowUpdateOperation' property must be set to true when setting any of the following: 'updateRequestTemplate', 'additionalUpdateRequestTemplates', 'updateIntegrationResponses'/); +}); + +test('Construct throws error when updateIntegrationResponses is set and allowUpdateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + updateIntegrationResponses: [] + }); + + expect(app).toThrowError(/The 'allowUpdateOperation' property must be set to true when setting any of the following: 'updateRequestTemplate', 'additionalUpdateRequestTemplates', 'updateIntegrationResponses'/); +}); + +test('Construct throws error when deleteRequestTemplate is set and allowDeleteOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + deleteRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowDeleteOperation' property must be set to true when setting any of the following: 'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'/); +}); + +test('Construct throws error when additionalDeleteRequestTemplates is set and allowDeleteOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + additionalDeleteRequestTemplates: {} + }); + + expect(app).toThrowError(/The 'allowDeleteOperation' property must be set to true when setting any of the following: 'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'/); +}); + +test('Construct throws error when deleteIntegrationResponses is set and allowDeleteOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToDynamoDB(stack, 'api-gateway-dynamodb', { + deleteIntegrationResponses: [] + }); + + expect(app).toThrowError(/The 'allowDeleteOperation' property must be set to true when setting any of the following: 'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'/); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json index 8fa2766ed..09e5898b1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.expected.json @@ -57,7 +57,7 @@ "Name": "RestApi" } }, - "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C477ebd2134b237add637a36f3483536b1fd": { + "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47761b8949a9247c12493c939f05e630c9d": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -96,7 +96,7 @@ "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" }, "DeploymentId": { - "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C477ebd2134b237add637a36f3483536b1fd" + "Ref": "testapigatewaydynamodbadditionalrequesttemplatesRestApiDeployment0AE7C47761b8949a9247c12493c939f05e630c9d" }, "MethodSettings": [ { @@ -145,11 +145,15 @@ }, "IntegrationHttpMethod": "POST", "IntegrationResponses": [ + { + "StatusCode": "200" + }, { "ResponseTemplates": { - "text/html": "OK" + "text/html": "Error" }, - "StatusCode": "200" + "SelectionPattern": "500", + "StatusCode": "500" } ], "PassthroughBehavior": "NEVER", diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts index ad28eb6e1..5d0088cb2 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/test/integ.custom-integration-responses.ts @@ -36,7 +36,8 @@ const existingTableObj = new dynamodb.Table(stack, 'existing-table', { new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-additional-request-templates', { existingTableObj, - readIntegrationResponses: [ + allowCreateOperation: true, + createIntegrationResponses: [ { statusCode: '200', responseTemplates: { diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts index 44bb47a02..8257676c6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/lib/index.ts @@ -169,11 +169,11 @@ export class ApiGatewayToKinesisStreams extends Construct { apiGatewayRole: this.apiGatewayRole, apiMethod: 'POST', apiResource: putRecordResource, - requestTemplate: this.getPutRecordTemplate(props.putRecordRequestTemplate), - additionalRequestTemplates: this.getAdditionalPutRecordTemplates(props.additionalPutRecordRequestTemplates), + requestTemplate: this.buildPutRecordTemplate(props.putRecordRequestTemplate), + additionalRequestTemplates: this.buildAdditionalPutRecordTemplates(props.additionalPutRecordRequestTemplates), contentType: "'x-amz-json-1.1'", requestValidator, - requestModel: { 'application/json': this.getPutRecordModel(props.putRecordRequestModel) }, + requestModel: { 'application/json': this.buildPutRecordModel(props.putRecordRequestModel) }, integrationResponses: props.putRecordIntegrationResponses }); @@ -185,11 +185,11 @@ export class ApiGatewayToKinesisStreams extends Construct { apiGatewayRole: this.apiGatewayRole, apiMethod: 'POST', apiResource: putRecordsResource, - requestTemplate: this.getPutRecordsTemplate(props.putRecordsRequestTemplate), - additionalRequestTemplates: this.getAdditionalPutRecordTemplates(props.additionalPutRecordsRequestTemplates), + requestTemplate: this.buildPutRecordsTemplate(props.putRecordsRequestTemplate), + additionalRequestTemplates: this.buildAdditionalPutRecordTemplates(props.additionalPutRecordsRequestTemplates), contentType: "'x-amz-json-1.1'", requestValidator, - requestModel: { 'application/json': this.getPutRecordsModel(props.putRecordsRequestModel) }, + requestModel: { 'application/json': this.buildPutRecordsModel(props.putRecordsRequestModel) }, integrationResponses: props.putRecordsIntegrationResponses }); @@ -205,7 +205,7 @@ export class ApiGatewayToKinesisStreams extends Construct { * * @param templates The additional request templates to transform. */ - private getAdditionalPutRecordTemplates(templates?: { [contentType: string]: string; }): { [contentType: string]: string; } { + private buildAdditionalPutRecordTemplates(templates?: { [contentType: string]: string; }): { [contentType: string]: string; } { const transformedTemplates: { [contentType: string]: string; } = {}; @@ -218,7 +218,7 @@ export class ApiGatewayToKinesisStreams extends Construct { return transformedTemplates; } - private getPutRecordTemplate(putRecordTemplate?: string): string { + private buildPutRecordTemplate(putRecordTemplate?: string): string { if (putRecordTemplate !== undefined) { return putRecordTemplate.replace("${StreamName}", this.kinesisStream.streamName); } @@ -226,7 +226,7 @@ export class ApiGatewayToKinesisStreams extends Construct { return `{ "StreamName": "${this.kinesisStream.streamName}", "Data": "$util.base64Encode($input.json('$.data'))", "PartitionKey": "$input.path('$.partitionKey')" }`; } - private getPutRecordModel(putRecordModel?: api.ModelOptions): api.IModel { + private buildPutRecordModel(putRecordModel?: api.ModelOptions): api.IModel { let modelProps: api.ModelOptions; if (putRecordModel !== undefined) { @@ -252,7 +252,7 @@ export class ApiGatewayToKinesisStreams extends Construct { return this.apiGateway.addModel('PutRecordModel', modelProps); } - private getPutRecordsTemplate(putRecordsTemplate?: string): string { + private buildPutRecordsTemplate(putRecordsTemplate?: string): string { if (putRecordsTemplate !== undefined) { return putRecordsTemplate.replace("${StreamName}", this.kinesisStream.streamName); } @@ -260,7 +260,7 @@ export class ApiGatewayToKinesisStreams extends Construct { return `{ "StreamName": "${this.kinesisStream.streamName}", "Records": [ #foreach($elem in $input.path('$.records')) { "Data": "$util.base64Encode($elem.data)", "PartitionKey": "$elem.partitionKey"}#if($foreach.hasNext),#end #end ] }`; } - private getPutRecordsModel(putRecordsModel?: api.ModelOptions): api.IModel { + private buildPutRecordsModel(putRecordsModel?: api.ModelOptions): api.IModel { let modelProps: api.ModelOptions; if (putRecordsModel !== undefined) { diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesisstreams.test.ts similarity index 100% rename from source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesis.test.ts rename to source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/apigateway-kinesisstreams.test.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md index 1e0744b0c..7896f7ec8 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md @@ -63,17 +63,17 @@ new ApiGatewayToSqs(this, "ApiGatewayToSqsPattern", new ApiGatewayToSqsProps.Bui |deployDeadLetterQueue?|`boolean`|Whether to deploy a secondary queue to be used as a dead letter queue. Defaults to `true`.| |maxReceiveCount|`number`|The number of times a message can be unsuccessfully dequeued before being moved to the dead-letter queue.| |allowCreateOperation?|`boolean`|Whether to deploy an API Gateway Method for POST HTTP operations on the queue (i.e. sqs:SendMessage).| -|createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type, if the `allowCreateOperation` property is set to true.| -|additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type.| -|createIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true.| +|createRequestTemplate?|`string`|API Gateway Request Template for the create method for the default `application/json` content-type. This property is required if the `allowCreateOperation` property is set to true.| +|additionalCreateRequestTemplates?|`{ [contentType: string]: string; }`|Optional Create Request Templates for content-types other than `application/json`. Use the `createRequestTemplate` property to set the request template for the `application/json` content-type. This property can only be specified if the `allowCreateOperation` property is set to true.| +|createIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the create method. This property can only be specified if the `allowCreateOperation` property is set to true.| |allowReadOperation?|`boolean`|Whether to deploy an API Gateway Method for GET HTTP operations on the queue (i.e. sqs:ReceiveMessage).| -|readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type, if the `allowReadOperation` property is set to true.| +|readRequestTemplate?|`string`|API Gateway Request Template for the read method for the default `application/json` content-type.| |additionalReadRequestTemplates?|`{ [contentType: string]: string; }`|Optional Read Request Templates for content-types other than `application/json`. Use the `readRequestTemplate` property to set the request template for the `application/json` content-type.| -|readIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true.| +|readIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the read method.| |allowDeleteOperation?|`boolean`|Whether to deploy an API Gateway Method for HTTP DELETE operations on the queue (i.e. sqs:DeleteMessage).| -|deleteRequestTemplate?|`string`|API Gateway Request Template for THE delete method for the default `application/json` content-type, if the `allowDeleteOperation` property is set to true.| -|additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type.| -|deleteIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true.| +|deleteRequestTemplate?|`string`|API Gateway Request Template for THE delete method for the default `application/json` content-type. This property can only be specified if the `allowDeleteOperation` property is set to true.| +|additionalDeleteRequestTemplates?|`{ [contentType: string]: string; }`|Optional Delete request templates for content-types other than `application/json`. Use the `deleteRequestTemplate` property to set the request template for the `application/json` content-type. This property can only be specified if the `allowDeleteOperation` property is set to true.| +|deleteIntegrationResponses?|[`api.IntegrationResponses[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.IntegrationResponse.html)|Optional, custom API Gateway Integration Response for the delete method. This property can only be specified if the `allowDeleteOperation` property is set to true.| |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| |enableEncryptionWithCustomerManagedKey?|`boolean`|If no key is provided, this flag determines whether the queue is encrypted with a new CMK or an AWS managed key. This flag is ignored if any of the following are defined: queueProps.encryptionMasterKey, encryptionKey or encryptionKeyProps.| |encryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html)|An optional, imported encryption key to encrypt the SQS Queue with.| @@ -98,6 +98,13 @@ new ApiGatewayToSqs(this, "ApiGatewayToSqsPattern", new ApiGatewayToSqsProps.Bui |POST|`/`| `{ "data": "Hello World!" }` |`sqs::SendMessage`|Delivers a message to the queue.| |DELETE|`/message?receiptHandle=[value]`||`sqs::DeleteMessage`|Deletes a specified message from the queue| +# API Gateway Request/Response Template Properties Overview +There are sets of properties corresponding to the various API operations this construct supports (CREATE/READ/UPDATE/DELETE). Each API operation has a property to enable it, followed by a set of properties used to specify request templates and integration responses. Taking the **CREATE** operation as an example: +* The API method is enabled by setting the `allowCreateOperation` property to true. +* Once set, the request template for the `application/json` content-type can be set using the `createRequestTemplate` property. +* Additional request templates for content-types other than `application/json` can be specified in the `additionalCreateRequestTemplates` property. +* Non-default integration responses can be specified in the `createIntegrationResponses` property. + ## Default settings Out of the box implementation of the Construct without any override will set the following defaults: diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts index 16652bc55..164f2d44e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/lib/index.ts @@ -69,8 +69,8 @@ export interface ApiGatewayToSqsProps { */ readonly allowCreateOperation?: boolean; /** - * API Gateway Request Template for the create method for the default `application/json` content-type, - * if the `allowCreateOperation` property is set to true. + * API Gateway Request Template for the create method for the default `application/json` content-type. + * This property can only be specified if the `allowCreateOperation` property is set to true. * * @default - 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")' */ @@ -83,7 +83,8 @@ export interface ApiGatewayToSqsProps { */ readonly additionalCreateRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the create method, if the `allowCreateOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the create method. + * This property can only be specified if the `allowCreateOperation` property is set to true. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -91,12 +92,12 @@ export interface ApiGatewayToSqsProps { /** * Whether to deploy an API Gateway Method for GET HTTP operations on the queue (i.e. sqs:ReceiveMessage). * - * @default - false + * @default - true */ readonly allowReadOperation?: boolean; /** - * API Gateway Request Template for the read method for the default `application/json` content-type, - * if the `allowReadOperation` property is set to true. + * API Gateway Request Template for the read method for the default `application/json` content-type. + * This property can only be specified if the `allowReadOperation` property is not set to false. * * @default - "Action=ReceiveMessage" */ @@ -104,12 +105,14 @@ export interface ApiGatewayToSqsProps { /** * Optional Read Request Templates for content-types other than `application/json`. * Use the `readRequestTemplate` property to set the request template for the `application/json` content-type. + * This property can only be specified if the `allowReadOperation` property is not set to false. * * @default - None */ readonly additionalReadRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the read method, if the `allowReadOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the read method. + * This property can only be specified if the `allowReadOperation` property is not set to false. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -121,8 +124,8 @@ export interface ApiGatewayToSqsProps { */ readonly allowDeleteOperation?: boolean; /** - * API Gateway Request Template for THE delete method for the default `application/json` content-type, - * if the `allowDeleteOperation` property is set to true. + * API Gateway Request Template for THE delete method for the default `application/json` content-type. + * This property can only be specified if the `allowDeleteOperation` property is set to true. * * @default - "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))" */ @@ -135,7 +138,8 @@ export interface ApiGatewayToSqsProps { */ readonly additionalDeleteRequestTemplates?: { [contentType: string]: string; }; /** - * Optional, custom API Gateway Integration Response for the delete method, if the `allowDeleteOperation` property is set to true. + * Optional, custom API Gateway Integration Response for the delete method. + * This property can only be specified if the `allowDeleteOperation` property is set to true. * * @default - [{statusCode:"200"},{statusCode:"500",responseTemplates:{"text/html":"Error"},selectionPattern:"500"}] */ @@ -178,6 +182,10 @@ export class ApiGatewayToSqs extends Construct { public readonly sqsQueue: sqs.Queue; public readonly deadLetterQueue?: sqs.DeadLetterQueue; + private readonly defaultCreateRequestTemplate = 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")'; + private readonly defaultReadRequestTemplate = 'Action=ReceiveMessage'; + private readonly defaultDeleteRequestTemplate = "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))"; + /** * @summary Constructs a new instance of the ApiGatewayToSqs class. * @param {cdk.App} scope - represents the scope for all the resources. @@ -190,6 +198,22 @@ export class ApiGatewayToSqs extends Construct { super(scope, id); defaults.CheckProps(props); + if ((props.createRequestTemplate || props.additionalCreateRequestTemplates || props.createIntegrationResponses) + && props.allowCreateOperation !== true) { + throw new Error(`The 'allowCreateOperation' property must be set to true when setting any of the following: ` + + `'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'`); + } + if ((props.readRequestTemplate || props.additionalReadRequestTemplates || props.readIntegrationResponses) + && props.allowReadOperation === false) { + throw new Error(`The 'allowReadOperation' property must be set to true or undefined when setting any of the following: ` + + `'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'`); + } + if ((props.deleteRequestTemplate || props.additionalDeleteRequestTemplates || props.deleteIntegrationResponses) + && props.allowDeleteOperation !== true) { + throw new Error(`The 'allowDeleteOperation' property must be set to true when setting any of the following: ` + + `'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'`); + } + // Setup the dead letter queue, if applicable this.deadLetterQueue = defaults.buildDeadLetterQueue(this, { existingQueueObj: props.existingQueueObj, @@ -221,7 +245,7 @@ export class ApiGatewayToSqs extends Construct { const apiGatewayResource = this.apiGateway.root.addResource('message'); // Create - const createRequestTemplate = props.createRequestTemplate ?? 'Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")'; + const createRequestTemplate = props.createRequestTemplate ?? this.defaultCreateRequestTemplate; if (props.allowCreateOperation && props.allowCreateOperation === true) { this.addActionToPolicy("sqs:SendMessage"); defaults.addProxyMethodToApiResource({ @@ -238,7 +262,7 @@ export class ApiGatewayToSqs extends Construct { } // Read - const readRequestTemplate = props.readRequestTemplate ?? "Action=ReceiveMessage"; + const readRequestTemplate = props.readRequestTemplate ?? this.defaultReadRequestTemplate; if (props.allowReadOperation === undefined || props.allowReadOperation === true) { this.addActionToPolicy("sqs:ReceiveMessage"); defaults.addProxyMethodToApiResource({ @@ -255,7 +279,7 @@ export class ApiGatewayToSqs extends Construct { } // Delete - const deleteRequestTemplate = props.deleteRequestTemplate ?? "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))"; + const deleteRequestTemplate = props.deleteRequestTemplate ?? this.defaultDeleteRequestTemplate; if (props.allowDeleteOperation && props.allowDeleteOperation === true) { this.addActionToPolicy("sqs:DeleteMessage"); defaults.addProxyMethodToApiResource({ diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts index dc57a1f6f..49775b0bd 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/test/apigateway-sqs.test.ts @@ -304,44 +304,76 @@ test('Construct accepts additional create request templates', () => { }); }); +test('Construct accepts additional delete request templates', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + enableEncryptionWithCustomerManagedKey: true, + allowDeleteOperation: true, + additionalDeleteRequestTemplates: { + 'text/plain': 'DeleteMe' + } + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { + RequestTemplates: { + 'application/json': `Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))`, + 'text/plain': 'DeleteMe' + } + } + }); +}); + test('Construct can override default create request template type', () => { const stack = new Stack(); new ApiGatewayToSqs(stack, 'api-gateway-sqs', { enableEncryptionWithCustomerManagedKey: true, allowCreateOperation: true, - createRequestTemplate: 'Hello', - additionalCreateRequestTemplates: { - 'text/plain': 'Goodbye', - } + createRequestTemplate: 'Hello' }); expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { HttpMethod: 'POST', Integration: { RequestTemplates: { - 'application/json': 'Hello', - 'text/plain': 'Goodbye' + 'application/json': 'Hello' } } }); }); -test('Construct accepts additional delete request templates', () => { +test('Construct can override default read request template type', () => { const stack = new Stack(); new ApiGatewayToSqs(stack, 'api-gateway-sqs', { enableEncryptionWithCustomerManagedKey: true, - allowDeleteOperation: true, - additionalDeleteRequestTemplates: { - 'text/plain': 'DeleteMe' + allowReadOperation: true, + readRequestTemplate: 'Hello' + }); + + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + Integration: { + RequestTemplates: { + 'application/json': 'Hello' + } } }); +}); + +test('Construct can override default delete request template type', () => { + const stack = new Stack(); + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + enableEncryptionWithCustomerManagedKey: true, + allowDeleteOperation: true, + deleteRequestTemplate: 'Hello' + }); expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', Integration: { RequestTemplates: { - 'application/json': `Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))`, - 'text/plain': 'DeleteMe' + 'application/json': 'Hello' } } }); @@ -495,4 +527,88 @@ test('Construct uses custom deleteIntegrationResponses property', () => { ] } }); +}); + +test('Construct throws error when createRequestTemplate is set and allowCreateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + createRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowCreateOperation' property must be set to true when setting any of the following: 'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'/); +}); + +test('Construct throws error when additionalCreateRequestTemplates is set and allowCreateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + additionalCreateRequestTemplates: {} + }); + + expect(app).toThrowError(/The 'allowCreateOperation' property must be set to true when setting any of the following: 'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'/); +}); + +test('Construct throws error when createIntegrationResponses is set and allowCreateOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + createIntegrationResponses: [] + }); + + expect(app).toThrowError(/The 'allowCreateOperation' property must be set to true when setting any of the following: 'createRequestTemplate', 'additionalCreateRequestTemplates', 'createIntegrationResponses'/); +}); + +test('Construct throws error when readRequestTemplate is set and allowReadOperation is false', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowReadOperation: false, + readRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowReadOperation' property must be set to true or undefined when setting any of the following: 'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'/); +}); + +test('Construct throws error when additionalReadRequestTemplates is set and allowReadOperation is false', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowReadOperation: false, + additionalReadRequestTemplates: {}, + }); + + expect(app).toThrowError(/The 'allowReadOperation' property must be set to true or undefined when setting any of the following: 'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'/); +}); + +test('Construct throws error when readIntegrationResponses is set and allowReadOperation is false', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + allowReadOperation: false, + readIntegrationResponses: [], + }); + + expect(app).toThrowError(/The 'allowReadOperation' property must be set to true or undefined when setting any of the following: 'readRequestTemplate', 'additionalReadRequestTemplates', 'readIntegrationResponses'/); +}); + +test('Construct throws error when deleteRequestTemplate is set and allowDeleteOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + deleteRequestTemplate: '{}', + }); + + expect(app).toThrowError(/The 'allowDeleteOperation' property must be set to true when setting any of the following: 'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'/); +}); + +test('Construct throws error when additionalDeleteRequestTemplates is set and allowDeleteOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + additionalDeleteRequestTemplates: {} + }); + + expect(app).toThrowError(/The 'allowDeleteOperation' property must be set to true when setting any of the following: 'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'/); +}); + +test('Construct throws error when deleteIntegrationResponses is set and allowDeleteOperation is not true', () => { + const stack = new Stack(); + const app = () => new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + deleteIntegrationResponses: [] + }); + + expect(app).toThrowError(/The 'allowDeleteOperation' property must be set to true when setting any of the following: 'deleteRequestTemplate', 'additionalDeleteRequestTemplates', 'deleteIntegrationResponses'/); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts index f4c498fc5..79daaec13 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts @@ -12,6 +12,7 @@ */ import * as api from 'aws-cdk-lib/aws-apigateway'; +import { IntegrationResponse } from 'aws-cdk-lib/aws-apigateway'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { LogGroup } from 'aws-cdk-lib/aws-logs'; @@ -88,4 +89,22 @@ export function DefaultGlobalRestApiProps(_logGroup: LogGroup) { */ export function DefaultRegionalRestApiProps(_logGroup: LogGroup) { return DefaultRestApiProps([api.EndpointType.REGIONAL], _logGroup); +} + +/** + * @returns The set of default integration responses for status codes 200 and 500. + */ +export function DefaultIntegrationResponses(): IntegrationResponse[] { + return [ + { + statusCode: "200" + }, + { + statusCode: "500", + responseTemplates: { + "text/html": "Error" + }, + selectionPattern: "500" + } + ]; } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts index 8076b45cb..5809e2fbe 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts @@ -256,18 +256,7 @@ export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceI }; // Use user-provided integration responses, otherwise fallback to the default ones we provide. - const integrationResponses = params.integrationResponses ?? [ - { - statusCode: "200" - }, - { - statusCode: "500", - responseTemplates: { - "text/html": "Error" - }, - selectionPattern: "500" - } - ]; + const integrationResponses = params.integrationResponses ?? apiDefaults.DefaultIntegrationResponses(); let baseProps: api.AwsIntegrationProps = { service: params.service, diff --git a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts index 553514428..1ad8e510a 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts @@ -652,7 +652,7 @@ test('Test for ApiKey creation using lambdaApiProps', () => { }); }); -test('Test addMethodToApiResource with action', () => { +test('Additional request templates can be specified on addMethodToApiResource method', () => { const stack = new Stack(); const [ restApi ] = defaults.GlobalRestApi(stack); From b6e251e803a02ea617fa458ce34b6523fba69033 Mon Sep 17 00:00:00 2001 From: George Bearden Date: Fri, 3 Feb 2023 10:31:31 -0500 Subject: [PATCH 9/9] Update docs and add new test to apigateway helper to validate request template content types. --- .../aws-apigateway-dynamodb/README.md | 12 +++++---- .../aws-apigateway-sqs/README.md | 12 +++++---- .../core/lib/apigateway-helper.ts | 5 ++++ .../core/test/apigateway-helper.test.ts | 27 +++++++++++++++++++ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md index 416a840ec..7fed5cc7d 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md @@ -94,11 +94,13 @@ new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatew |apiGatewayLogGroup|[`logs.LogGroup`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroup.html)|Returns an instance of the LogGroup created by the construct for API Gateway access logging to CloudWatch.| # API Gateway Request/Response Template Properties Overview -There are sets of properties corresponding to the various API operations this construct supports (CREATE/READ/UPDATE/DELETE). Each API operation has a property to enable it, followed by a set of properties used to specify request templates and integration responses. Taking the **CREATE** operation as an example: -* The API method is enabled by setting the `allowCreateOperation` property to true. -* Once set, the request template for the `application/json` content-type can be set using the `createRequestTemplate` property. -* Additional request templates for content-types other than `application/json` can be specified in the `additionalCreateRequestTemplates` property. -* Non-default integration responses can be specified in the `createIntegrationResponses` property. +This construct allows you to implement four DynamoDB API operations, CREATE/READ/UPDATE/DELETE (corresponding the HTTP POST/GET/PUT/DELETE requests respectively). They are completely independent and each follows the same pattern: +* Setting `allowCreateOperation` to true will implement the `application/json` content-type with default request and response templates +* The request template for `application/json` requests can be customized using the `createRequestTemplate` prop value +* *Additional* request templates can be specified using the `additionalCreateRequestTemplates` prop value. Note - these DO NOT replace the `application/json` content-type +* Customized integration responses can be specified for any content type in the `createIntegrationResponses` prop value. + +Supplying any of these values without setting allowCreateOperation to true will result in an error. This pattern is the same for all four API operations. ## Default settings diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md index 7896f7ec8..ac7778663 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md @@ -99,11 +99,13 @@ new ApiGatewayToSqs(this, "ApiGatewayToSqsPattern", new ApiGatewayToSqsProps.Bui |DELETE|`/message?receiptHandle=[value]`||`sqs::DeleteMessage`|Deletes a specified message from the queue| # API Gateway Request/Response Template Properties Overview -There are sets of properties corresponding to the various API operations this construct supports (CREATE/READ/UPDATE/DELETE). Each API operation has a property to enable it, followed by a set of properties used to specify request templates and integration responses. Taking the **CREATE** operation as an example: -* The API method is enabled by setting the `allowCreateOperation` property to true. -* Once set, the request template for the `application/json` content-type can be set using the `createRequestTemplate` property. -* Additional request templates for content-types other than `application/json` can be specified in the `additionalCreateRequestTemplates` property. -* Non-default integration responses can be specified in the `createIntegrationResponses` property. +This construct allows you to implement four DynamoDB API operations, CREATE/READ/DELETE (corresponding the HTTP POST/GET/DELETE requests respectively). They are completely independent and each follows the same pattern: +* Setting `allowCreateOperation` to true will implement the `application/json` content-type with default request and response templates +* The request template for `application/json` requests can be customized using the `createRequestTemplate` prop value +* *Additional* request templates can be specified using the `additionalCreateRequestTemplates` prop value. Note - these DO NOT replace the `application/json` content-type +* Customized integration responses can be specified for any content type in the `createIntegrationResponses` prop value. + +Supplying any of these values without setting allowCreateOperation to true will result in an error. This pattern is the same for all four API operations. ## Default settings diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts index 5809e2fbe..7ba30d17f 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts @@ -250,6 +250,11 @@ export interface AddProxyMethodToApiResourceInputParams { } export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceInputParams): api.Method { + // Make sure the user hasn't also specified the application/json content-type in the additionalRequestTemplates optional property + if (params.additionalRequestTemplates && 'application/json' in params.additionalRequestTemplates) { + throw new Error(`Request Template for the application/json content-type must be specified in the requestTemplate property and not in the additionalRequestTemplates property `); + } + const requestTemplates = { "application/json": params.requestTemplate, ...params.additionalRequestTemplates diff --git a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts index 1ad8e510a..50745fa5f 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts @@ -774,4 +774,31 @@ test('Can override integration responses on addMethodToApiResource method', () = ], } }); +}); + +test('Specifying application/json content-type in additionalRequestTemplates property throws an error', () => { + const stack = new Stack(); + const [ restApi ] = defaults.GlobalRestApi(stack); + + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + const apiGatewayResource = restApi.root.addResource('api-gateway-resource'); + + const app = () => { + defaults.addProxyMethodToApiResource({ + action: 'Query', + service: 'dynamodb', + apiResource: apiGatewayResource, + apiGatewayRole, + apiMethod: 'GET', + requestTemplate: '{}', + additionalRequestTemplates: { + 'application/json': '{}' + } + }); + }; + + expect(app).toThrowError('Request Template for the application/json content-type must be specified in the requestTemplate property and not in the additionalRequestTemplates property'); }); \ No newline at end of file