From be70b93b830b91526500080cb0ce38c3822dfe6e Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Sat, 13 Feb 2021 17:24:45 +0530 Subject: [PATCH 01/13] feat(apigatewayv2): add support for WebSocket APIs --- .../aws-apigatewayv2-integrations/README.md | 27 + .../lib/index.ts | 1 + .../lib/websocket/index.ts | 1 + .../lib/websocket/lambda.ts | 44 ++ .../test/websocket/integ.lambda.expected.json | 534 ++++++++++++++++++ .../test/websocket/integ.lambda.ts | 65 +++ .../test/websocket/lambda.test.ts | 34 ++ packages/@aws-cdk/aws-apigatewayv2/README.md | 62 +- .../@aws-cdk/aws-apigatewayv2/lib/index.ts | 3 +- .../aws-apigatewayv2/lib/websocket/api.ts | 156 +++++ .../aws-apigatewayv2/lib/websocket/index.ts | 4 + .../lib/websocket/integration.ts | 110 ++++ .../aws-apigatewayv2/lib/websocket/route.ts | 84 +++ .../aws-apigatewayv2/lib/websocket/stage.ts | 67 +++ .../@aws-cdk/aws-apigatewayv2/package.json | 10 +- .../test/websocket/api.test.ts | 112 ++++ .../test/websocket/route.test.ts | 54 ++ .../test/websocket/stage.test.ts | 24 + 18 files changed, 1388 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index 6dd9de9e4e475..59546a4c9c086 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -21,6 +21,8 @@ - [Lambda Integration](#lambda) - [HTTP Proxy Integration](#http-proxy) - [Private Integration](#private-integration) +- [WebSocket APIs](#websocket-apis) + - [Lambda WebSocket Integration](#lambda-websocket-integration) ## HTTP APIs @@ -146,3 +148,28 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { }), }); ``` + +## WebSocket APIs + +WebSocket integrations connect a route to backend resources. Currently supported integrations include: LambdaWebSocketIntegration + +### Lambda WebSocket Integration + +Lambda integrations enable integrating a WebSocket API route with a Lambda function. When a client connects/disconnects or sends message specific to a route, the API Gateway service forwards the request to the Lambda function + +The API Gateway service will invoke the lambda function with an event payload of a specific format. + +The following code configures a `$connect` route with a Lambda integration + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultStageName: 'dev', +}); + +const connectHandler = new lambda.Function(stack, 'ConnectHandler', {...}); +webSocketApi.addConnectRoute({ + integration: new LambdaWebSocketIntegration({ + handler: connectHandler, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts index c202386ae710e..fd16aff655ff2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts @@ -1 +1,2 @@ export * from './http'; +export * from './websocket'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts new file mode 100644 index 0000000000000..04a64da0c7540 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts @@ -0,0 +1 @@ +export * from './lambda'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts new file mode 100644 index 0000000000000..20647300461f0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts @@ -0,0 +1,44 @@ +import { + IWebSocketRouteIntegration, + WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, + WebSocketRouteIntegrationConfig, +} from '@aws-cdk/aws-apigatewayv2'; +import { ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { Names, Stack } from '@aws-cdk/core'; + +/** + * Lambda WebSocket Integration props + */ +export interface LambdaWebSocketIntegrationProps { + /** + * The handler for this integration. + */ + readonly handler: IFunction +} + +/** + * Lambda WebSocket Integration + */ +export class LambdaWebSocketIntegration implements IWebSocketRouteIntegration { + constructor(private props: LambdaWebSocketIntegrationProps) {} + + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + const route = options.route; + this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { + scope: options.scope, + principal: new ServicePrincipal('apigateway.amazonaws.com'), + sourceArn: Stack.of(route).formatArn({ + service: 'execute-api', + resource: route.webSocketApi.webSocketApiId, + resourceName: `*/*${route.routeKey}`, + }), + }); + + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: this.props.handler.functionArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json new file mode 100644 index 0000000000000..d35243e179f2a --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -0,0 +1,534 @@ +{ + "Resources": { + "mywsapi32E6CE11": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "mywsapi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "mywsapiDefaultStageD7F52467": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "StageName": "dev", + "AutoDeploy": true + } + }, + "mywsapiconnectRouteWebSocketApiIntegmywsapiconnectRoute456CB290Permission2D0BC294": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$connect" + ] + ] + } + } + }, + "mywsapiconnectRoute45A0ED6A": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$connect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiWebSocketIntegrationce265f68100ffd2606f1dd2405c99c41FB7E6A43" + } + ] + ] + } + } + }, + "mywsapiWebSocketIntegrationce265f68100ffd2606f1dd2405c99c41FB7E6A43": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + } + } + }, + "mywsapidisconnectRouteWebSocketApiIntegmywsapidisconnectRoute26B84CF3PermissionB3F6D0A8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$disconnect" + ] + ] + } + } + }, + "mywsapidisconnectRoute421A8CB9": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$disconnect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiWebSocketIntegration95ae226072221a4970d36b36f8be00bf1ED15F29" + } + ] + ] + } + } + }, + "mywsapiWebSocketIntegration95ae226072221a4970d36b36f8be00bf1ED15F29": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + } + } + }, + "mywsapidefaultRouteWebSocketApiIntegmywsapidefaultRouteA13D926BPermission58B64FCE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$default" + ] + ] + } + } + }, + "mywsapidefaultRouteE9382DF8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiWebSocketIntegration577eac694fb2c36f751ca93aa2b0a9c2B01A5F80" + } + ] + ] + } + } + }, + "mywsapiWebSocketIntegration577eac694fb2c36f751ca93aa2b0a9c2B01A5F80": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + } + } + }, + "mywsapisendmessageRouteWebSocketApiIntegmywsapisendmessageRoute8A775F3CPermission660FB575": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MessageHandlerDFBBCD6B", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*sendmessage" + ] + ] + } + } + }, + "mywsapisendmessageRouteAE873328": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "sendmessage", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiWebSocketIntegrationd4b89276bcd1262f823f059f39c4e3d0050617A5" + } + ] + ] + } + } + }, + "mywsapiWebSocketIntegrationd4b89276bcd1262f823f059f39c4e3d0050617A5": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "MessageHandlerDFBBCD6B", + "Arn" + ] + } + } + }, + "ConnectHandlerServiceRole7E4A9B1F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "ConnectHandler2FFD52D8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"conencted\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "ConnectHandlerServiceRole7E4A9B1F", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "ConnectHandlerServiceRole7E4A9B1F" + ] + }, + "DisconnectHandlerServiceRoleE54F14F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "DisconnectHandlerCB7ED6F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"disconnected\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DisconnectHandlerServiceRoleE54F14F9", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DisconnectHandlerServiceRoleE54F14F9" + ] + }, + "DefaultHandlerServiceRoleDF00569C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "DefaultHandler604DF7AC": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"default\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DefaultHandlerServiceRoleDF00569C", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DefaultHandlerServiceRoleDF00569C" + ] + }, + "MessageHandlerServiceRoleDF05266A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MessageHandlerDFBBCD6B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"received\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "MessageHandlerServiceRoleDF05266A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "MessageHandlerServiceRoleDF05266A" + ] + } + }, + "Outputs": { + "ApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "wss://", + { + "Ref": "mywsapi32E6CE11" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts new file mode 100644 index 0000000000000..5f3e7bec6b512 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -0,0 +1,65 @@ +import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { LambdaWebSocketIntegration } from '../../lib'; + +/* + * Stack verification steps: + * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch + * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch + */ + +const app = new App(); +const stack = new Stack(app, 'WebSocketApiInteg'); + +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultStageName: 'dev', +}); + +const connectHandler = new lambda.Function(stack, 'ConnectHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "conencted" }; };'), +}); +webSocketApi.addConnectRoute({ + integration: new LambdaWebSocketIntegration({ + handler: connectHandler, + }), +}); + +const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "disconnected" }; };'), +}); +webSocketApi.addDisconnectRoute({ + integration: new LambdaWebSocketIntegration({ + handler: disconnetHandler, + }), +}); + +const defaultHandler = new lambda.Function(stack, 'DefaultHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "default" }; };'), +}); +webSocketApi.addDefaultRoute({ + integration: new LambdaWebSocketIntegration({ + handler: defaultHandler, + }), +}); + +const messageHandler = new lambda.Function(stack, 'MessageHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "received" }; };'), +}); +webSocketApi.addRoute('sendmessage', { + integration: new LambdaWebSocketIntegration({ + handler: messageHandler, + }), +}); + +new CfnOutput(stack, 'ApiEndpoint', { value: webSocketApi.defaultStage?.url! }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts new file mode 100644 index 0000000000000..57ba9497d2f25 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts @@ -0,0 +1,34 @@ +import '@aws-cdk/assert/jest'; +import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2'; +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { LambdaWebSocketIntegration } from '../../lib'; + + +describe('LambdaWebSocketIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + const fooFn = fooFunction(stack, 'Fn'); + + // WHEN + api.addConnectRoute({ + integration: new LambdaWebSocketIntegration({ handler: fooFn }), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationUri: stack.resolve(fooFn.functionArn), + }); + }); +}); + +function fooFunction(stack: Stack, id: string) { + return new Function(stack, id, { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }); +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 4da900f271e8f..5e4930e4fa907 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -7,7 +7,7 @@ Features | Stability -------------------------------------------|-------------------------------------------------------- CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) Higher level constructs for HTTP APIs | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) -Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shields.io/badge/not--implemented-black.svg?style=for-the-badge) +Higher level constructs for Websocket APIs | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) > **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources]) are always > stable and safe to use. @@ -38,6 +38,8 @@ Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shie - [Metrics](#metrics) - [VPC Link](#vpc-link) - [Private Integration](#private-integration) +- [Metrics](#metrics) +- [WebSocket API](#websocket-api) ## Introduction @@ -277,3 +279,61 @@ Amazon ECS container-based applications. Using private integrations, resources clients outside of the VPC. These integrations can be found in the [APIGatewayV2-Integrations](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-integrations-readme.html) constructs library. + +## WebSocket API + +A WebSocket API in API Gateway is a collection of WebSocket routes that are integrated with backend HTTP endpoints, Lambda functions, or other AWS services. You can use API Gateway features to help you with all aspects of the API lifecycle, from creation through monitoring your production APIs. [Read more](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) + +WebSocket APIs have two fundamental concepts - Routes and Integrations. + +In your WebSocket API, incoming JSON messages are directed to backend integrations based on routes that you configure. (Non-JSON messages are directed to a $default route that you configure.) + +A route includes a route key, which is the value that is expected once a route selection expression is evaluated. The routeSelectionExpression is an attribute defined at the API level. It specifies a JSON property that is expected to be present in the message payload. + +There are three predefined routes that can be used: $connect, $disconnect, and $default. In addition, you can create custom routes. + +- API Gateway calls the $connect route when a persistent connection between the client and a WebSocket API is being initiated. +- API Gateway calls the $disconnect route when the client or the server disconnects from the API. +- API Gateway calls a custom route after the route selection expression is evaluated against the message if a matching route is found; the match determines which integration is invoked. +- API Gateway calls the $default route if the route selection expression cannot be evaluated against the message or if no matching route is found. + +Integrations define how the WebSocket API behaves when a client reaches a specific Route.Learn more at +[Configuring integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html). + +Integrations are available at the `aws-apigatewayv2-integrations` module and more information is available in that module. + +The below snippet shows how a basic WebSocket API can be configured + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultStageName: 'dev', +}); + +const connectHandler = new lambda.Function(stack, 'ConnectHandler', {...}); +webSocketApi.addConnectRoute({ + integration: new LambdaWebSocketIntegration({ + handler: connectHandler, + }), +}); + +const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', {...}); +webSocketApi.addDisconnectRoute({ + integration: new LambdaWebSocketIntegration({ + handler: disconnetHandler, + }), +}); + +const defaultHandler = new lambda.Function(stack, 'DefaultHandler', {...}); +webSocketApi.addDefaultRoute({ + integration: new LambdaWebSocketIntegration({ + handler: defaultHandler, + }), +}); + +const messageHandler = new lambda.Function(stack, 'MessageHandler', {...}); +webSocketApi.addRoute('sendmessage', { + integration: new LambdaWebSocketIntegration({ + handler: messageHandler, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts index 31ea86b4a91c2..12dd8113f8b4c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -1,3 +1,4 @@ export * from './apigatewayv2.generated'; export * from './common'; -export * from './http'; \ No newline at end of file +export * from './http'; +export * from './websocket'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts new file mode 100644 index 0000000000000..fe3f691f17068 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -0,0 +1,156 @@ +import * as crypto from 'crypto'; +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnApi } from '../apigatewayv2.generated'; +import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; +import { WebSocketRoute, WebSocketRouteOptions } from './route'; +import { WebSocketStage } from './stage'; + +/** + * Represents a WebSocket API + */ +export interface IWebSocketApi extends IResource { + /** + * The identifier of this API Gateway WebSocket API. + * @attribute + */ + readonly webSocketApiId: string; + + /** + * The default endpoint for an API + * @attribute + */ + readonly apiEndpoint: string; + + /** + * Add a websocket integration + * @internal + */ + _addIntegration(config: WebSocketRouteIntegrationConfig): WebSocketIntegration +} + +/** + * Props for WebSocket API + */ +export interface WebSocketApiProps { + /** + * Name for the WebSocket API resoruce + * @default - id of the WebSocketApi construct. + */ + readonly apiName?: string; + + /** + * The description of the API. + * @default - none + */ + readonly description?: string; + + /** + * The route selection expression for the API + * @default '$request.body.action' + */ + readonly routeSelectionExpression?: string; + + /** + * The name of the default stage with deployment + * @default - none + */ + readonly defaultStageName?: string; +} + +/** + * Create a new API Gateway WebSocket API endpoint. + * @resource AWS::ApiGatewayV2::Api + */ +export class WebSocketApi extends Resource implements IWebSocketApi { + public readonly webSocketApiId: string; + public readonly apiEndpoint: string; + + /** + * A human friendly name for this WebSocket API. Note that this is different from `webSocketApiId`. + */ + public readonly webSocketApiName?: string; + + /** + * default stage of the api resource + */ + public readonly defaultStage: WebSocketStage | undefined; + + private integrations: Record = {}; + + constructor(scope: Construct, id: string, props?: WebSocketApiProps) { + super(scope, id); + + this.webSocketApiName = props?.apiName ?? id; + + const resource = new CfnApi(this, 'Resource', { + name: this.webSocketApiName, + protocolType: 'WEBSOCKET', + description: props?.description, + routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', + }); + this.webSocketApiId = resource.ref; + this.apiEndpoint = resource.attrApiEndpoint; + + if (props?.defaultStageName) { + this.defaultStage = new WebSocketStage(this, 'DefaultStage', { + webSocketApi: this, + stageName: props.defaultStageName, + autoDeploy: true, + }); + } + } + + /** + * @internal + */ + public _addIntegration(config: WebSocketRouteIntegrationConfig): WebSocketIntegration { + const stringifiedConfig = JSON.stringify(config); + const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + + if (configHash in this.integrations) { + return this.integrations[configHash]; + } + + const integration = new WebSocketIntegration(this, `WebSocketIntegration-${configHash}`, { + webSocketApi: this, + integrationType: config.type, + integrationUri: config.uri, + }); + this.integrations[configHash] = integration; + + return integration; + } + + /** + * Add a new route + */ + public addRoute(routeKey: string, options: WebSocketRouteOptions) { + return new WebSocketRoute(this, `${routeKey}-Route`, { + webSocketApi: this, + routeKey, + ...options, + }); + } + + /** + * Add a connect route + */ + public addConnectRoute(options: WebSocketRouteOptions) { + return this.addRoute('$connect', options); + } + + /** + * Add a disconnect route + */ + public addDisconnectRoute(options: WebSocketRouteOptions) { + return this.addRoute('$disconnect', options); + } + + /** + * Add a default route + */ + public addDefaultRoute(options: WebSocketRouteOptions) { + return this.addRoute('$default', options); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts new file mode 100644 index 0000000000000..b0ce6a8a91419 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './route'; +export * from './stage'; +export * from './integration'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts new file mode 100644 index 0000000000000..25da1d7823d75 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts @@ -0,0 +1,110 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnIntegration } from '../apigatewayv2.generated'; +import { IIntegration } from '../common'; +import { IWebSocketApi } from './api'; +import { IWebSocketRoute } from './route'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Represents an Integration for an WebSocket API. + */ +export interface IWebSocketIntegration extends IIntegration { + /** The WebSocket API associated with this integration */ + readonly webSocketApi: IWebSocketApi; +} + +/** + * WebSocket Integration Types + */ +export enum WebSocketIntegrationType { + /** + * AWS Proxy Integration Type + */ + AWS_PROXY = 'AWS_PROXY' +} + +/** + * The integration properties + */ +export interface WebSocketIntegrationProps { + /** + * The WebSocket API to which this integration should be bound. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * Integration type + */ + readonly integrationType: WebSocketIntegrationType; + + /** + * Integration URI. + */ + readonly integrationUri: string; +} + +/** + * The integration for an API route. + * @resource AWS::ApiGatewayV2::Integration + */ +export class WebSocketIntegration extends Resource implements IWebSocketIntegration { + public readonly integrationId: string; + public readonly webSocketApi: IWebSocketApi; + + constructor(scope: Construct, id: string, props: WebSocketIntegrationProps) { + super(scope, id); + const integ = new CfnIntegration(this, 'Resource', { + apiId: props.webSocketApi.webSocketApiId, + integrationType: props.integrationType, + integrationUri: props.integrationUri, + }); + this.integrationId = integ.ref; + this.webSocketApi = props.webSocketApi; + } +} + +/** + * Options to the WebSocketRouteIntegration during its bind operation. + */ +export interface WebSocketRouteIntegrationBindOptions { + /** + * The route to which this is being bound. + */ + readonly route: IWebSocketRoute; + + /** + * The current scope in which the bind is occurring. + * If the `WebSocketRouteIntegration` being bound creates additional constructs, + * this will be used as their parent scope. + */ + readonly scope: CoreConstruct; +} + +/** + * The interface that various route integration classes will inherit. + */ +export interface IWebSocketRouteIntegration { + /** + * Bind this integration to the route. + */ + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig; +} + +/** + * Config returned back as a result of the bind. + */ +export interface WebSocketRouteIntegrationConfig { + /** + * Integration type. + */ + readonly type: WebSocketIntegrationType; + + /** + * Integration URI + */ + readonly uri: string; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts new file mode 100644 index 0000000000000..eff941dadfddc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -0,0 +1,84 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnRoute } from '../apigatewayv2.generated'; +import { IRoute } from '../common'; +import { IWebSocketApi } from './api'; +import { IWebSocketRouteIntegration } from './integration'; + +/** + * Represents a Route for an WebSocket API. + */ +export interface IWebSocketRoute extends IRoute { + /** + * The WebSocket API associated with this route. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The key to this route. + * @attribute + */ + readonly routeKey: string; +} + +/** + * Options used to add route to the API + */ +export interface WebSocketRouteOptions { + /** + * The integration to be configured on this route. + */ + readonly integration: IWebSocketRouteIntegration; +} + + +/** + * Properties to initialize a new Route + */ +export interface WebSocketRouteProps extends WebSocketRouteOptions { + /** + * the API the route is associated with + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The key to this route. + */ + readonly routeKey: string; +} + +/** + * Route class that creates the Route for API Gateway WebSocket API + * @resource AWS::ApiGatewayV2::Route + */ +export class WebSocketRoute extends Resource implements IWebSocketRoute { + public readonly routeId: string; + public readonly webSocketApi: IWebSocketApi; + public readonly routeKey: string; + + /** + * Integration response ID + */ + public readonly integrationResponseId?: string; + + constructor(scope: Construct, id: string, props: WebSocketRouteProps) { + super(scope, id); + + this.webSocketApi = props.webSocketApi; + this.routeKey = props.routeKey; + + const config = props.integration.bind({ + route: this, + scope: this, + }); + + const integration = props.webSocketApi._addIntegration(config); + + const route = new CfnRoute(this, 'Resource', { + apiId: props.webSocketApi.webSocketApiId, + routeKey: props.routeKey, + target: `integrations/${integration.integrationId}`, + }); + this.routeId = route.ref; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts new file mode 100644 index 0000000000000..5d33cde32cdcc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -0,0 +1,67 @@ +import { Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnStage } from '../apigatewayv2.generated'; +import { IStage } from '../common'; +import { IWebSocketApi } from './api'; + +const DEFAULT_STAGE_NAME = 'dev'; + +/** + * Represents the WebSocketStage + */ +export interface IWebSocketStage extends IStage { +} + +/** + * Properties to initialize an instance of `WebSocketStage`. + */ +export interface WebSocketStageProps { + /** + * The WebSocket API to which this stage is associated. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The name of the stage. + */ + readonly stageName: string; + + /** + * Whether updates to an API automatically trigger a new deployment. + * @default false + */ + readonly autoDeploy?: boolean; +} + +/** + * Represents a stage where an instance of the API is deployed. + * @resource AWS::ApiGatewayV2::Stage + */ +export class WebSocketStage extends Resource implements IWebSocketStage { + public readonly stageName: string; + private webSocketApi: IWebSocketApi; + + constructor(scope: Construct, id: string, props: WebSocketStageProps) { + super(scope, id, { + physicalName: props.stageName ? props.stageName : DEFAULT_STAGE_NAME, + }); + + this.webSocketApi = props.webSocketApi; + this.stageName = this.physicalName; + + new CfnStage(this, 'Resource', { + apiId: props.webSocketApi.webSocketApiId, + stageName: this.physicalName, + autoDeploy: props.autoDeploy, + }); + } + + /** + * The URL to this stage. + */ + public get url(): string { + const s = Stack.of(this); + const urlPath = this.stageName; + return `wss://${this.webSocketApi.webSocketApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 19abb0ca10b3f..e6aa7a18b5365 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -109,7 +109,13 @@ "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps" + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketApi", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketIntegration", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketRoute", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketStage", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketRouteProps" ] }, "stability": "experimental", @@ -121,7 +127,7 @@ }, { "name": "Higher level constructs for Websocket APIs", - "stability": "Not Implemented" + "stability": "Experimental" } ], "awscdkio": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts new file mode 100644 index 0000000000000..e0007c440d3ae --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -0,0 +1,112 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, +} from '../../lib'; + +describe('WebSocketApi', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'api'); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'WEBSOCKET', + }); + + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Stage'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Route'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Integration'); + }); + + test('setting defaultStageName', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new WebSocketApi(stack, 'api', { + defaultStageName: 'dev', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.webSocketApiId), + StageName: 'dev', + AutoDeploy: true, + }); + }); + + test('addRoute: adds a route with passed key', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + // WHEN + api.addRoute('myroute', { integration: new DummyIntegration() }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.webSocketApiId), + RouteKey: 'myroute', + }); + }); + + test('addConnectRoute: adds a $connect route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + // WHEN + api.addConnectRoute({ integration: new DummyIntegration() }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.webSocketApiId), + RouteKey: '$connect', + }); + }); + + test('addDisconnectRoute: adds a $disconnect route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + // WHEN + api.addDisconnectRoute({ integration: new DummyIntegration() }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.webSocketApiId), + RouteKey: '$disconnect', + }); + }); + + test('addDefaultRoute: adds a $default route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + // WHEN + api.addDefaultRoute({ integration: new DummyIntegration() }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.webSocketApiId), + RouteKey: '$default', + }); + }); +}); + +class DummyIntegration implements IWebSocketRouteIntegration { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts new file mode 100644 index 0000000000000..af0183f3097d9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, + WebSocketRoute, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, +} from '../../lib'; + +describe('WebSocketRoute', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const webSocketApi = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketRoute(stack, 'Route', { + webSocketApi, + integration: new DummyIntegration(), + routeKey: 'message', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(webSocketApi.webSocketApiId), + RouteKey: 'message', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'ApiWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f23D15DECE', + }, + ], + ], + }, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(webSocketApi.webSocketApiId), + IntegrationType: 'AWS_PROXY', + IntegrationUri: 'some-uri', + }); + }); +}); + + +class DummyIntegration implements IWebSocketRouteIntegration { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts new file mode 100644 index 0000000000000..bb9ba441c5c67 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -0,0 +1,24 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { WebSocketApi, WebSocketStage } from '../../lib'; + +describe('WebSocketStage', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const defaultStage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.webSocketApiId), + StageName: 'dev', + }); + expect(defaultStage.url.endsWith('/dev')).toBe(true); + }); +}); From 89f4ad9788370b2f97ba60341b34d2a268f778c9 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Sat, 13 Feb 2021 18:18:43 +0530 Subject: [PATCH 02/13] fix config hashing --- .../test/websocket/integ.lambda.expected.json | 16 ++++++++-------- .../aws-apigatewayv2/lib/websocket/api.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json index d35243e179f2a..7d0fccc00da86 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -68,14 +68,14 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegrationce265f68100ffd2606f1dd2405c99c41FB7E6A43" + "Ref": "mywsapiWebSocketIntegration50b017444a02be00a0b575d1233145812C29D4A1" } ] ] } } }, - "mywsapiWebSocketIntegrationce265f68100ffd2606f1dd2405c99c41FB7E6A43": { + "mywsapiWebSocketIntegration50b017444a02be00a0b575d1233145812C29D4A1": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -140,14 +140,14 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegration95ae226072221a4970d36b36f8be00bf1ED15F29" + "Ref": "mywsapiWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba06EB555F" } ] ] } } }, - "mywsapiWebSocketIntegration95ae226072221a4970d36b36f8be00bf1ED15F29": { + "mywsapiWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba06EB555F": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -212,14 +212,14 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegration577eac694fb2c36f751ca93aa2b0a9c2B01A5F80" + "Ref": "mywsapiWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d79AE25D4B" } ] ] } } }, - "mywsapiWebSocketIntegration577eac694fb2c36f751ca93aa2b0a9c2B01A5F80": { + "mywsapiWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d79AE25D4B": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -284,14 +284,14 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegrationd4b89276bcd1262f823f059f39c4e3d0050617A5" + "Ref": "mywsapiWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430B81BE641" } ] ] } } }, - "mywsapiWebSocketIntegrationd4b89276bcd1262f823f059f39c4e3d0050617A5": { + "mywsapiWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430B81BE641": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index fe3f691f17068..8c3e7039580bd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -1,5 +1,5 @@ import * as crypto from 'crypto'; -import { IResource, Resource } from '@aws-cdk/core'; +import { IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi } from '../apigatewayv2.generated'; import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; @@ -105,7 +105,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { * @internal */ public _addIntegration(config: WebSocketRouteIntegrationConfig): WebSocketIntegration { - const stringifiedConfig = JSON.stringify(config); + const stringifiedConfig = JSON.stringify(Stack.of(this).resolve(config)); const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); if (configHash in this.integrations) { From 1cd6d413dfda6288290f84b14538fa31a7138535 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Thu, 18 Feb 2021 16:57:03 +0530 Subject: [PATCH 03/13] fix readme --- packages/@aws-cdk/aws-apigatewayv2/README.md | 23 +++++++------------ .../aws-apigatewayv2/lib/websocket/api.ts | 12 +++++----- .../aws-apigatewayv2/lib/websocket/route.ts | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 5e4930e4fa907..62ec6eb13a77d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -38,7 +38,6 @@ Higher level constructs for Websocket APIs | ![Experimental](https://img.shields - [Metrics](#metrics) - [VPC Link](#vpc-link) - [Private Integration](#private-integration) -- [Metrics](#metrics) - [WebSocket API](#websocket-api) ## Introduction @@ -232,7 +231,7 @@ API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-acces These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library. -## Metrics +### Metrics The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch. These metrics can be referred to using the metric APIs available on the `HttpApi` construct. @@ -282,25 +281,19 @@ These integrations can be found in the [APIGatewayV2-Integrations](https://docs. ## WebSocket API -A WebSocket API in API Gateway is a collection of WebSocket routes that are integrated with backend HTTP endpoints, Lambda functions, or other AWS services. You can use API Gateway features to help you with all aspects of the API lifecycle, from creation through monitoring your production APIs. [Read more](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) +A WebSocket API in API Gateway is a collection of WebSocket routes that are integrated with backend HTTP endpoints, +Lambda functions, or other AWS services. You can use API Gateway features to help you with all aspects of the API +lifecycle, from creation through monitoring your production APIs. [Read more](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) WebSocket APIs have two fundamental concepts - Routes and Integrations. -In your WebSocket API, incoming JSON messages are directed to backend integrations based on routes that you configure. (Non-JSON messages are directed to a $default route that you configure.) +WebSocket APIs direct JSON messages to backend integrations based on configured routes. (Non-JSON messages are directed +to the configured `$default` route.) -A route includes a route key, which is the value that is expected once a route selection expression is evaluated. The routeSelectionExpression is an attribute defined at the API level. It specifies a JSON property that is expected to be present in the message payload. - -There are three predefined routes that can be used: $connect, $disconnect, and $default. In addition, you can create custom routes. - -- API Gateway calls the $connect route when a persistent connection between the client and a WebSocket API is being initiated. -- API Gateway calls the $disconnect route when the client or the server disconnects from the API. -- API Gateway calls a custom route after the route selection expression is evaluated against the message if a matching route is found; the match determines which integration is invoked. -- API Gateway calls the $default route if the route selection expression cannot be evaluated against the message or if no matching route is found. - -Integrations define how the WebSocket API behaves when a client reaches a specific Route.Learn more at +Integrations define how the WebSocket API behaves when a client reaches a specific Route. Learn more at [Configuring integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html). -Integrations are available at the `aws-apigatewayv2-integrations` module and more information is available in that module. +Integrations are available in the `aws-apigatewayv2-integrations` module and more information is available in that module. The below snippet shows how a basic WebSocket API can be configured diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index 8c3e7039580bd..95703f4bc8ec0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -26,7 +26,7 @@ export interface IWebSocketApi extends IResource { * Add a websocket integration * @internal */ - _addIntegration(config: WebSocketRouteIntegrationConfig): WebSocketIntegration + _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration } /** @@ -53,7 +53,7 @@ export interface WebSocketApiProps { /** * The name of the default stage with deployment - * @default - none + * @default - No default stage is created */ readonly defaultStageName?: string; } @@ -104,15 +104,15 @@ export class WebSocketApi extends Resource implements IWebSocketApi { /** * @internal */ - public _addIntegration(config: WebSocketRouteIntegrationConfig): WebSocketIntegration { - const stringifiedConfig = JSON.stringify(Stack.of(this).resolve(config)); + public _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration { + const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); if (configHash in this.integrations) { return this.integrations[configHash]; } - const integration = new WebSocketIntegration(this, `WebSocketIntegration-${configHash}`, { + const integration = new WebSocketIntegration(scope, `WebSocketIntegration-${configHash}`, { webSocketApi: this, integrationType: config.type, integrationUri: config.uri, @@ -153,4 +153,4 @@ export class WebSocketApi extends Resource implements IWebSocketApi { public addDefaultRoute(options: WebSocketRouteOptions) { return this.addRoute('$default', options); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts index eff941dadfddc..4982962a10f5e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -72,7 +72,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { scope: this, }); - const integration = props.webSocketApi._addIntegration(config); + const integration = props.webSocketApi._addIntegration(this, config); const route = new CfnRoute(this, 'Resource', { apiId: props.webSocketApi.webSocketApiId, From ce05383ab6a446ae5276c55b1d33116fef3e90df Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 19 Feb 2021 15:04:22 +0530 Subject: [PATCH 04/13] update --- .../lib/http/alb.ts | 4 +- .../lib/http/http-proxy.ts | 4 +- .../lib/http/lambda.ts | 4 +- .../lib/http/nlb.ts | 4 +- .../lib/http/private/integration.ts | 4 +- .../lib/http/service-discovery.ts | 4 +- .../lib/websocket/lambda.ts | 6 +- .../test/http/private/integration.test.ts | 4 +- .../test/websocket/integ.lambda.expected.json | 524 +++++++++--------- .../test/websocket/integ.lambda.ts | 31 +- .../test/websocket/lambda.test.ts | 7 +- packages/@aws-cdk/aws-apigatewayv2/README.md | 38 +- .../lib/common/api-mapping.ts | 109 +++- .../aws-apigatewayv2/lib/common/api.ts | 155 ++++++ .../aws-apigatewayv2/lib/common/index.ts | 1 + .../lib/common/integration.ts | 6 + .../aws-apigatewayv2/lib/common/stage.ts | 181 +++++- .../aws-apigatewayv2/lib/http/api-mapping.ts | 109 ---- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 133 +---- .../aws-apigatewayv2/lib/http/index.ts | 1 - .../aws-apigatewayv2/lib/http/integration.ts | 6 +- .../aws-apigatewayv2/lib/http/stage.ts | 150 +---- .../aws-apigatewayv2/lib/websocket/api.ts | 109 ++-- .../lib/websocket/integration.ts | 8 +- .../aws-apigatewayv2/lib/websocket/route.ts | 2 +- .../aws-apigatewayv2/lib/websocket/stage.ts | 37 +- .../@aws-cdk/aws-apigatewayv2/package.json | 6 +- .../test/{http => common}/api-mapping.test.ts | 24 +- .../aws-apigatewayv2/test/http/api.test.ts | 22 +- .../aws-apigatewayv2/test/http/route.test.ts | 6 +- .../aws-apigatewayv2/test/http/stage.test.ts | 5 +- .../test/websocket/api.test.ts | 41 +- .../test/websocket/route.test.ts | 10 +- .../test/websocket/stage.test.ts | 2 +- 34 files changed, 926 insertions(+), 831 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts rename packages/@aws-cdk/aws-apigatewayv2/test/{http => common}/api-mapping.test.ts (89%) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts index b6afd1cc76450..0f8563abdfe0d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts @@ -1,4 +1,4 @@ -import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import { HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { HttpPrivateIntegrationOptions } from './base-types'; @@ -22,7 +22,7 @@ export class HttpAlbIntegration extends HttpPrivateIntegration { super(); } - public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { let vpc: ec2.IVpc | undefined = this.props.vpcLink?.vpc; if (!vpc && (this.props.listener instanceof elbv2.ApplicationListener)) { vpc = this.props.listener.loadBalancer.vpc; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts index a7ef2d1b4d7b9..a55affd6a486e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts @@ -1,7 +1,7 @@ import { HttpIntegrationType, HttpRouteIntegrationBindOptions, - HttpRouteIntegrationConfig, + IHttpRouteIntegrationConfig, HttpMethod, IHttpRouteIntegration, PayloadFormatVersion, @@ -30,7 +30,7 @@ export class HttpProxyIntegration implements IHttpRouteIntegration { constructor(private readonly props: HttpProxyIntegrationProps) { } - public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { return { method: this.props.method ?? HttpMethod.ANY, payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index a962b268d7165..20bb659a99eb8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -1,7 +1,7 @@ import { HttpIntegrationType, HttpRouteIntegrationBindOptions, - HttpRouteIntegrationConfig, + IHttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion, } from '@aws-cdk/aws-apigatewayv2'; @@ -34,7 +34,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { constructor(private readonly props: LambdaProxyIntegrationProps) { } - public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { const route = options.route; this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { scope: options.scope, diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts index 85e3f3773d1c4..89b169fb2e52c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts @@ -1,4 +1,4 @@ -import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import { HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { HttpPrivateIntegrationOptions } from './base-types'; @@ -22,7 +22,7 @@ export class HttpNlbIntegration extends HttpPrivateIntegration { super(); } - public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { let vpc: ec2.IVpc | undefined = this.props.vpcLink?.vpc; if (!vpc && (this.props.listener instanceof elbv2.NetworkListener)) { vpc = this.props.listener.loadBalancer.vpc; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts index 6d32b22794722..e845ac8b65dec 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts @@ -2,7 +2,7 @@ import { HttpConnectionType, HttpIntegrationType, HttpRouteIntegrationBindOptions, - HttpRouteIntegrationConfig, + IHttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion, HttpMethod, @@ -61,5 +61,5 @@ export abstract class HttpPrivateIntegration implements IHttpRouteIntegration { return vpcLink; } - public abstract bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; + public abstract bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig; } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts index 44e8b148754dd..a19f33541e8a8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts @@ -1,4 +1,4 @@ -import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import { HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; import { HttpPrivateIntegrationOptions } from './base-types'; import { HttpPrivateIntegration } from './private/integration'; @@ -21,7 +21,7 @@ export class HttpServiceDiscoveryIntegration extends HttpPrivateIntegration { super(); } - public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { if (!this.props.vpcLink) { throw new Error('The vpcLink property is mandatory'); } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts index 20647300461f0..b5d9a0a090f32 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts @@ -2,7 +2,7 @@ import { IWebSocketRouteIntegration, WebSocketIntegrationType, WebSocketRouteIntegrationBindOptions, - WebSocketRouteIntegrationConfig, + IWebSocketRouteIntegrationConfig, } from '@aws-cdk/aws-apigatewayv2'; import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; @@ -24,14 +24,14 @@ export interface LambdaWebSocketIntegrationProps { export class LambdaWebSocketIntegration implements IWebSocketRouteIntegration { constructor(private props: LambdaWebSocketIntegrationProps) {} - bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + bind(options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig { const route = options.route; this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { scope: options.scope, principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn: Stack.of(route).formatArn({ service: 'execute-api', - resource: route.webSocketApi.webSocketApiId, + resource: route.webSocketApi.apiId, resourceName: `*/*${route.routeKey}`, }), }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts index ce5f269f648a0..86462ed579cf1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts @@ -1,5 +1,5 @@ import '@aws-cdk/assert/jest'; -import { HttpApi, HttpRoute, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpRoute, HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; import { Stack } from '@aws-cdk/core'; import { HttpPrivateIntegration } from '../../../lib/http/private/integration'; @@ -12,7 +12,7 @@ describe('HttpPrivateIntegration', () => { super(); } - public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { const vpcLink = this._configureVpcLink(options, {}); return { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json index 7d0fccc00da86..b4d04623cc881 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -1,5 +1,205 @@ { "Resources": { + "ConnectHandlerServiceRole7E4A9B1F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "ConnectHandler2FFD52D8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"conencted\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "ConnectHandlerServiceRole7E4A9B1F", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "ConnectHandlerServiceRole7E4A9B1F" + ] + }, + "DisconnectHandlerServiceRoleE54F14F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "DisconnectHandlerCB7ED6F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"disconnected\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DisconnectHandlerServiceRoleE54F14F9", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DisconnectHandlerServiceRoleE54F14F9" + ] + }, + "DefaultHandlerServiceRoleDF00569C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "DefaultHandler604DF7AC": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"default\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DefaultHandlerServiceRoleDF00569C", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DefaultHandlerServiceRoleDF00569C" + ] + }, + "MessageHandlerServiceRoleDF05266A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MessageHandlerDFBBCD6B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"received\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "MessageHandlerServiceRoleDF05266A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "MessageHandlerServiceRoleDF05266A" + ] + }, "mywsapi32E6CE11": { "Type": "AWS::ApiGatewayV2::Api", "Properties": { @@ -55,6 +255,21 @@ } } }, + "mywsapiconnectRouteWebSocketIntegration50b017444a02be00a0b575d123314581176017EE": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + } + } + }, "mywsapiconnectRoute45A0ED6A": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -68,28 +283,13 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegration50b017444a02be00a0b575d1233145812C29D4A1" + "Ref": "mywsapiconnectRouteWebSocketIntegration50b017444a02be00a0b575d123314581176017EE" } ] ] } } }, - "mywsapiWebSocketIntegration50b017444a02be00a0b575d1233145812C29D4A1": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "mywsapi32E6CE11" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "ConnectHandler2FFD52D8", - "Arn" - ] - } - } - }, "mywsapidisconnectRouteWebSocketApiIntegmywsapidisconnectRoute26B84CF3PermissionB3F6D0A8": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -127,6 +327,21 @@ } } }, + "mywsapidisconnectRouteWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba1F7F68BC": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + } + } + }, "mywsapidisconnectRoute421A8CB9": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -140,28 +355,13 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba06EB555F" + "Ref": "mywsapidisconnectRouteWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba1F7F68BC" } ] ] } } }, - "mywsapiWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba06EB555F": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "mywsapi32E6CE11" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "DisconnectHandlerCB7ED6F7", - "Arn" - ] - } - } - }, "mywsapidefaultRouteWebSocketApiIntegmywsapidefaultRouteA13D926BPermission58B64FCE": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -199,6 +399,21 @@ } } }, + "mywsapidefaultRouteWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d7A2B7F2FA": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + } + } + }, "mywsapidefaultRouteE9382DF8": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -212,28 +427,13 @@ [ "integrations/", { - "Ref": "mywsapiWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d79AE25D4B" + "Ref": "mywsapidefaultRouteWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d7A2B7F2FA" } ] ] } } }, - "mywsapiWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d79AE25D4B": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "mywsapi32E6CE11" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "DefaultHandler604DF7AC", - "Arn" - ] - } - } - }, "mywsapisendmessageRouteWebSocketApiIntegmywsapisendmessageRoute8A775F3CPermission660FB575": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -271,27 +471,7 @@ } } }, - "mywsapisendmessageRouteAE873328": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "mywsapi32E6CE11" - }, - "RouteKey": "sendmessage", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "mywsapiWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430B81BE641" - } - ] - ] - } - } - }, - "mywsapiWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430B81BE641": { + "mywsapisendmessageRouteWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430786B6471": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -306,205 +486,25 @@ } } }, - "ConnectHandlerServiceRole7E4A9B1F": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] - } - ] - } - }, - "ConnectHandler2FFD52D8": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"conencted\" }; };" - }, - "Role": { - "Fn::GetAtt": [ - "ConnectHandlerServiceRole7E4A9B1F", - "Arn" - ] - }, - "Handler": "index.handler", - "Runtime": "nodejs12.x" - }, - "DependsOn": [ - "ConnectHandlerServiceRole7E4A9B1F" - ] - }, - "DisconnectHandlerServiceRoleE54F14F9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] - } - ] - } - }, - "DisconnectHandlerCB7ED6F7": { - "Type": "AWS::Lambda::Function", + "mywsapisendmessageRouteAE873328": { + "Type": "AWS::ApiGatewayV2::Route", "Properties": { - "Code": { - "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"disconnected\" }; };" - }, - "Role": { - "Fn::GetAtt": [ - "DisconnectHandlerServiceRoleE54F14F9", - "Arn" - ] + "ApiId": { + "Ref": "mywsapi32E6CE11" }, - "Handler": "index.handler", - "Runtime": "nodejs12.x" - }, - "DependsOn": [ - "DisconnectHandlerServiceRoleE54F14F9" - ] - }, - "DefaultHandlerServiceRoleDF00569C": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" + "RouteKey": "sendmessage", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapisendmessageRouteWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430786B6471" } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] ] - } - ] - } - }, - "DefaultHandler604DF7AC": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"default\" }; };" - }, - "Role": { - "Fn::GetAtt": [ - "DefaultHandlerServiceRoleDF00569C", - "Arn" ] - }, - "Handler": "index.handler", - "Runtime": "nodejs12.x" - }, - "DependsOn": [ - "DefaultHandlerServiceRoleDF00569C" - ] - }, - "MessageHandlerServiceRoleDF05266A": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] - } - ] + } } - }, - "MessageHandlerDFBBCD6B": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"received\" }; };" - }, - "Role": { - "Fn::GetAtt": [ - "MessageHandlerServiceRoleDF05266A", - "Arn" - ] - }, - "Handler": "index.handler", - "Runtime": "nodejs12.x" - }, - "DependsOn": [ - "MessageHandlerServiceRoleDF05266A" - ] } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts index 5f3e7bec6b512..e13c2da741676 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -14,52 +14,37 @@ import { LambdaWebSocketIntegration } from '../../lib'; const app = new App(); const stack = new Stack(app, 'WebSocketApiInteg'); -const webSocketApi = new WebSocketApi(stack, 'mywsapi', { - defaultStageName: 'dev', -}); - const connectHandler = new lambda.Function(stack, 'ConnectHandler', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "conencted" }; };'), }); -webSocketApi.addConnectRoute({ - integration: new LambdaWebSocketIntegration({ - handler: connectHandler, - }), -}); const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "disconnected" }; };'), }); -webSocketApi.addDisconnectRoute({ - integration: new LambdaWebSocketIntegration({ - handler: disconnetHandler, - }), -}); const defaultHandler = new lambda.Function(stack, 'DefaultHandler', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "default" }; };'), }); -webSocketApi.addDefaultRoute({ - integration: new LambdaWebSocketIntegration({ - handler: defaultHandler, - }), -}); const messageHandler = new lambda.Function(stack, 'MessageHandler', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "received" }; };'), }); -webSocketApi.addRoute('sendmessage', { - integration: new LambdaWebSocketIntegration({ - handler: messageHandler, - }), + +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultStageName: 'dev', + connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, + disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, + defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, }); +webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: messageHandler }) }); + new CfnOutput(stack, 'ApiEndpoint', { value: webSocketApi.defaultStage?.url! }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts index 57ba9497d2f25..5f431ca28fc49 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts @@ -9,12 +9,13 @@ describe('LambdaWebSocketIntegration', () => { test('default', () => { // GIVEN const stack = new Stack(); - const api = new WebSocketApi(stack, 'Api'); const fooFn = fooFunction(stack, 'Fn'); // WHEN - api.addConnectRoute({ - integration: new LambdaWebSocketIntegration({ handler: fooFn }), + new WebSocketApi(stack, 'Api', { + connectRouteOptions: { + integration: new LambdaWebSocketIntegration({ handler: fooFn }), + }, }); // THEN diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 62ec6eb13a77d..a4ca9622b19f8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -287,6 +287,10 @@ lifecycle, from creation through monitoring your production APIs. [Read more](ht WebSocket APIs have two fundamental concepts - Routes and Integrations. +```ts +addRoute +``` + WebSocket APIs direct JSON messages to backend integrations based on configured routes. (Non-JSON messages are directed to the configured `$default` route.) @@ -295,38 +299,28 @@ Integrations define how the WebSocket API behaves when a client reaches a specif Integrations are available in the `aws-apigatewayv2-integrations` module and more information is available in that module. -The below snippet shows how a basic WebSocket API can be configured +To add the default WebSocket routes supported by API Gateway (`$connect`, `$disconnect` and `$default`), configure them as part of api props: ```ts -const webSocketApi = new WebSocketApi(stack, 'mywsapi', { +new WebSocketApi(stack, 'mywsapi', { defaultStageName: 'dev', + connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, + disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, + defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, }); +``` -const connectHandler = new lambda.Function(stack, 'ConnectHandler', {...}); -webSocketApi.addConnectRoute({ - integration: new LambdaWebSocketIntegration({ - handler: connectHandler, - }), -}); - -const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', {...}); -webSocketApi.addDisconnectRoute({ - integration: new LambdaWebSocketIntegration({ - handler: disconnetHandler, - }), -}); +To add any other route: -const defaultHandler = new lambda.Function(stack, 'DefaultHandler', {...}); -webSocketApi.addDefaultRoute({ - integration: new LambdaWebSocketIntegration({ - handler: defaultHandler, - }), +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultStageName: 'dev', }); - -const messageHandler = new lambda.Function(stack, 'MessageHandler', {...}); webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: messageHandler, }), }); ``` + + diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index d843b51f8b315..23675a5521697 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -1,4 +1,9 @@ -import { IResource } from '@aws-cdk/core'; +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; +import { IApi } from './api'; +import { IDomainName } from './domain-name'; +import { IStage } from './stage'; /** * Represents an ApiGatewayV2 ApiMapping resource @@ -11,3 +16,105 @@ export interface IApiMapping extends IResource { */ readonly apiMappingId: string; } + +/** + * Properties used to create the ApiMapping resource + */ +export interface ApiMappingProps { + /** + * Api mapping key. The path where this stage should be mapped to on the domain + * @default - undefined for the root path mapping. + */ + readonly apiMappingKey?: string; + + /** + * The Api to which this mapping is applied + */ + readonly api: IApi; + + /** + * custom domain name of the mapping target + */ + readonly domainName: IDomainName; + + /** + * stage for the ApiMapping resource + * + * @default - Default stage of the passed API + */ + readonly stage?: IStage; +} + +/** + * The attributes used to import existing ApiMapping + */ +export interface ApiMappingAttributes { + /** + * The API mapping ID + */ + readonly apiMappingId: string; +} + +/** + * Create a new API mapping for API Gateway API endpoint. + * @resource AWS::ApiGatewayV2::ApiMapping + */ +export class ApiMapping extends Resource implements IApiMapping { + /** + * import from API ID + */ + public static fromApiMappingAttributes(scope: Construct, id: string, attrs: ApiMappingAttributes): IApiMapping { + class Import extends Resource implements IApiMapping { + public readonly apiMappingId = attrs.apiMappingId; + } + return new Import(scope, id); + } + /** + * ID of the API Mapping + */ + public readonly apiMappingId: string; + + /** + * API Mapping key + */ + public readonly mappingKey?: string; + + constructor(scope: Construct, id: string, props: ApiMappingProps) { + super(scope, id); + + if ((!props.stage?.stageName) && !props.api.defaultStage) { + throw new Error('stage is required if default stage is not available'); + } + + const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; + if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { + throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); + } + + if (props.apiMappingKey === '') { + throw new Error('empty string for api mapping key not allowed'); + } + + const apiMappingProps: CfnApiMappingProps = { + apiId: props.api.apiId, + domainName: props.domainName.name, + stage: props.stage?.stageName ?? props.api.defaultStage!.stageName, + apiMappingKey: props.apiMappingKey, + }; + + const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); + + // ensure the dependency on the provided stage + if (props.stage) { + this.node.addDependency(props.stage); + } + + // if stage not specified, we ensure the default stage is ready before we create the api mapping + if (!props.stage?.stageName && props.api.defaultStage) { + this.node.addDependency(props.api.defaultStage!); + } + + this.apiMappingId = resource.ref; + this.mappingKey = props.apiMappingKey; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts new file mode 100644 index 0000000000000..a0febf4183f5f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts @@ -0,0 +1,155 @@ +import * as crypto from 'crypto'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IIntegration, IRouteIntegrationConfig } from './integration'; +import { IStage } from './stage'; +/** + * Represents a API Gateway HTTP/WebSocket API + */ +export interface IApi { + /** + * The identifier of this API Gateway API. + * @attribute + */ + readonly apiId: string; + + /** + * The default endpoint for an API + * @attribute + */ + readonly apiEndpoint: string; + + /** + * The default stage for this API + */ + readonly defaultStage?: IStage; + + /** + * Return the given named metric for this Api Gateway + * + * @default - average over 5 minutes + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of client-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of server-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the amount of data processed in bytes. + * + * @default - sum over 5 minutes + */ + metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the total number API requests in a given period. + * + * @default - SampleCount over 5 minutes + */ + metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the time between when API Gateway relays a request to the backend + * and when it receives a response from the backend. + * + * @default - no statistic + */ + metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * The time between when API Gateway receives a request from a client + * and when it returns a response to the client. + * The latency includes the integration latency and other API Gateway overhead. + * + * @default - no statistic + */ + metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Add an integration + * @internal + */ + _addIntegration(scope: Construct, config: IRouteIntegrationConfig): IIntegration; +} + +/** + * Base class representing an API + */ +export abstract class ApiBase extends Resource implements IApi { + abstract readonly apiId: string; + abstract readonly apiEndpoint: string; + protected integrations: Record = {}; + + /** + * @internal + */ + abstract _addIntegration(scope: Construct, config: IRouteIntegrationConfig): IIntegration; + + /** + * @internal + */ + protected _getIntegrationConfigHash(scope: Construct, config: IRouteIntegrationConfig): string { + const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); + const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + return configHash; + } + + /** + * @internal + */ + protected _getSavedIntegration(configHash: string) { + return this.integrations[configHash]; + } + + /** + * @internal + */ + protected _saveIntegration(configHash: string, integration: IIntegration) { + this.integrations[configHash] = integration; + } + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ApiGateway', + metricName, + dimensions: { ApiId: this.apiId }, + ...props, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index eeb237a4e7f84..b0a0f1c0265eb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -1,3 +1,4 @@ +export * from './api'; export * from './integration'; export * from './route'; export * from './stage'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts index 7255607639468..175be75145ac2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts @@ -9,4 +9,10 @@ export interface IIntegration extends IResource { * @attribute */ readonly integrationId: string; +} + +/** + * Config representing route integration + */ +export interface IRouteIntegrationConfig { } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index b608a7a34ad97..88fe64b080391 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -1,4 +1,9 @@ -import { IResource } from '@aws-cdk/core'; +import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IApi } from './api'; +import { ApiMapping } from './api-mapping'; +import { IDomainName } from './domain-name'; /** * Represents a Stage. @@ -9,6 +14,80 @@ export interface IStage extends IResource { * @attribute */ readonly stageName: string; + + /** + * The URL to this stage. + */ + readonly url: string; + + /** + * Return the given named metric for this HTTP Api Gateway Stage + * + * @default - average over 5 minutes + */ + metric(metricName: string, props?: MetricOptions): Metric + + /** + * Metric for the number of client-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricClientError(props?: MetricOptions): Metric + + /** + * Metric for the number of server-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricServerError(props?: MetricOptions): Metric + + /** + * Metric for the amount of data processed in bytes. + * + * @default - sum over 5 minutes + */ + metricDataProcessed(props?: MetricOptions): Metric + + /** + * Metric for the total number API requests in a given period. + * + * @default - SampleCount over 5 minutes + */ + metricCount(props?: MetricOptions): Metric + + /** + * Metric for the time between when API Gateway relays a request to the backend + * and when it receives a response from the backend. + * + * @default - no statistic + */ + metricIntegrationLatency(props?: MetricOptions): Metric + + /** + * The time between when API Gateway receives a request from a client + * and when it returns a response to the client. + * The latency includes the integration latency and other API Gateway overhead. + * + * @default - no statistic + */ + metricLatency(props?: MetricOptions): Metric +} + +/** + * Options for DomainMapping + */ +export interface DomainMappingOptions { + /** + * The domain name for the mapping + * + */ + readonly domainName: IDomainName; + + /** + * The API mapping key. Leave it undefined for the root path mapping. + * @default - empty key for the root path mapping + */ + readonly mappingKey?: string; } /** @@ -16,15 +95,105 @@ export interface IStage extends IResource { * Options that are common between HTTP and Websocket APIs. */ export interface CommonStageOptions { - /** - * The name of the stage. See `StageName` class for more details. - * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. - */ - readonly stageName?: string; + /** * Whether updates to an API automatically trigger a new deployment. * @default false */ readonly autoDeploy?: boolean; + + /** + * The options for custom domain and api mapping + * + * @default - no custom domain and api mapping configuration + */ + readonly domainMapping?: DomainMappingOptions; +} + +/** + * The attributes used to import existing Stage + */ +export interface StageAttributes { + /** + * The name of the stage + */ + readonly stageName: string; + + /** + * The API to which this stage is associated + */ + readonly api: IApi; +} + +/** + * Base class representing a Stage + */ +export abstract class StageBase extends Resource implements IStage { + /** + * Import an existing stage into this CDK app. + */ + public static fromStageAttributes(scope: Construct, id: string, attrs: StageAttributes): IStage { + class Import extends StageBase implements IStage { + public readonly stageName = attrs.stageName; + protected readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } + } + return new Import(scope, id); + } + + public abstract readonly stageName: string; + protected abstract readonly api: IApi; + + /** + * The URL to this stage. + */ + abstract get url(): string; + + /** + * @internal + */ + protected _addDomainMapping(domainMapping: DomainMappingOptions) { + new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { + api: this.api, + domainName: domainMapping.domainName, + stage: this, + apiMappingKey: domainMapping.mappingKey, + }); + // ensure the dependency + this.node.addDependency(domainMapping.domainName); + } + + public metric(metricName: string, props?: MetricOptions): Metric { + return this.api.metric(metricName, props).with({ + dimensions: { ApiId: this.api.apiId, Stage: this.stageName }, + }).attachTo(this); + } + + public metricClientError(props?: MetricOptions): Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: MetricOptions): Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: MetricOptions): Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: MetricOptions): Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: MetricOptions): Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: MetricOptions): Metric { + return this.metric('Latency', props); + } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts deleted file mode 100644 index ee9323240833d..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Resource } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; -import { IApiMapping, IDomainName } from '../common'; -import { IHttpApi } from '../http/api'; -import { IHttpStage } from './stage'; - -/** - * Properties used to create the HttpApiMapping resource - */ -export interface HttpApiMappingProps { - /** - * Api mapping key. The path where this stage should be mapped to on the domain - * @default - undefined for the root path mapping. - */ - readonly apiMappingKey?: string; - - /** - * The HttpApi to which this mapping is applied - */ - readonly api: IHttpApi; - - /** - * custom domain name of the mapping target - */ - readonly domainName: IDomainName; - - /** - * stage for the HttpApiMapping resource - * - * @default - the $default stage - */ - readonly stage?: IHttpStage; -} - -/** - * The attributes used to import existing HttpApiMapping - */ -export interface HttpApiMappingAttributes { - /** - * The API mapping ID - */ - readonly apiMappingId: string; -} - -/** - * Create a new API mapping for API Gateway HTTP API endpoint. - * @resource AWS::ApiGatewayV2::ApiMapping - */ -export class HttpApiMapping extends Resource implements IApiMapping { - /** - * import from API ID - */ - public static fromHttpApiMappingAttributes(scope: Construct, id: string, attrs: HttpApiMappingAttributes): IApiMapping { - class Import extends Resource implements IApiMapping { - public readonly apiMappingId = attrs.apiMappingId; - } - return new Import(scope, id); - } - /** - * ID of the API Mapping - */ - public readonly apiMappingId: string; - - /** - * API Mapping key - */ - public readonly mappingKey?: string; - - constructor(scope: Construct, id: string, props: HttpApiMappingProps) { - super(scope, id); - - if ((!props.stage?.stageName) && !props.api.defaultStage) { - throw new Error('stage is required if default stage is not available'); - } - - const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; - if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { - throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); - } - - if (props.apiMappingKey === '') { - throw new Error('empty string for api mapping key not allowed'); - } - - const apiMappingProps: CfnApiMappingProps = { - apiId: props.api.httpApiId, - domainName: props.domainName.name, - stage: props.stage?.stageName ?? props.api.defaultStage!.stageName, - apiMappingKey: props.apiMappingKey, - }; - - const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); - - // ensure the dependency on the provided stage - if (props.stage) { - this.node.addDependency(props.stage); - } - - // if stage not specified, we ensure the default stage is ready before we create the api mapping - if (!props.stage?.stageName && props.api.defaultStage) { - this.node.addDependency(props.api.defaultStage!); - } - - this.apiMappingId = resource.ref; - this.mappingKey = props.apiMappingKey; - } - -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 52e5f1fccbe07..80295350a2c37 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,11 +1,10 @@ -import * as crypto from 'crypto'; -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; -import { DefaultDomainMappingOptions } from '../http/stage'; +import { ApiBase, IApi } from '../common/api'; +import { DomainMappingOptions, IStage } from '../common/stage'; import { IHttpRouteAuthorizer } from './authorizer'; -import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration'; +import { IHttpRouteIntegration, HttpIntegration, IHttpRouteIntegrationConfig } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; import { HttpStage, HttpStageOptions } from './stage'; import { VpcLink, VpcLinkProps } from './vpc-link'; @@ -13,76 +12,14 @@ import { VpcLink, VpcLinkProps } from './vpc-link'; /** * Represents an HTTP API */ -export interface IHttpApi extends IResource { +export interface IHttpApi extends IApi { /** * The identifier of this API Gateway HTTP API. * @attribute + * @deprecated - use apiId instead */ readonly httpApiId: string; - /** - * The default endpoint for an API - * @attribute - */ - readonly apiEndpoint: string; - - /** - * The default stage - */ - readonly defaultStage?: HttpStage; - - /** - * Return the given named metric for this HTTP Api Gateway - * - * @default - average over 5 minutes - */ - metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the number of client-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the number of server-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the amount of data processed in bytes. - * - * @default - sum over 5 minutes - */ - metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the total number API requests in a given period. - * - * @default - SampleCount over 5 minutes - */ - metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the time between when API Gateway relays a request to the backend - * and when it receives a response from the backend. - * - * @default - no statistic - */ - metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * The time between when API Gateway receives a request from a client - * and when it returns a response to the client. - * The latency includes the integration latency and other API Gateway overhead. - * - * @default - no statistic - */ - metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - /** * Add a new VpcLink */ @@ -92,7 +29,7 @@ export interface IHttpApi extends IResource { * Add a http integration * @internal */ - _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration; + _addIntegration(scope: Construct, config: IHttpRouteIntegrationConfig): HttpIntegration; } /** @@ -135,7 +72,7 @@ export interface HttpApiProps { * * @default - no default domain mapping configured. meaningless if `createDefaultStage` is `false`. */ - readonly defaultDomainMapping?: DefaultDomainMappingOptions; + readonly defaultDomainMapping?: DomainMappingOptions; /** * Specifies whether clients can invoke your API using the default endpoint. @@ -218,45 +155,12 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { readonly authorizationScopes?: string[]; } -abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported +abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that this is not exported + public abstract readonly apiId: string; public abstract readonly httpApiId: string; public abstract readonly apiEndpoint: string; private vpcLinks: Record = {}; - private httpIntegrations: Record = {}; - - public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/ApiGateway', - metricName, - dimensions: { ApiId: this.httpApiId }, - ...props, - }).attachTo(this); - } - - public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Latency', props); - } public addVpcLink(options: VpcLinkProps): VpcLink { const { vpcId } = options.vpc; @@ -274,12 +178,12 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t /** * @internal */ - public _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration { - const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); - const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + public _addIntegration(scope: Construct, config: IHttpRouteIntegrationConfig): HttpIntegration { + const configHash = this._getIntegrationConfigHash(scope, config); + const existingInteration = this._getSavedIntegration(configHash); - if (configHash in this.httpIntegrations) { - return this.httpIntegrations[configHash]; + if (existingInteration) { + return existingInteration as HttpIntegration; } const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, { @@ -291,7 +195,7 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, }); - this.httpIntegrations[configHash] = integration; + this._saveIntegration(configHash, integration); return integration; } @@ -322,6 +226,7 @@ export class HttpApi extends HttpApiBase { */ public static fromHttpApiAttributes(scope: Construct, id: string, attrs: HttpApiAttributes): IHttpApi { class Import extends HttpApiBase { + public readonly apiId = attrs.httpApiId; public readonly httpApiId = attrs.httpApiId; private readonly _apiEndpoint = attrs.apiEndpoint; @@ -339,6 +244,7 @@ export class HttpApi extends HttpApiBase { * A human friendly name for this HTTP API. Note that this is different from `httpApiId`. */ public readonly httpApiName?: string; + public readonly apiId: string; public readonly httpApiId: string; /** @@ -349,7 +255,7 @@ export class HttpApi extends HttpApiBase { /** * default stage of the api resource */ - public readonly defaultStage: HttpStage | undefined; + public readonly defaultStage: IStage | undefined; private readonly _apiEndpoint: string; @@ -392,6 +298,7 @@ export class HttpApi extends HttpApiBase { }; const resource = new CfnApi(this, 'Resource', apiProps); + this.apiId = resource.ref; this.httpApiId = resource.ref; this._apiEndpoint = resource.attrApiEndpoint; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index efd60f9f24d7c..81ddfec695bc3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -2,6 +2,5 @@ export * from './api'; export * from './route'; export * from './integration'; export * from './stage'; -export * from './api-mapping'; export * from './vpc-link'; export * from './authorizer'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index e609c9396c08f..96e69bd1d65ba 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,7 +1,7 @@ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; -import { IIntegration } from '../common'; +import { IIntegration, IRouteIntegrationConfig } from '../common'; import { IHttpApi } from './api'; import { HttpMethod, IHttpRoute } from './route'; @@ -171,13 +171,13 @@ export interface IHttpRouteIntegration { /** * Bind this integration to the route. */ - bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; + bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig; } /** * Config returned back as a result of the bind. */ -export interface HttpRouteIntegrationConfig { +export interface IHttpRouteIntegrationConfig extends IRouteIntegrationConfig { /** * Integration type. */ diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index fbe54345e25e3..1ce8fff09ffab 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -1,30 +1,22 @@ -import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; -import { Resource, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, IDomainName, IStage } from '../common'; +import { CommonStageOptions, StageBase } from '../common'; +import { IApi } from '../common/api'; import { IHttpApi } from './api'; -import { HttpApiMapping } from './api-mapping'; const DEFAULT_STAGE_NAME = '$default'; /** - * Represents the HttpStage - */ -export interface IHttpStage extends IStage { -} - -/** - * Options to create a new stage for an HTTP API. + * The options to create a new Stage for an HTTP API */ export interface HttpStageOptions extends CommonStageOptions { /** - * The options for custom domain and api mapping - * - * @default - no custom domain and api mapping configuration + * The name of the stage. See `StageName` class for more details. + * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. */ - readonly domainMapping?: DomainMappingOptions; + readonly stageName?: string; } /** @@ -37,52 +29,13 @@ export interface HttpStageProps extends HttpStageOptions { readonly httpApi: IHttpApi; } -/** - * Options for defaultDomainMapping - */ -export interface DefaultDomainMappingOptions { - /** - * The domain name for the mapping - * - */ - readonly domainName: IDomainName; - - /** - * The API mapping key. Leave it undefined for the root path mapping. - * @default - empty key for the root path mapping - */ - readonly mappingKey?: string; -} - -/** - * Options for DomainMapping - */ -export interface DomainMappingOptions extends DefaultDomainMappingOptions { - /** - * The API Stage - * - * @default - the $default stage - */ - readonly stage?: IStage; -} - /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage */ -export class HttpStage extends Resource implements IStage { - /** - * Import an existing stage into this CDK app. - */ - public static fromStageName(scope: Construct, id: string, stageName: string): IStage { - class Import extends Resource implements IStage { - public readonly stageName = stageName; - } - return new Import(scope, id); - } - +export class HttpStage extends StageBase { public readonly stageName: string; - private httpApi: IHttpApi; + protected readonly api: IApi; constructor(scope: Construct, id: string, props: HttpStageProps) { super(scope, id, { @@ -90,25 +43,17 @@ export class HttpStage extends Resource implements IStage { }); new CfnStage(this, 'Resource', { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, stageName: this.physicalName, autoDeploy: props.autoDeploy, }); this.stageName = this.physicalName; - this.httpApi = props.httpApi; + this.api = props.httpApi; if (props.domainMapping) { - new HttpApiMapping(this, `${props.domainMapping.domainName}${props.domainMapping.mappingKey}`, { - api: props.httpApi, - domainName: props.domainMapping.domainName, - stage: this, - apiMappingKey: props.domainMapping.mappingKey, - }); - // ensure the dependency - this.node.addDependency(props.domainMapping.domainName); + this._addDomainMapping(props.domainMapping); } - } /** @@ -117,75 +62,6 @@ export class HttpStage extends Resource implements IStage { public get url(): string { const s = Stack.of(this); const urlPath = this.stageName === DEFAULT_STAGE_NAME ? '' : this.stageName; - return `https://${this.httpApi.httpApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; - } - - /** - * Return the given named metric for this HTTP Api Gateway Stage - * - * @default - average over 5 minutes - */ - public metric(metricName: string, props?: MetricOptions): Metric { - var api = this.httpApi; - return api.metric(metricName, props).with({ - dimensions: { ApiId: this.httpApi.httpApiId, Stage: this.stageName }, - }).attachTo(this); - } - - /** - * Metric for the number of client-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - public metricClientError(props?: MetricOptions): Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the number of server-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - public metricServerError(props?: MetricOptions): Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the amount of data processed in bytes. - * - * @default - sum over 5 minutes - */ - public metricDataProcessed(props?: MetricOptions): Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the total number API requests in a given period. - * - * @default - SampleCount over 5 minutes - */ - public metricCount(props?: MetricOptions): Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - /** - * Metric for the time between when API Gateway relays a request to the backend - * and when it receives a response from the backend. - * - * @default - no statistic - */ - public metricIntegrationLatency(props?: MetricOptions): Metric { - return this.metric('IntegrationLatency', props); - } - - /** - * The time between when API Gateway receives a request from a client - * and when it returns a response to the client. - * The latency includes the integration latency and other API Gateway overhead. - * - * @default - no statistic - */ - public metricLatency(props?: MetricOptions): Metric { - return this.metric('Latency', props); + return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index 95703f4bc8ec0..64e63c0a3908a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -1,32 +1,20 @@ -import * as crypto from 'crypto'; -import { IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi } from '../apigatewayv2.generated'; -import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; +import { ApiBase, IApi } from '../common/api'; +import { DomainMappingOptions, IStage } from '../common/stage'; +import { IWebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; import { WebSocketRoute, WebSocketRouteOptions } from './route'; import { WebSocketStage } from './stage'; /** * Represents a WebSocket API */ -export interface IWebSocketApi extends IResource { - /** - * The identifier of this API Gateway WebSocket API. - * @attribute - */ - readonly webSocketApiId: string; - - /** - * The default endpoint for an API - * @attribute - */ - readonly apiEndpoint: string; - +export interface IWebSocketApi extends IApi { /** * Add a websocket integration * @internal */ - _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration + _addIntegration(scope: Construct, config: IWebSocketRouteIntegrationConfig): WebSocketIntegration } /** @@ -56,14 +44,42 @@ export interface WebSocketApiProps { * @default - No default stage is created */ readonly defaultStageName?: string; + + /** + * Configure a custom domain with the API mapping resource to the WebSocket API + * + * @default - no default domain mapping configured. meaningless if `defaultStageName` is not provided. + */ + readonly defaultDomainMapping?: DomainMappingOptions; + + /** + * Options to configure a '$connect' route + * + * @default - no '$connect' route configured + */ + readonly connectRouteOptions?: WebSocketRouteOptions; + + /** + * Options to configure a '$disconnect' route + * + * @default - no '$disconnect' route configured + */ + readonly disconnectRouteOptions?: WebSocketRouteOptions; + + /** + * Options to configure a '$default' route + * + * @default - no '$default' route configured + */ + readonly defaultRouteOptions?: WebSocketRouteOptions; } /** * Create a new API Gateway WebSocket API endpoint. * @resource AWS::ApiGatewayV2::Api */ -export class WebSocketApi extends Resource implements IWebSocketApi { - public readonly webSocketApiId: string; +export class WebSocketApi extends ApiBase implements IWebSocketApi { + public readonly apiId: string; public readonly apiEndpoint: string; /** @@ -74,9 +90,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { /** * default stage of the api resource */ - public readonly defaultStage: WebSocketStage | undefined; - - private integrations: Record = {}; + public readonly defaultStage: IStage | undefined; constructor(scope: Construct, id: string, props?: WebSocketApiProps) { super(scope, id); @@ -89,7 +103,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { description: props?.description, routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', }); - this.webSocketApiId = resource.ref; + this.apiId = resource.ref; this.apiEndpoint = resource.attrApiEndpoint; if (props?.defaultStageName) { @@ -97,19 +111,35 @@ export class WebSocketApi extends Resource implements IWebSocketApi { webSocketApi: this, stageName: props.defaultStageName, autoDeploy: true, + domainMapping: props?.defaultDomainMapping, }); } + + if (!props?.defaultStageName && props?.defaultDomainMapping) { + throw new Error('defaultDomainMapping not supported when defaultStageName is not provided', + ); + } + + if (props?.connectRouteOptions) { + this.addRoute('$connect', props.connectRouteOptions); + } + if (props?.disconnectRouteOptions) { + this.addRoute('$disconnect', props.disconnectRouteOptions); + } + if (props?.defaultRouteOptions) { + this.addRoute('$default', props.defaultRouteOptions); + } } /** * @internal */ - public _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration { - const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); - const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + public _addIntegration(scope: Construct, config: IWebSocketRouteIntegrationConfig): WebSocketIntegration { + const configHash = this._getIntegrationConfigHash(scope, config); + const existingIntegration = this._getSavedIntegration(configHash); - if (configHash in this.integrations) { - return this.integrations[configHash]; + if (existingIntegration) { + return existingIntegration as WebSocketIntegration; } const integration = new WebSocketIntegration(scope, `WebSocketIntegration-${configHash}`, { @@ -117,7 +147,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { integrationType: config.type, integrationUri: config.uri, }); - this.integrations[configHash] = integration; + this._saveIntegration(configHash, integration); return integration; } @@ -132,25 +162,4 @@ export class WebSocketApi extends Resource implements IWebSocketApi { ...options, }); } - - /** - * Add a connect route - */ - public addConnectRoute(options: WebSocketRouteOptions) { - return this.addRoute('$connect', options); - } - - /** - * Add a disconnect route - */ - public addDisconnectRoute(options: WebSocketRouteOptions) { - return this.addRoute('$disconnect', options); - } - - /** - * Add a default route - */ - public addDefaultRoute(options: WebSocketRouteOptions) { - return this.addRoute('$default', options); - } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts index 25da1d7823d75..a8eaf83439641 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts @@ -1,7 +1,7 @@ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; -import { IIntegration } from '../common'; +import { IIntegration, IRouteIntegrationConfig } from '../common'; import { IWebSocketApi } from './api'; import { IWebSocketRoute } from './route'; @@ -58,7 +58,7 @@ export class WebSocketIntegration extends Resource implements IWebSocketIntegrat constructor(scope: Construct, id: string, props: WebSocketIntegrationProps) { super(scope, id); const integ = new CfnIntegration(this, 'Resource', { - apiId: props.webSocketApi.webSocketApiId, + apiId: props.webSocketApi.apiId, integrationType: props.integrationType, integrationUri: props.integrationUri, }); @@ -91,13 +91,13 @@ export interface IWebSocketRouteIntegration { /** * Bind this integration to the route. */ - bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig; + bind(options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig; } /** * Config returned back as a result of the bind. */ -export interface WebSocketRouteIntegrationConfig { +export interface IWebSocketRouteIntegrationConfig extends IRouteIntegrationConfig { /** * Integration type. */ diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts index 4982962a10f5e..0588889a603bc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -75,7 +75,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { const integration = props.webSocketApi._addIntegration(this, config); const route = new CfnRoute(this, 'Resource', { - apiId: props.webSocketApi.webSocketApiId, + apiId: props.webSocketApi.apiId, routeKey: props.routeKey, target: `integrations/${integration.integrationId}`, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index 5d33cde32cdcc..ce906f01eee0c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -1,21 +1,14 @@ -import { Resource, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { IStage } from '../common'; +import { CommonStageOptions, StageBase } from '../common'; +import { IApi } from '../common/api'; import { IWebSocketApi } from './api'; -const DEFAULT_STAGE_NAME = 'dev'; - -/** - * Represents the WebSocketStage - */ -export interface IWebSocketStage extends IStage { -} - /** * Properties to initialize an instance of `WebSocketStage`. */ -export interface WebSocketStageProps { +export interface WebSocketStageProps extends CommonStageOptions { /** * The WebSocket API to which this stage is associated. */ @@ -25,35 +18,33 @@ export interface WebSocketStageProps { * The name of the stage. */ readonly stageName: string; - - /** - * Whether updates to an API automatically trigger a new deployment. - * @default false - */ - readonly autoDeploy?: boolean; } /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage */ -export class WebSocketStage extends Resource implements IWebSocketStage { +export class WebSocketStage extends StageBase { public readonly stageName: string; - private webSocketApi: IWebSocketApi; + protected readonly api: IApi; constructor(scope: Construct, id: string, props: WebSocketStageProps) { super(scope, id, { - physicalName: props.stageName ? props.stageName : DEFAULT_STAGE_NAME, + physicalName: props.stageName, }); - this.webSocketApi = props.webSocketApi; + this.api = props.webSocketApi; this.stageName = this.physicalName; new CfnStage(this, 'Resource', { - apiId: props.webSocketApi.webSocketApiId, + apiId: props.webSocketApi.apiId, stageName: this.physicalName, autoDeploy: props.autoDeploy, }); + + if (props.domainMapping) { + this._addDomainMapping(props.domainMapping); + } } /** @@ -62,6 +53,6 @@ export class WebSocketStage extends Resource implements IWebSocketStage { public get url(): string { const s = Stack.of(this); const urlPath = this.stageName; - return `wss://${this.webSocketApi.webSocketApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; + return `wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index e6aa7a18b5365..15af89b308a41 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -103,11 +103,15 @@ }, "awslint": { "exclude": [ + "props-physical-name:@aws-cdk/aws-apigatewayv2.ApiMappingProps", + "construct-interface-extends-iconstruct:@aws-cdk/aws-apigatewayv2.IHttpApi", + "construct-interface-extends-iconstruct:@aws-cdk/aws-apigatewayv2.IWebSocketApi", + "resource-interface-extends-resource:@aws-cdk/aws-apigatewayv2.IHttpApi", + "resource-interface-extends-resource:@aws-cdk/aws-apigatewayv2.IWebSocketApi", "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", "from-method:@aws-cdk/aws-apigatewayv2.HttpStage", "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", "from-method:@aws-cdk/aws-apigatewayv2.WebSocketApi", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts similarity index 89% rename from packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index fe113727c3f50..c712c5c8d984a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { Stack } from '@aws-cdk/core'; -import { DomainName, HttpApi, HttpApiMapping } from '../../lib'; +import { DomainName, HttpApi, ApiMapping } from '../../lib'; const domainName = 'example.com'; const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; @@ -17,7 +17,7 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, }); @@ -47,7 +47,7 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, stage: beta, @@ -75,7 +75,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '', @@ -94,7 +94,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '/', @@ -113,7 +113,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '/foo', @@ -132,7 +132,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo/bar', @@ -151,7 +151,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo/', @@ -170,7 +170,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '^foo', @@ -189,7 +189,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo.*$', @@ -207,12 +207,12 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - const mapping = new HttpApiMapping(stack, 'Mapping', { + const mapping = new ApiMapping(stack, 'Mapping', { api, domainName: dn, }); - const imported = HttpApiMapping.fromHttpApiMappingAttributes(stack, 'ImportedMapping', { + const imported = ApiMapping.fromApiMappingAttributes(stack, 'ImportedMapping', { apiMappingId: mapping.apiMappingId, } ); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 01252be7d84f1..df7141df24d30 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -5,7 +5,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, - HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; describe('HttpApi', () => { @@ -19,7 +19,7 @@ describe('HttpApi', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.httpApiId), + ApiId: stack.resolve(api.apiId), StageName: '$default', AutoDeploy: true, }); @@ -34,7 +34,7 @@ describe('HttpApi', () => { const stack = new Stack(); const imported = HttpApi.fromHttpApiAttributes(stack, 'imported', { httpApiId: 'http-1234', apiEndpoint: 'api-endpoint' }); - expect(imported.httpApiId).toEqual('http-1234'); + expect(imported.apiId).toEqual('http-1234'); expect(imported.apiEndpoint).toEqual('api-endpoint'); }); @@ -55,12 +55,12 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: '$default', }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), }); }); @@ -75,12 +75,12 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'GET /pets', }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'PATCH /pets', }); }); @@ -95,7 +95,7 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'ANY /pets', }); }); @@ -149,7 +149,7 @@ describe('HttpApi', () => { }); const metricName = '4xxError'; const statistic = 'Sum'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const countMetric = api.metric(metricName, { statistic }); @@ -168,7 +168,7 @@ describe('HttpApi', () => { createDefaultStage: false, }); const color = '#00ff00'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const metrics = new Array(); @@ -374,7 +374,7 @@ describe('HttpApi', () => { }); class DummyRouteIntegration implements IHttpRouteIntegration { - public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { return { payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, type: HttpIntegrationType.HTTP_PROXY, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index a5ce1b7b64b64..3b7cf43f166d7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { Stack, App } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteAuthorizerBindOptions, - HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpRouteAuthorizerConfig, IHttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; describe('HttpRoute', () => { @@ -164,7 +164,7 @@ describe('HttpRoute', () => { const httpApi = new HttpApi(stack, 'HttpApi'); class PrivateIntegration implements IHttpRouteIntegration { - public bind(): HttpRouteIntegrationConfig { + public bind(): IHttpRouteIntegrationConfig { return { method: HttpMethod.ANY, payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, @@ -244,7 +244,7 @@ describe('HttpRoute', () => { }); class DummyIntegration implements IHttpRouteIntegration { - public bind(): HttpRouteIntegrationConfig { + public bind(): IHttpRouteIntegrationConfig { return { type: HttpIntegrationType.HTTP_PROXY, payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index 6c4359b5439c9..aea7dd7b57b29 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -31,7 +31,10 @@ describe('HttpStage', () => { httpApi: api, }); - const imported = HttpStage.fromStageName(stack, 'Import', stage.stageName ); + const imported = HttpStage.fromStageAttributes(stack, 'Import', { + stageName: stage.stageName, + api, + }); expect(imported.stageName).toEqual(stage.stageName); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts index e0007c440d3ae..0e1995f67a076 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, - WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, + WebSocketRouteIntegrationBindOptions, IWebSocketRouteIntegrationConfig, } from '../../lib'; describe('WebSocketApi', () => { @@ -35,7 +35,7 @@ describe('WebSocketApi', () => { // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.webSocketApiId), + ApiId: stack.resolve(api.apiId), StageName: 'dev', AutoDeploy: true, }); @@ -51,59 +51,56 @@ describe('WebSocketApi', () => { // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(api.webSocketApiId), + ApiId: stack.resolve(api.apiId), RouteKey: 'myroute', }); }); - test('addConnectRoute: adds a $connect route', () => { + test('connectRouteOptions: adds a $connect route', () => { // GIVEN const stack = new Stack(); - const api = new WebSocketApi(stack, 'api'); - - // WHEN - api.addConnectRoute({ integration: new DummyIntegration() }); + const api = new WebSocketApi(stack, 'api', { + connectRouteOptions: { integration: new DummyIntegration() }, + }); // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(api.webSocketApiId), + ApiId: stack.resolve(api.apiId), RouteKey: '$connect', }); }); - test('addDisconnectRoute: adds a $disconnect route', () => { + test('disconnectRouteOptions: adds a $disconnect route', () => { // GIVEN const stack = new Stack(); - const api = new WebSocketApi(stack, 'api'); - - // WHEN - api.addDisconnectRoute({ integration: new DummyIntegration() }); + const api = new WebSocketApi(stack, 'api', { + disconnectRouteOptions: { integration: new DummyIntegration() }, + }); // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(api.webSocketApiId), + ApiId: stack.resolve(api.apiId), RouteKey: '$disconnect', }); }); - test('addDefaultRoute: adds a $default route', () => { + test('defaultRouteOptions: adds a $default route', () => { // GIVEN const stack = new Stack(); - const api = new WebSocketApi(stack, 'api'); - - // WHEN - api.addDefaultRoute({ integration: new DummyIntegration() }); + const api = new WebSocketApi(stack, 'api', { + defaultRouteOptions: { integration: new DummyIntegration() }, + }); // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(api.webSocketApiId), + ApiId: stack.resolve(api.apiId), RouteKey: '$default', }); }); }); class DummyIntegration implements IWebSocketRouteIntegration { - bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + bind(_options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig { return { type: WebSocketIntegrationType.AWS_PROXY, uri: 'some-uri', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts index af0183f3097d9..a498cd6854bad 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, - WebSocketRoute, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, + WebSocketRoute, WebSocketRouteIntegrationBindOptions, IWebSocketRouteIntegrationConfig, } from '../../lib'; describe('WebSocketRoute', () => { @@ -20,7 +20,7 @@ describe('WebSocketRoute', () => { // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(webSocketApi.webSocketApiId), + ApiId: stack.resolve(webSocketApi.apiId), RouteKey: 'message', Target: { 'Fn::Join': [ @@ -28,7 +28,7 @@ describe('WebSocketRoute', () => { [ 'integrations/', { - Ref: 'ApiWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f23D15DECE', + Ref: 'RouteWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f20338431E', }, ], ], @@ -36,7 +36,7 @@ describe('WebSocketRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(webSocketApi.webSocketApiId), + ApiId: stack.resolve(webSocketApi.apiId), IntegrationType: 'AWS_PROXY', IntegrationUri: 'some-uri', }); @@ -45,7 +45,7 @@ describe('WebSocketRoute', () => { class DummyIntegration implements IWebSocketRouteIntegration { - bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + bind(_options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig { return { type: WebSocketIntegrationType.AWS_PROXY, uri: 'some-uri', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts index bb9ba441c5c67..c112cb368ae1b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -16,7 +16,7 @@ describe('WebSocketStage', () => { // THEN expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.webSocketApiId), + ApiId: stack.resolve(api.apiId), StageName: 'dev', }); expect(defaultStage.url.endsWith('/dev')).toBe(true); From ea5a32f245acb583c6353ebff2eaa38de645b93f Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 19 Feb 2021 15:11:01 +0530 Subject: [PATCH 05/13] update readme --- packages/@aws-cdk/aws-apigatewayv2-integrations/README.md | 6 +++--- .../@aws-cdk/aws-apigatewayv2/lib/common/integration.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index 59546a4c9c086..b3c0afcd45db2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -159,15 +159,15 @@ Lambda integrations enable integrating a WebSocket API route with a Lambda funct The API Gateway service will invoke the lambda function with an event payload of a specific format. -The following code configures a `$connect` route with a Lambda integration +The following code configures a `sendmessage` route with a Lambda integration ```ts const webSocketApi = new WebSocketApi(stack, 'mywsapi', { defaultStageName: 'dev', }); -const connectHandler = new lambda.Function(stack, 'ConnectHandler', {...}); -webSocketApi.addConnectRoute({ +const messageHandler = new lambda.Function(stack, 'MessageHandler', {...}); +webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: connectHandler, }), diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts index 175be75145ac2..9eda613007eca 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts @@ -15,4 +15,4 @@ export interface IIntegration extends IResource { * Config representing route integration */ export interface IRouteIntegrationConfig { -} \ No newline at end of file +} From 3369f85e8be2ab3602a766cfbf3a04d60e55947f Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 19 Feb 2021 15:16:03 +0530 Subject: [PATCH 06/13] update --- .../test/websocket/integ.lambda.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts index e13c2da741676..2be3f892c8662 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -5,10 +5,10 @@ import { LambdaWebSocketIntegration } from '../../lib'; /* * Stack verification steps: - * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch - * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch - * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch - * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch + * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch console + * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch console + * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch console + * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch console */ const app = new App(); From fe7a65821c1bfa59be5d5611e3db33cf5889bc6c Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 19 Feb 2021 15:18:17 +0530 Subject: [PATCH 07/13] update --- .../test/websocket/integ.lambda.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts index 2be3f892c8662..e13c2da741676 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -5,10 +5,10 @@ import { LambdaWebSocketIntegration } from '../../lib'; /* * Stack verification steps: - * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch console - * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch console - * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch console - * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch console + * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch + * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch */ const app = new App(); From 0ac9d480afaba736863044bc59e5d25752ab6f6b Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 19 Feb 2021 21:41:51 +0530 Subject: [PATCH 08/13] update readme --- packages/@aws-cdk/aws-apigatewayv2/README.md | 6 ------ packages/@aws-cdk/aws-apigatewayv2/package.json | 3 --- 2 files changed, 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index a4ca9622b19f8..868b7ebc5a6d2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -287,10 +287,6 @@ lifecycle, from creation through monitoring your production APIs. [Read more](ht WebSocket APIs have two fundamental concepts - Routes and Integrations. -```ts -addRoute -``` - WebSocket APIs direct JSON messages to backend integrations based on configured routes. (Non-JSON messages are directed to the configured `$default` route.) @@ -322,5 +318,3 @@ webSocketApi.addRoute('sendmessage', { }), }); ``` - - diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 15af89b308a41..97b7e76544f01 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -110,14 +110,11 @@ "resource-interface-extends-resource:@aws-cdk/aws-apigatewayv2.IWebSocketApi", "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", - "from-method:@aws-cdk/aws-apigatewayv2.HttpStage", - "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", "from-method:@aws-cdk/aws-apigatewayv2.WebSocketApi", "from-method:@aws-cdk/aws-apigatewayv2.WebSocketIntegration", "from-method:@aws-cdk/aws-apigatewayv2.WebSocketRoute", - "from-method:@aws-cdk/aws-apigatewayv2.WebSocketStage", "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketRouteProps" ] From a066643b7f9f1aa47e49883962676b59f8616027 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Sat, 27 Feb 2021 11:49:08 +0530 Subject: [PATCH 09/13] address comments --- .../lib/http/alb.ts | 4 +- .../lib/http/http-proxy.ts | 4 +- .../lib/http/lambda.ts | 4 +- .../lib/http/nlb.ts | 4 +- .../lib/http/private/integration.ts | 4 +- .../lib/http/service-discovery.ts | 4 +- .../lib/websocket/lambda.ts | 4 +- .../test/http/private/integration.test.ts | 4 +- .../aws-apigatewayv2/lib/common/api.ts | 81 +----------------- .../aws-apigatewayv2/lib/common/base/api.ts | 50 +++++++++++ .../aws-apigatewayv2/lib/common/base/index.ts | 2 + .../aws-apigatewayv2/lib/common/base/stage.ts | 63 ++++++++++++++ .../lib/common/integration.ts | 6 -- .../lib/common/private/integration-cache.ts | 32 ++++++++ .../aws-apigatewayv2/lib/common/stage.ts | 82 ++----------------- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 19 ++--- .../aws-apigatewayv2/lib/http/integration.ts | 6 +- .../aws-apigatewayv2/lib/http/stage.ts | 35 +++++++- .../aws-apigatewayv2/lib/websocket/api.ts | 15 ++-- .../lib/websocket/integration.ts | 6 +- .../aws-apigatewayv2/lib/websocket/stage.ts | 34 +++++++- .../aws-apigatewayv2/test/http/api.test.ts | 4 +- .../aws-apigatewayv2/test/http/route.test.ts | 6 +- .../aws-apigatewayv2/test/http/stage.test.ts | 2 +- .../test/websocket/api.test.ts | 4 +- .../test/websocket/route.test.ts | 4 +- .../test/websocket/stage.test.ts | 20 +++++ 27 files changed, 285 insertions(+), 218 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts index 0f8563abdfe0d..b6afd1cc76450 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts @@ -1,4 +1,4 @@ -import { HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { HttpPrivateIntegrationOptions } from './base-types'; @@ -22,7 +22,7 @@ export class HttpAlbIntegration extends HttpPrivateIntegration { super(); } - public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { let vpc: ec2.IVpc | undefined = this.props.vpcLink?.vpc; if (!vpc && (this.props.listener instanceof elbv2.ApplicationListener)) { vpc = this.props.listener.loadBalancer.vpc; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts index a55affd6a486e..a7ef2d1b4d7b9 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts @@ -1,7 +1,7 @@ import { HttpIntegrationType, HttpRouteIntegrationBindOptions, - IHttpRouteIntegrationConfig, + HttpRouteIntegrationConfig, HttpMethod, IHttpRouteIntegration, PayloadFormatVersion, @@ -30,7 +30,7 @@ export class HttpProxyIntegration implements IHttpRouteIntegration { constructor(private readonly props: HttpProxyIntegrationProps) { } - public bind(_: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { return { method: this.props.method ?? HttpMethod.ANY, payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index 20bb659a99eb8..a962b268d7165 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -1,7 +1,7 @@ import { HttpIntegrationType, HttpRouteIntegrationBindOptions, - IHttpRouteIntegrationConfig, + HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion, } from '@aws-cdk/aws-apigatewayv2'; @@ -34,7 +34,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { constructor(private readonly props: LambdaProxyIntegrationProps) { } - public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { const route = options.route; this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { scope: options.scope, diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts index 89b169fb2e52c..85e3f3773d1c4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts @@ -1,4 +1,4 @@ -import { HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { HttpPrivateIntegrationOptions } from './base-types'; @@ -22,7 +22,7 @@ export class HttpNlbIntegration extends HttpPrivateIntegration { super(); } - public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { let vpc: ec2.IVpc | undefined = this.props.vpcLink?.vpc; if (!vpc && (this.props.listener instanceof elbv2.NetworkListener)) { vpc = this.props.listener.loadBalancer.vpc; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts index e845ac8b65dec..6d32b22794722 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts @@ -2,7 +2,7 @@ import { HttpConnectionType, HttpIntegrationType, HttpRouteIntegrationBindOptions, - IHttpRouteIntegrationConfig, + HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion, HttpMethod, @@ -61,5 +61,5 @@ export abstract class HttpPrivateIntegration implements IHttpRouteIntegration { return vpcLink; } - public abstract bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig; + public abstract bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts index a19f33541e8a8..44e8b148754dd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts @@ -1,4 +1,4 @@ -import { HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; import { HttpPrivateIntegrationOptions } from './base-types'; import { HttpPrivateIntegration } from './private/integration'; @@ -21,7 +21,7 @@ export class HttpServiceDiscoveryIntegration extends HttpPrivateIntegration { super(); } - public bind(_: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { if (!this.props.vpcLink) { throw new Error('The vpcLink property is mandatory'); } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts index b5d9a0a090f32..85e199a71c3d7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts @@ -2,7 +2,7 @@ import { IWebSocketRouteIntegration, WebSocketIntegrationType, WebSocketRouteIntegrationBindOptions, - IWebSocketRouteIntegrationConfig, + WebSocketRouteIntegrationConfig, } from '@aws-cdk/aws-apigatewayv2'; import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; @@ -24,7 +24,7 @@ export interface LambdaWebSocketIntegrationProps { export class LambdaWebSocketIntegration implements IWebSocketRouteIntegration { constructor(private props: LambdaWebSocketIntegrationProps) {} - bind(options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig { + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { const route = options.route; this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { scope: options.scope, diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts index 86462ed579cf1..ce5f269f648a0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts @@ -1,5 +1,5 @@ import '@aws-cdk/assert/jest'; -import { HttpApi, HttpRoute, HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpRoute, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; import { Stack } from '@aws-cdk/core'; import { HttpPrivateIntegration } from '../../../lib/http/private/integration'; @@ -12,7 +12,7 @@ describe('HttpPrivateIntegration', () => { super(); } - public bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { const vpcLink = this._configureVpcLink(options, {}); return { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts index a0febf4183f5f..0463ad5274b54 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts @@ -1,9 +1,6 @@ -import * as crypto from 'crypto'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Resource, Stack } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import { IIntegration, IRouteIntegrationConfig } from './integration'; import { IStage } from './stage'; + /** * Represents a API Gateway HTTP/WebSocket API */ @@ -76,80 +73,4 @@ export interface IApi { * @default - no statistic */ metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Add an integration - * @internal - */ - _addIntegration(scope: Construct, config: IRouteIntegrationConfig): IIntegration; -} - -/** - * Base class representing an API - */ -export abstract class ApiBase extends Resource implements IApi { - abstract readonly apiId: string; - abstract readonly apiEndpoint: string; - protected integrations: Record = {}; - - /** - * @internal - */ - abstract _addIntegration(scope: Construct, config: IRouteIntegrationConfig): IIntegration; - - /** - * @internal - */ - protected _getIntegrationConfigHash(scope: Construct, config: IRouteIntegrationConfig): string { - const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); - const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); - return configHash; - } - - /** - * @internal - */ - protected _getSavedIntegration(configHash: string) { - return this.integrations[configHash]; - } - - /** - * @internal - */ - protected _saveIntegration(configHash: string, integration: IIntegration) { - this.integrations[configHash] = integration; - } - - public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/ApiGateway', - metricName, - dimensions: { ApiId: this.apiId }, - ...props, - }).attachTo(this); - } - - public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Latency', props); - } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts new file mode 100644 index 0000000000000..8b98eb2a2de76 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts @@ -0,0 +1,50 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Resource } from '@aws-cdk/core'; +import { IApi } from '../api'; +import { IntegrationCache } from '../private/integration-cache'; + +/** + * Base class representing an API + * @internal + */ +export abstract class ApiBase extends Resource implements IApi { + abstract readonly apiId: string; + abstract readonly apiEndpoint: string; + /** + * @internal + */ + protected _integrationCache: IntegrationCache = new IntegrationCache(); + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ApiGateway', + metricName, + dimensions: { ApiId: this.apiId }, + ...props, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts new file mode 100644 index 0000000000000..b34869c30f4d7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts @@ -0,0 +1,2 @@ +export * from './api'; +export * from './stage'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts new file mode 100644 index 0000000000000..16cb1c9f0c330 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts @@ -0,0 +1,63 @@ +import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; +import { Resource } from '@aws-cdk/core'; +import { IApi } from '../api'; +import { ApiMapping } from '../api-mapping'; +import { DomainMappingOptions, IStage } from '../stage'; + +/** + * Base class representing a Stage + * @internal + */ +export abstract class StageBase extends Resource implements IStage { + public abstract readonly stageName: string; + public abstract readonly api: IApi; + + /** + * The URL to this stage. + */ + abstract get url(): string; + + /** + * @internal + */ + protected _addDomainMapping(domainMapping: DomainMappingOptions) { + new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { + api: this.api, + domainName: domainMapping.domainName, + stage: this, + apiMappingKey: domainMapping.mappingKey, + }); + // ensure the dependency + this.node.addDependency(domainMapping.domainName); + } + + public metric(metricName: string, props?: MetricOptions): Metric { + return this.api.metric(metricName, props).with({ + dimensions: { ApiId: this.api.apiId, Stage: this.stageName }, + }).attachTo(this); + } + + public metricClientError(props?: MetricOptions): Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: MetricOptions): Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: MetricOptions): Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: MetricOptions): Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: MetricOptions): Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: MetricOptions): Metric { + return this.metric('Latency', props); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts index 9eda613007eca..83e200aadb007 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts @@ -10,9 +10,3 @@ export interface IIntegration extends IResource { */ readonly integrationId: string; } - -/** - * Config representing route integration - */ -export interface IRouteIntegrationConfig { -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts new file mode 100644 index 0000000000000..b4d124e82f7f6 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts @@ -0,0 +1,32 @@ +import * as crypto from 'crypto'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { HttpRouteIntegrationConfig } from '../../http'; +import { WebSocketRouteIntegrationConfig } from '../../websocket'; +import { IIntegration } from '../integration'; + +type IntegrationConfig = HttpRouteIntegrationConfig | WebSocketRouteIntegrationConfig; + +/** + * @internal + */ +export class IntegrationCache { + private integrations: Record = {}; + + getSavedIntegration(scope: Construct, config: IntegrationConfig) { + const configHash = this.getIntegrationConfigHash(scope, config); + const integration = this.integrations[configHash]; + return { configHash, integration }; + } + + saveIntegration(scope: Construct, config: IntegrationConfig, integration: IIntegration) { + const configHash = this.getIntegrationConfigHash(scope, config); + this.integrations[configHash] = integration; + } + + private getIntegrationConfigHash(scope: Construct, config: IntegrationConfig): string { + const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); + const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + return configHash; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index 88fe64b080391..a385af8656658 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -1,8 +1,6 @@ import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; -import { IResource, Resource } from '@aws-cdk/core'; -import { Construct } from 'constructs'; +import { IResource } from '@aws-cdk/core'; import { IApi } from './api'; -import { ApiMapping } from './api-mapping'; import { IDomainName } from './domain-name'; /** @@ -15,6 +13,12 @@ export interface IStage extends IResource { */ readonly stageName: string; + /** + * The API this stage is associated to. + * @attribute + */ + readonly api: IApi; + /** * The URL to this stage. */ @@ -125,75 +129,3 @@ export interface StageAttributes { */ readonly api: IApi; } - -/** - * Base class representing a Stage - */ -export abstract class StageBase extends Resource implements IStage { - /** - * Import an existing stage into this CDK app. - */ - public static fromStageAttributes(scope: Construct, id: string, attrs: StageAttributes): IStage { - class Import extends StageBase implements IStage { - public readonly stageName = attrs.stageName; - protected readonly api = attrs.api; - - get url(): string { - throw new Error('url is not available for imported stages.'); - } - } - return new Import(scope, id); - } - - public abstract readonly stageName: string; - protected abstract readonly api: IApi; - - /** - * The URL to this stage. - */ - abstract get url(): string; - - /** - * @internal - */ - protected _addDomainMapping(domainMapping: DomainMappingOptions) { - new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { - api: this.api, - domainName: domainMapping.domainName, - stage: this, - apiMappingKey: domainMapping.mappingKey, - }); - // ensure the dependency - this.node.addDependency(domainMapping.domainName); - } - - public metric(metricName: string, props?: MetricOptions): Metric { - return this.api.metric(metricName, props).with({ - dimensions: { ApiId: this.api.apiId, Stage: this.stageName }, - }).attachTo(this); - } - - public metricClientError(props?: MetricOptions): Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: MetricOptions): Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: MetricOptions): Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: MetricOptions): Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: MetricOptions): Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: MetricOptions): Metric { - return this.metric('Latency', props); - } -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 80295350a2c37..ac673ae2eeb7c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,10 +1,11 @@ import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; -import { ApiBase, IApi } from '../common/api'; +import { IApi } from '../common/api'; +import { ApiBase } from '../common/base'; import { DomainMappingOptions, IStage } from '../common/stage'; import { IHttpRouteAuthorizer } from './authorizer'; -import { IHttpRouteIntegration, HttpIntegration, IHttpRouteIntegrationConfig } from './integration'; +import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; import { HttpStage, HttpStageOptions } from './stage'; import { VpcLink, VpcLinkProps } from './vpc-link'; @@ -29,7 +30,7 @@ export interface IHttpApi extends IApi { * Add a http integration * @internal */ - _addIntegration(scope: Construct, config: IHttpRouteIntegrationConfig): HttpIntegration; + _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration; } /** @@ -178,12 +179,10 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th /** * @internal */ - public _addIntegration(scope: Construct, config: IHttpRouteIntegrationConfig): HttpIntegration { - const configHash = this._getIntegrationConfigHash(scope, config); - const existingInteration = this._getSavedIntegration(configHash); - - if (existingInteration) { - return existingInteration as HttpIntegration; + public _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration { + const { configHash, integration: existingIntegration } = this._integrationCache.getSavedIntegration(scope, config); + if (existingIntegration) { + return existingIntegration as HttpIntegration; } const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, { @@ -195,7 +194,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, }); - this._saveIntegration(configHash, integration); + this._integrationCache.saveIntegration(scope, config, integration); return integration; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index 96e69bd1d65ba..e609c9396c08f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,7 +1,7 @@ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; -import { IIntegration, IRouteIntegrationConfig } from '../common'; +import { IIntegration } from '../common'; import { IHttpApi } from './api'; import { HttpMethod, IHttpRoute } from './route'; @@ -171,13 +171,13 @@ export interface IHttpRouteIntegration { /** * Bind this integration to the route. */ - bind(options: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig; + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; } /** * Config returned back as a result of the bind. */ -export interface IHttpRouteIntegrationConfig extends IRouteIntegrationConfig { +export interface HttpRouteIntegrationConfig { /** * Integration type. */ diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index 1ce8fff09ffab..370d185b4086c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -1,13 +1,19 @@ import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, StageBase } from '../common'; +import { CommonStageOptions, IStage, StageAttributes } from '../common'; import { IApi } from '../common/api'; +import { StageBase } from '../common/base'; import { IHttpApi } from './api'; - const DEFAULT_STAGE_NAME = '$default'; +/** + * Represents the HttpStage + */ +export interface IHttpStage extends IStage { +} + /** * The options to create a new Stage for an HTTP API */ @@ -29,13 +35,34 @@ export interface HttpStageProps extends HttpStageOptions { readonly httpApi: IHttpApi; } +/** + * The attributes used to import existing HttpStage + */ +export interface HttpStageAttributes extends StageAttributes { +} + /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage */ -export class HttpStage extends StageBase { +export class HttpStage extends StageBase implements IHttpStage { + /** + * Import an existing stage into this CDK app. + */ + public static fromHttpStageAttributes(scope: Construct, id: string, attrs: HttpStageAttributes): IHttpStage { + class Import extends StageBase implements IHttpStage { + public readonly stageName = attrs.stageName; + public readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } + } + return new Import(scope, id); + } + public readonly stageName: string; - protected readonly api: IApi; + public readonly api: IApi; constructor(scope: Construct, id: string, props: HttpStageProps) { super(scope, id, { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index 64e63c0a3908a..c7ffc72703bd1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -1,8 +1,9 @@ import { Construct } from 'constructs'; import { CfnApi } from '../apigatewayv2.generated'; -import { ApiBase, IApi } from '../common/api'; +import { IApi } from '../common/api'; +import { ApiBase } from '../common/base'; import { DomainMappingOptions, IStage } from '../common/stage'; -import { IWebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; +import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; import { WebSocketRoute, WebSocketRouteOptions } from './route'; import { WebSocketStage } from './stage'; @@ -14,7 +15,7 @@ export interface IWebSocketApi extends IApi { * Add a websocket integration * @internal */ - _addIntegration(scope: Construct, config: IWebSocketRouteIntegrationConfig): WebSocketIntegration + _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration } /** @@ -134,10 +135,8 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { /** * @internal */ - public _addIntegration(scope: Construct, config: IWebSocketRouteIntegrationConfig): WebSocketIntegration { - const configHash = this._getIntegrationConfigHash(scope, config); - const existingIntegration = this._getSavedIntegration(configHash); - + public _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration { + const { configHash, integration: existingIntegration } = this._integrationCache.getSavedIntegration(scope, config); if (existingIntegration) { return existingIntegration as WebSocketIntegration; } @@ -147,7 +146,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { integrationType: config.type, integrationUri: config.uri, }); - this._saveIntegration(configHash, integration); + this._integrationCache.saveIntegration(scope, config, integration); return integration; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts index a8eaf83439641..e75bd00b63d95 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts @@ -1,7 +1,7 @@ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; -import { IIntegration, IRouteIntegrationConfig } from '../common'; +import { IIntegration } from '../common'; import { IWebSocketApi } from './api'; import { IWebSocketRoute } from './route'; @@ -91,13 +91,13 @@ export interface IWebSocketRouteIntegration { /** * Bind this integration to the route. */ - bind(options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig; + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig; } /** * Config returned back as a result of the bind. */ -export interface IWebSocketRouteIntegrationConfig extends IRouteIntegrationConfig { +export interface WebSocketRouteIntegrationConfig { /** * Integration type. */ diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index ce906f01eee0c..2588328083c6c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -1,10 +1,17 @@ import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, StageBase } from '../common'; +import { CommonStageOptions, IStage, StageAttributes } from '../common'; import { IApi } from '../common/api'; +import { StageBase } from '../common/base'; import { IWebSocketApi } from './api'; +/** + * Represents the WebSocketStage + */ +export interface IWebSocketStage extends IStage { +} + /** * Properties to initialize an instance of `WebSocketStage`. */ @@ -20,13 +27,34 @@ export interface WebSocketStageProps extends CommonStageOptions { readonly stageName: string; } +/** + * The attributes used to import existing WebSocketStage + */ +export interface WebSocketStageAttributes extends StageAttributes { +} + /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage */ -export class WebSocketStage extends StageBase { +export class WebSocketStage extends StageBase implements IWebSocketStage { + /** + * Import an existing stage into this CDK app. + */ + public static fromWebSocketStageAttributes(scope: Construct, id: string, attrs: WebSocketStageAttributes): IWebSocketStage { + class Import extends StageBase implements IWebSocketStage { + public readonly stageName = attrs.stageName; + public readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } + } + return new Import(scope, id); + } + public readonly stageName: string; - protected readonly api: IApi; + public readonly api: IApi; constructor(scope: Construct, id: string, props: WebSocketStageProps) { super(scope, id, { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index df7141df24d30..a8c5f418f7782 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -5,7 +5,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, - HttpRouteIntegrationBindOptions, IHttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; describe('HttpApi', () => { @@ -374,7 +374,7 @@ describe('HttpApi', () => { }); class DummyRouteIntegration implements IHttpRouteIntegration { - public bind(_: HttpRouteIntegrationBindOptions): IHttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { return { payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, type: HttpIntegrationType.HTTP_PROXY, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 3b7cf43f166d7..a5ce1b7b64b64 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { Stack, App } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteAuthorizerBindOptions, - HttpRouteAuthorizerConfig, IHttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; describe('HttpRoute', () => { @@ -164,7 +164,7 @@ describe('HttpRoute', () => { const httpApi = new HttpApi(stack, 'HttpApi'); class PrivateIntegration implements IHttpRouteIntegration { - public bind(): IHttpRouteIntegrationConfig { + public bind(): HttpRouteIntegrationConfig { return { method: HttpMethod.ANY, payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, @@ -244,7 +244,7 @@ describe('HttpRoute', () => { }); class DummyIntegration implements IHttpRouteIntegration { - public bind(): IHttpRouteIntegrationConfig { + public bind(): HttpRouteIntegrationConfig { return { type: HttpIntegrationType.HTTP_PROXY, payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index aea7dd7b57b29..ec1e28542d598 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -31,7 +31,7 @@ describe('HttpStage', () => { httpApi: api, }); - const imported = HttpStage.fromStageAttributes(stack, 'Import', { + const imported = HttpStage.fromHttpStageAttributes(stack, 'Import', { stageName: stage.stageName, api, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts index 0e1995f67a076..22e8411e4c1a8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, - WebSocketRouteIntegrationBindOptions, IWebSocketRouteIntegrationConfig, + WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, } from '../../lib'; describe('WebSocketApi', () => { @@ -100,7 +100,7 @@ describe('WebSocketApi', () => { }); class DummyIntegration implements IWebSocketRouteIntegration { - bind(_options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { return { type: WebSocketIntegrationType.AWS_PROXY, uri: 'some-uri', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts index a498cd6854bad..04e8e5fc7efac 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, - WebSocketRoute, WebSocketRouteIntegrationBindOptions, IWebSocketRouteIntegrationConfig, + WebSocketRoute, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, } from '../../lib'; describe('WebSocketRoute', () => { @@ -45,7 +45,7 @@ describe('WebSocketRoute', () => { class DummyIntegration implements IWebSocketRouteIntegration { - bind(_options: WebSocketRouteIntegrationBindOptions): IWebSocketRouteIntegrationConfig { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { return { type: WebSocketIntegrationType.AWS_PROXY, uri: 'some-uri', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts index c112cb368ae1b..5ebdf0c61a980 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -21,4 +21,24 @@ describe('WebSocketStage', () => { }); expect(defaultStage.url.endsWith('/dev')).toBe(true); }); + + test('import', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const stage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + const imported = WebSocketStage.fromWebSocketStageAttributes(stack, 'Import', { + stageName: stage.stageName, + api, + }); + + // THEN + expect(imported.stageName).toEqual(stage.stageName); + }); }); From 67b3bb5ad2a4f4bc97953fc55b6193cc67a2a9a4 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Wed, 3 Mar 2021 16:46:21 +0530 Subject: [PATCH 10/13] address comments --- .../aws-apigatewayv2-integrations/README.md | 5 +++-- .../aws-apigatewayv2/lib/common/base/api.ts | 2 +- .../aws-apigatewayv2/lib/common/base/stage.ts | 8 ++++---- .../aws-apigatewayv2/lib/common/stage.ts | 14 +------------- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 4 ++-- .../aws-apigatewayv2/lib/http/stage.ts | 17 ++++++++++++++--- .../{common => }/private/integration-cache.ts | 17 +++++++---------- .../aws-apigatewayv2/lib/websocket/api.ts | 7 +++---- .../aws-apigatewayv2/lib/websocket/stage.ts | 18 ++++++++++++++---- 9 files changed, 49 insertions(+), 43 deletions(-) rename packages/@aws-cdk/aws-apigatewayv2/lib/{common => }/private/integration-cache.ts (60%) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index b3c0afcd45db2..10de29f13dd52 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -151,11 +151,12 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { ## WebSocket APIs -WebSocket integrations connect a route to backend resources. Currently supported integrations include: LambdaWebSocketIntegration +WebSocket integrations connect a route to backend resources. The following integrations are supported in the CDK. ### Lambda WebSocket Integration -Lambda integrations enable integrating a WebSocket API route with a Lambda function. When a client connects/disconnects or sends message specific to a route, the API Gateway service forwards the request to the Lambda function +Lambda integrations enable integrating a WebSocket API route with a Lambda function. When a client connects/disconnects +or sends message specific to a route, the API Gateway service forwards the request to the Lambda function The API Gateway service will invoke the lambda function with an event payload of a specific format. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts index 8b98eb2a2de76..c760c43ba9ef1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts @@ -1,7 +1,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import { Resource } from '@aws-cdk/core'; +import { IntegrationCache } from '../../private/integration-cache'; import { IApi } from '../api'; -import { IntegrationCache } from '../private/integration-cache'; /** * Base class representing an API diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts index 16cb1c9f0c330..0dd6b1446408d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts @@ -10,7 +10,7 @@ import { DomainMappingOptions, IStage } from '../stage'; */ export abstract class StageBase extends Resource implements IStage { public abstract readonly stageName: string; - public abstract readonly api: IApi; + protected abstract readonly baseApi: IApi; /** * The URL to this stage. @@ -22,7 +22,7 @@ export abstract class StageBase extends Resource implements IStage { */ protected _addDomainMapping(domainMapping: DomainMappingOptions) { new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { - api: this.api, + api: this.baseApi, domainName: domainMapping.domainName, stage: this, apiMappingKey: domainMapping.mappingKey, @@ -32,8 +32,8 @@ export abstract class StageBase extends Resource implements IStage { } public metric(metricName: string, props?: MetricOptions): Metric { - return this.api.metric(metricName, props).with({ - dimensions: { ApiId: this.api.apiId, Stage: this.stageName }, + return this.baseApi.metric(metricName, props).with({ + dimensions: { ApiId: this.baseApi.apiId, Stage: this.stageName }, }).attachTo(this); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index a385af8656658..4e4587fd9b590 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -1,6 +1,5 @@ import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; import { IResource } from '@aws-cdk/core'; -import { IApi } from './api'; import { IDomainName } from './domain-name'; /** @@ -13,12 +12,6 @@ export interface IStage extends IResource { */ readonly stageName: string; - /** - * The API this stage is associated to. - * @attribute - */ - readonly api: IApi; - /** * The URL to this stage. */ @@ -98,7 +91,7 @@ export interface DomainMappingOptions { * Options required to create a new stage. * Options that are common between HTTP and Websocket APIs. */ -export interface CommonStageOptions { +export interface StageOptions { /** @@ -123,9 +116,4 @@ export interface StageAttributes { * The name of the stage */ readonly stageName: string; - - /** - * The API to which this stage is associated - */ - readonly api: IApi; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index ac673ae2eeb7c..b74c00e5824fb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -180,7 +180,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th * @internal */ public _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration { - const { configHash, integration: existingIntegration } = this._integrationCache.getSavedIntegration(scope, config); + const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config); if (existingIntegration) { return existingIntegration as HttpIntegration; } @@ -252,7 +252,7 @@ export class HttpApi extends HttpApiBase { public readonly disableExecuteApiEndpoint?: boolean; /** - * default stage of the api resource + * The default stage of this API */ public readonly defaultStage: IStage | undefined; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index 370d185b4086c..d6a5f96320e3b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -1,7 +1,7 @@ import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, IStage, StageAttributes } from '../common'; +import { StageOptions, IStage, StageAttributes } from '../common'; import { IApi } from '../common/api'; import { StageBase } from '../common/base'; import { IHttpApi } from './api'; @@ -12,12 +12,16 @@ const DEFAULT_STAGE_NAME = '$default'; * Represents the HttpStage */ export interface IHttpStage extends IStage { + /** + * The API this stage is associated to. + */ + readonly api: IHttpApi; } /** * The options to create a new Stage for an HTTP API */ -export interface HttpStageOptions extends CommonStageOptions { +export interface HttpStageOptions extends StageOptions { /** * The name of the stage. See `StageName` class for more details. * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. @@ -39,6 +43,10 @@ export interface HttpStageProps extends HttpStageOptions { * The attributes used to import existing HttpStage */ export interface HttpStageAttributes extends StageAttributes { + /** + * The API to which this stage is associated + */ + readonly api: IHttpApi; } /** @@ -51,6 +59,7 @@ export class HttpStage extends StageBase implements IHttpStage { */ public static fromHttpStageAttributes(scope: Construct, id: string, attrs: HttpStageAttributes): IHttpStage { class Import extends StageBase implements IHttpStage { + protected readonly baseApi = attrs.api; public readonly stageName = attrs.stageName; public readonly api = attrs.api; @@ -61,8 +70,9 @@ export class HttpStage extends StageBase implements IHttpStage { return new Import(scope, id); } + protected readonly baseApi: IApi; public readonly stageName: string; - public readonly api: IApi; + public readonly api: IHttpApi; constructor(scope: Construct, id: string, props: HttpStageProps) { super(scope, id, { @@ -76,6 +86,7 @@ export class HttpStage extends StageBase implements IHttpStage { }); this.stageName = this.physicalName; + this.baseApi = props.httpApi; this.api = props.httpApi; if (props.domainMapping) { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts similarity index 60% rename from packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts index b4d124e82f7f6..2fc4f94f79b8e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/private/integration-cache.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts @@ -1,30 +1,27 @@ import * as crypto from 'crypto'; import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { HttpRouteIntegrationConfig } from '../../http'; -import { WebSocketRouteIntegrationConfig } from '../../websocket'; -import { IIntegration } from '../integration'; +import { IIntegration } from '../common/integration'; +import { HttpRouteIntegrationConfig } from '../http'; +import { WebSocketRouteIntegrationConfig } from '../websocket'; type IntegrationConfig = HttpRouteIntegrationConfig | WebSocketRouteIntegrationConfig; -/** - * @internal - */ export class IntegrationCache { private integrations: Record = {}; - getSavedIntegration(scope: Construct, config: IntegrationConfig) { - const configHash = this.getIntegrationConfigHash(scope, config); + getIntegration(scope: Construct, config: IntegrationConfig) { + const configHash = this.integrationConfigHash(scope, config); const integration = this.integrations[configHash]; return { configHash, integration }; } saveIntegration(scope: Construct, config: IntegrationConfig, integration: IIntegration) { - const configHash = this.getIntegrationConfigHash(scope, config); + const configHash = this.integrationConfigHash(scope, config); this.integrations[configHash] = integration; } - private getIntegrationConfigHash(scope: Construct, config: IntegrationConfig): string { + private integrationConfigHash(scope: Construct, config: IntegrationConfig): string { const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); return configHash; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index c7ffc72703bd1..4ecf275dd64dd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -89,7 +89,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { public readonly webSocketApiName?: string; /** - * default stage of the api resource + * The default stage for this API */ public readonly defaultStage: IStage | undefined; @@ -117,8 +117,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { } if (!props?.defaultStageName && props?.defaultDomainMapping) { - throw new Error('defaultDomainMapping not supported when defaultStageName is not provided', - ); + throw new Error('defaultDomainMapping not supported when defaultStageName is not provided'); } if (props?.connectRouteOptions) { @@ -136,7 +135,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { * @internal */ public _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration { - const { configHash, integration: existingIntegration } = this._integrationCache.getSavedIntegration(scope, config); + const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config); if (existingIntegration) { return existingIntegration as WebSocketIntegration; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index 2588328083c6c..a50353a79ca2d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -1,8 +1,7 @@ import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, IStage, StageAttributes } from '../common'; -import { IApi } from '../common/api'; +import { StageOptions, IApi, IStage, StageAttributes } from '../common'; import { StageBase } from '../common/base'; import { IWebSocketApi } from './api'; @@ -10,12 +9,16 @@ import { IWebSocketApi } from './api'; * Represents the WebSocketStage */ export interface IWebSocketStage extends IStage { + /** + * The API this stage is associated to. + */ + readonly api: IWebSocketApi; } /** * Properties to initialize an instance of `WebSocketStage`. */ -export interface WebSocketStageProps extends CommonStageOptions { +export interface WebSocketStageProps extends StageOptions { /** * The WebSocket API to which this stage is associated. */ @@ -31,6 +34,10 @@ export interface WebSocketStageProps extends CommonStageOptions { * The attributes used to import existing WebSocketStage */ export interface WebSocketStageAttributes extends StageAttributes { + /** + * The API to which this stage is associated + */ + readonly api: IWebSocketApi; } /** @@ -43,6 +50,7 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { */ public static fromWebSocketStageAttributes(scope: Construct, id: string, attrs: WebSocketStageAttributes): IWebSocketStage { class Import extends StageBase implements IWebSocketStage { + public readonly baseApi = attrs.api; public readonly stageName = attrs.stageName; public readonly api = attrs.api; @@ -53,14 +61,16 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { return new Import(scope, id); } + protected readonly baseApi: IApi; public readonly stageName: string; - public readonly api: IApi; + public readonly api: IWebSocketApi; constructor(scope: Construct, id: string, props: WebSocketStageProps) { super(scope, id, { physicalName: props.stageName, }); + this.baseApi = props.webSocketApi; this.api = props.webSocketApi; this.stageName = this.physicalName; From 5cffd46b707ec2d5c0126e5f60009343690d8c33 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Thu, 4 Mar 2021 08:25:11 +0530 Subject: [PATCH 11/13] address comments --- .../aws-apigatewayv2-integrations/README.md | 7 +- .../test/websocket/integ.lambda.expected.json | 20 ++-- .../test/websocket/integ.lambda.ts | 10 +- packages/@aws-cdk/aws-apigatewayv2/README.md | 13 +- .../lib/common/api-mapping.ts | 25 ++-- .../aws-apigatewayv2/lib/common/api.ts | 9 +- .../aws-apigatewayv2/lib/common/base.ts | 111 ++++++++++++++++++ .../aws-apigatewayv2/lib/common/base/api.ts | 50 -------- .../aws-apigatewayv2/lib/common/base/index.ts | 2 - .../aws-apigatewayv2/lib/common/base/stage.ts | 63 ---------- .../lib/private/integration-cache.ts | 2 +- .../aws-apigatewayv2/lib/websocket/api.ts | 33 ------ .../@aws-cdk/aws-apigatewayv2/package.json | 4 - .../test/common/api-mapping.test.ts | 40 ++++++- .../test/websocket/api.test.ts | 17 --- 15 files changed, 197 insertions(+), 209 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index 10de29f13dd52..cce77fd6398e6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -163,8 +163,11 @@ The API Gateway service will invoke the lambda function with an event payload of The following code configures a `sendmessage` route with a Lambda integration ```ts -const webSocketApi = new WebSocketApi(stack, 'mywsapi', { - defaultStageName: 'dev', +const webSocketApi = new WebSocketApi(stack, 'mywsapi'); +new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, }); const messageHandler = new lambda.Function(stack, 'MessageHandler', {...}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json index b4d04623cc881..aa5dfdd7de0bd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -208,16 +208,6 @@ "RouteSelectionExpression": "$request.body.action" } }, - "mywsapiDefaultStageD7F52467": { - "Type": "AWS::ApiGatewayV2::Stage", - "Properties": { - "ApiId": { - "Ref": "mywsapi32E6CE11" - }, - "StageName": "dev", - "AutoDeploy": true - } - }, "mywsapiconnectRouteWebSocketApiIntegmywsapiconnectRoute456CB290Permission2D0BC294": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -505,6 +495,16 @@ ] } } + }, + "mystage114C35EC": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "StageName": "dev", + "AutoDeploy": true + } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts index e13c2da741676..ac931cb1eed05 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -1,4 +1,4 @@ -import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2'; +import { WebSocketApi, WebSocketStage } from '@aws-cdk/aws-apigatewayv2'; import * as lambda from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Stack } from '@aws-cdk/core'; import { LambdaWebSocketIntegration } from '../../lib'; @@ -39,12 +39,16 @@ const messageHandler = new lambda.Function(stack, 'MessageHandler', { }); const webSocketApi = new WebSocketApi(stack, 'mywsapi', { - defaultStageName: 'dev', connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, }); +const stage = new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: messageHandler }) }); -new CfnOutput(stack, 'ApiEndpoint', { value: webSocketApi.defaultStage?.url! }); +new CfnOutput(stack, 'ApiEndpoint', { value: stage.url }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 868b7ebc5a6d2..d8278a800a00f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -298,20 +298,23 @@ Integrations are available in the `aws-apigatewayv2-integrations` module and mor To add the default WebSocket routes supported by API Gateway (`$connect`, `$disconnect` and `$default`), configure them as part of api props: ```ts -new WebSocketApi(stack, 'mywsapi', { - defaultStageName: 'dev', +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, }); + +new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); ``` To add any other route: ```ts -const webSocketApi = new WebSocketApi(stack, 'mywsapi', { - defaultStageName: 'dev', -}); +const webSocketApi = new WebSocketApi(stack, 'mywsapi'); webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: messageHandler, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index 23675a5521697..541c3adfa0c56 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -1,6 +1,7 @@ import { IResource, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; +import { HttpApi } from '../http/api'; import { IApi } from './api'; import { IDomainName } from './domain-name'; import { IStage } from './stage'; @@ -82,8 +83,17 @@ export class ApiMapping extends Resource implements IApiMapping { constructor(scope: Construct, id: string, props: ApiMappingProps) { super(scope, id); - if ((!props.stage?.stageName) && !props.api.defaultStage) { - throw new Error('stage is required if default stage is not available'); + let stage = props.stage; + if (!stage) { + if (props.api instanceof HttpApi) { + if (props.api.defaultStage) { + stage = props.api.defaultStage; + } else { + throw new Error('stage is required if default stage is not available'); + } + } else { + throw new Error('stage is required for WebSocket API'); + } } const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; @@ -98,21 +108,14 @@ export class ApiMapping extends Resource implements IApiMapping { const apiMappingProps: CfnApiMappingProps = { apiId: props.api.apiId, domainName: props.domainName.name, - stage: props.stage?.stageName ?? props.api.defaultStage!.stageName, + stage: stage.stageName, apiMappingKey: props.apiMappingKey, }; const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); // ensure the dependency on the provided stage - if (props.stage) { - this.node.addDependency(props.stage); - } - - // if stage not specified, we ensure the default stage is ready before we create the api mapping - if (!props.stage?.stageName && props.api.defaultStage) { - this.node.addDependency(props.api.defaultStage!); - } + this.node.addDependency(stage); this.apiMappingId = resource.ref; this.mappingKey = props.apiMappingKey; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts index 0463ad5274b54..c632e6309083d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts @@ -1,10 +1,10 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { IStage } from './stage'; +import { IResource } from '@aws-cdk/core'; /** * Represents a API Gateway HTTP/WebSocket API */ -export interface IApi { +export interface IApi extends IResource { /** * The identifier of this API Gateway API. * @attribute @@ -17,11 +17,6 @@ export interface IApi { */ readonly apiEndpoint: string; - /** - * The default stage for this API - */ - readonly defaultStage?: IStage; - /** * Return the given named metric for this Api Gateway * diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts new file mode 100644 index 0000000000000..542fcfb16f8f4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts @@ -0,0 +1,111 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Resource } from '@aws-cdk/core'; +import { IntegrationCache } from '../private/integration-cache'; +import { IApi } from './api'; +import { ApiMapping } from './api-mapping'; +import { DomainMappingOptions, IStage } from './stage'; + +/** + * Base class representing an API + * @internal + */ +export abstract class ApiBase extends Resource implements IApi { + abstract readonly apiId: string; + abstract readonly apiEndpoint: string; + /** + * @internal + */ + protected _integrationCache: IntegrationCache = new IntegrationCache(); + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ApiGateway', + metricName, + dimensions: { ApiId: this.apiId }, + ...props, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} + + +/** + * Base class representing a Stage + * @internal + */ +export abstract class StageBase extends Resource implements IStage { + public abstract readonly stageName: string; + protected abstract readonly baseApi: IApi; + + /** + * The URL to this stage. + */ + abstract get url(): string; + + /** + * @internal + */ + protected _addDomainMapping(domainMapping: DomainMappingOptions) { + new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { + api: this.baseApi, + domainName: domainMapping.domainName, + stage: this, + apiMappingKey: domainMapping.mappingKey, + }); + // ensure the dependency + this.node.addDependency(domainMapping.domainName); + } + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.baseApi.metric(metricName, props).with({ + dimensions: { ApiId: this.baseApi.apiId, Stage: this.stageName }, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts deleted file mode 100644 index c760c43ba9ef1..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/api.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Resource } from '@aws-cdk/core'; -import { IntegrationCache } from '../../private/integration-cache'; -import { IApi } from '../api'; - -/** - * Base class representing an API - * @internal - */ -export abstract class ApiBase extends Resource implements IApi { - abstract readonly apiId: string; - abstract readonly apiEndpoint: string; - /** - * @internal - */ - protected _integrationCache: IntegrationCache = new IntegrationCache(); - - public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/ApiGateway', - metricName, - dimensions: { ApiId: this.apiId }, - ...props, - }).attachTo(this); - } - - public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Latency', props); - } -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts deleted file mode 100644 index b34869c30f4d7..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './api'; -export * from './stage'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts deleted file mode 100644 index 0dd6b1446408d..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base/stage.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; -import { Resource } from '@aws-cdk/core'; -import { IApi } from '../api'; -import { ApiMapping } from '../api-mapping'; -import { DomainMappingOptions, IStage } from '../stage'; - -/** - * Base class representing a Stage - * @internal - */ -export abstract class StageBase extends Resource implements IStage { - public abstract readonly stageName: string; - protected abstract readonly baseApi: IApi; - - /** - * The URL to this stage. - */ - abstract get url(): string; - - /** - * @internal - */ - protected _addDomainMapping(domainMapping: DomainMappingOptions) { - new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { - api: this.baseApi, - domainName: domainMapping.domainName, - stage: this, - apiMappingKey: domainMapping.mappingKey, - }); - // ensure the dependency - this.node.addDependency(domainMapping.domainName); - } - - public metric(metricName: string, props?: MetricOptions): Metric { - return this.baseApi.metric(metricName, props).with({ - dimensions: { ApiId: this.baseApi.apiId, Stage: this.stageName }, - }).attachTo(this); - } - - public metricClientError(props?: MetricOptions): Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: MetricOptions): Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: MetricOptions): Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: MetricOptions): Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: MetricOptions): Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: MetricOptions): Metric { - return this.metric('Latency', props); - } -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts index 2fc4f94f79b8e..2401d28e20d2d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts @@ -26,4 +26,4 @@ export class IntegrationCache { const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); return configHash; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index 4ecf275dd64dd..f2f2653c94ee6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -2,10 +2,8 @@ import { Construct } from 'constructs'; import { CfnApi } from '../apigatewayv2.generated'; import { IApi } from '../common/api'; import { ApiBase } from '../common/base'; -import { DomainMappingOptions, IStage } from '../common/stage'; import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; import { WebSocketRoute, WebSocketRouteOptions } from './route'; -import { WebSocketStage } from './stage'; /** * Represents a WebSocket API @@ -40,19 +38,6 @@ export interface WebSocketApiProps { */ readonly routeSelectionExpression?: string; - /** - * The name of the default stage with deployment - * @default - No default stage is created - */ - readonly defaultStageName?: string; - - /** - * Configure a custom domain with the API mapping resource to the WebSocket API - * - * @default - no default domain mapping configured. meaningless if `defaultStageName` is not provided. - */ - readonly defaultDomainMapping?: DomainMappingOptions; - /** * Options to configure a '$connect' route * @@ -88,11 +73,6 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { */ public readonly webSocketApiName?: string; - /** - * The default stage for this API - */ - public readonly defaultStage: IStage | undefined; - constructor(scope: Construct, id: string, props?: WebSocketApiProps) { super(scope, id); @@ -107,19 +87,6 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { this.apiId = resource.ref; this.apiEndpoint = resource.attrApiEndpoint; - if (props?.defaultStageName) { - this.defaultStage = new WebSocketStage(this, 'DefaultStage', { - webSocketApi: this, - stageName: props.defaultStageName, - autoDeploy: true, - domainMapping: props?.defaultDomainMapping, - }); - } - - if (!props?.defaultStageName && props?.defaultDomainMapping) { - throw new Error('defaultDomainMapping not supported when defaultStageName is not provided'); - } - if (props?.connectRouteOptions) { this.addRoute('$connect', props.connectRouteOptions); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 97b7e76544f01..b1dd874b85f9e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -104,10 +104,6 @@ "awslint": { "exclude": [ "props-physical-name:@aws-cdk/aws-apigatewayv2.ApiMappingProps", - "construct-interface-extends-iconstruct:@aws-cdk/aws-apigatewayv2.IHttpApi", - "construct-interface-extends-iconstruct:@aws-cdk/aws-apigatewayv2.IWebSocketApi", - "resource-interface-extends-resource:@aws-cdk/aws-apigatewayv2.IHttpApi", - "resource-interface-extends-resource:@aws-cdk/aws-apigatewayv2.IWebSocketApi", "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index c712c5c8d984a..b917f19513a57 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { Stack } from '@aws-cdk/core'; -import { DomainName, HttpApi, ApiMapping } from '../../lib'; +import { DomainName, HttpApi, ApiMapping, WebSocketApi } from '../../lib'; const domainName = 'example.com'; const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; @@ -218,4 +218,42 @@ describe('ApiMapping', () => { expect(imported.apiMappingId).toEqual(mapping.apiMappingId); }); + + test('stage validation - throws if defaultStage not available for HttpApi', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + expect(() => { + new ApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + }).toThrow(/stage is required if default stage is not available/); + }); + + test('stage validation - throws if stage not provided for WebSocketApi', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + expect(() => { + new ApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + }).toThrow(/stage is required for WebSocket API/); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts index 22e8411e4c1a8..fcc65d4e18207 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -24,23 +24,6 @@ describe('WebSocketApi', () => { expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Integration'); }); - test('setting defaultStageName', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const api = new WebSocketApi(stack, 'api', { - defaultStageName: 'dev', - }); - - // THEN - expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.apiId), - StageName: 'dev', - AutoDeploy: true, - }); - }); - test('addRoute: adds a route with passed key', () => { // GIVEN const stack = new Stack(); From f9494303ac4e0701732cbca49d4aae5d581b0769 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Thu, 4 Mar 2021 19:20:58 +0530 Subject: [PATCH 12/13] tweaks --- .../test/websocket/integ.lambda.expected.json | 2 +- .../test/websocket/integ.lambda.ts | 2 +- packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts | 4 +++- packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts | 2 -- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json index aa5dfdd7de0bd..48bf164ada435 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -35,7 +35,7 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"conencted\" }; };" + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"connected\" }; };" }, "Role": { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts index ac931cb1eed05..01e25f906b0f8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -17,7 +17,7 @@ const stack = new Stack(app, 'WebSocketApiInteg'); const connectHandler = new lambda.Function(stack, 'ConnectHandler', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "conencted" }; };'), + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "connected" }; };'), }); const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index 541c3adfa0c56..937a6776bd5f0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -40,8 +40,10 @@ export interface ApiMappingProps { /** * stage for the ApiMapping resource + * required for WebSocket API + * defaults to default stage of an HTTP API * - * @default - Default stage of the passed API + * @default - Default stage of the passed API for HTTP API, undefined for WebSocket API */ readonly stage?: IStage; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index 4e4587fd9b590..40b7832418633 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -92,8 +92,6 @@ export interface DomainMappingOptions { * Options that are common between HTTP and Websocket APIs. */ export interface StageOptions { - - /** * Whether updates to an API automatically trigger a new deployment. * @default false From 1a1f0cf4455200c953c6072c677ef319a0af21e0 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 4 Mar 2021 16:19:54 +0000 Subject: [PATCH 13/13] Update packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts --- packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index 937a6776bd5f0..adbe3fe3efc2c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -43,7 +43,7 @@ export interface ApiMappingProps { * required for WebSocket API * defaults to default stage of an HTTP API * - * @default - Default stage of the passed API for HTTP API, undefined for WebSocket API + * @default - Default stage of the passed API for HTTP API, required for WebSocket API */ readonly stage?: IStage; }