diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index d60c08de7f318..49e9530404a56 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -1,4 +1,5 @@ ## AWS::APIGatewayv2 Construct Library + --- @@ -6,11 +7,104 @@ > All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +## Table of Contents + +- [Introduction](#introduction) +- [HTTP API](#http-api) + - [Defining HTTP APIs](#defining-http-apis) + - [Publishing HTTP APIs](#publishing-http-apis) + +## Introduction + +Amazon API Gateway is an AWS service for creating, publishing, maintaining, monitoring, and securing REST, HTTP, and WebSocket +APIs at any scale. API developers can create APIs that access AWS or other web services, as well as data stored in the AWS Cloud. +As an API Gateway API developer, you can create APIs for use in your own client applications. Read the +[Amazon API Gateway Developer Guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html). + +This module supports features under [API Gateway v2](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_ApiGatewayV2.html) +that lets users set up Websocket and HTTP APIs. +REST APIs can be created using the `@aws-cdk/aws-apigateway` module. + +## HTTP API + +HTTP APIs enable creation of RESTful APIs that integrate with AWS Lambda functions, known as Lambda proxy integration, +or to any routable HTTP endpoint, known as HTTP proxy integration. + +### Defining HTTP APIs + +HTTP APIs have two fundamental concepts - Routes and Integrations. + +Routes direct incoming API requests to backend resources. Routes consist of two parts: an HTTP method and a resource +path, such as, `GET /books`. Learn more at [Working with +routes](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html). Use the `ANY` method +to match any methods for a route that are not explicitly defined. + +Integrations define how the HTTP API responds when a client reaches a specific Route. HTTP APIs support two types of +integrations - Lambda proxy integration and HTTP proxy integration. Learn more at [Configuring +integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations.html). + +The code snippet below configures a route `GET /books` with an HTTP proxy integration and uses the `ANY` method to +proxy all other HTTP method calls to `/books` to a lambda proxy. ```ts -import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +const getBooksIntegration = new HttpProxyIntegration({ + url: 'https://get-books-proxy.myproxy.internal', +}); + +const booksDefaultFn = new lambda.Function(stack, 'BooksDefaultFn', { ... }); +const booksDefaultIntegration = new LambdaProxyIntegration({ + handler: booksDefaultFn, +}); + +const httpApi = new HttpApi(stack, 'HttpApi'); + +httpApi.addRoutes({ + path: '/books', + methods: [ HttpMethod.GET ], + integration: getBooksIntegration, +}); +httpApi.addRoutes({ + path: '/books', + methods: [ HttpMethod.ANY ], + integration: booksDefaultIntegration, +}); +``` + +The `defaultIntegration` option while defining HTTP APIs lets you create a default catch-all integration that is +matched when a client reaches a route that is not explicitly defined. + +```ts +new HttpApi(stack, 'HttpProxyApi', { + defaultIntegration: new HttpProxyIntegration({ + url:'http://example.com', + }), +}); +``` + +### Publishing HTTP APIs + +A Stage is a logical reference to a lifecycle state of your API (for example, `dev`, `prod`, `beta`, or `v2`). API +stages are identified by their stage name. Each stage is a named reference to a deployment of the API made available for +client applications to call. + +Use `HttpStage` to create a Stage resource for HTTP APIs. The following code sets up a Stage, whose URL is available at +`https://{api_id}.execute-api.{region}.amazonaws.com/beta`. + +```ts +new HttpStage(stack, 'Stage', { + httpApi: api, + stageName: 'beta', +}); ``` + +If you omit the `stageName` will create a `$default` stage. A `$default` stage is one that is served from the base of +the API's URL - `https://{api_id}.execute-api.{region}.amazonaws.com/`. + +Note that, `HttpApi` will always creates a `$default` stage, unless the `createDefaultStage` property is unset. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts new file mode 100644 index 0000000000000..5995c40125978 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -0,0 +1,3 @@ +export * from './integration'; +export * from './route'; +export * from './stage'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts new file mode 100644 index 0000000000000..7255607639468 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts @@ -0,0 +1,12 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents an integration to an API Route. + */ +export interface IIntegration extends IResource { + /** + * Id of the integration. + * @attribute + */ + readonly integrationId: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/route.ts new file mode 100644 index 0000000000000..7419328256220 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/route.ts @@ -0,0 +1,12 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents a route. + */ +export interface IRoute extends IResource { + /** + * Id of the Route + * @attribute + */ + readonly routeId: string; +} \ 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 new file mode 100644 index 0000000000000..b608a7a34ad97 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -0,0 +1,30 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents a Stage. + */ +export interface IStage extends IResource { + /** + * The name of the stage; its primary identifier. + * @attribute + */ + readonly stageName: string; +} + +/** + * Options required to create a new stage. + * 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; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts new file mode 100644 index 0000000000000..c0ca922a69347 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -0,0 +1,133 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; +import { IHttpRouteIntegration } from './integration'; +import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; +import { HttpStage, HttpStageOptions } from './stage'; + +/** + * Represents an HTTP API + */ +export interface IHttpApi extends IResource { + /** + * The identifier of this API Gateway HTTP API. + * @attribute + */ + readonly httpApiId: string; +} + +/** + * Properties to initialize an instance of `HttpApi`. + */ +export interface HttpApiProps { + /** + * Name for the HTTP API resoruce + * @default - id of the HttpApi construct. + */ + readonly apiName?: string; + + /** + * An integration that will be configured on the catch-all route ($default). + * @default - none + */ + readonly defaultIntegration?: IHttpRouteIntegration; + + /** + * Whether a default stage and deployment should be automatically created. + * @default true + */ + readonly createDefaultStage?: boolean; +} + +/** + * Options for the Route with Integration resoruce + */ +export interface AddRoutesOptions extends BatchHttpRouteOptions { + /** + * The path at which all of these routes are configured. + */ + readonly path: string; + + /** + * The HTTP methods to be configured + * @default HttpMethod.ANY + */ + readonly methods?: HttpMethod[]; +} + +/** + * Create a new API Gateway HTTP API endpoint. + * @resource AWS::ApiGatewayV2::Api + */ +export class HttpApi extends Resource implements IHttpApi { + /** + * Import an existing HTTP API into this CDK app. + */ + public static fromApiId(scope: Construct, id: string, httpApiId: string): IHttpApi { + class Import extends Resource implements IHttpApi { + public readonly httpApiId = httpApiId; + } + return new Import(scope, id); + } + + public readonly httpApiId: string; + private readonly defaultStage: HttpStage | undefined; + + constructor(scope: Construct, id: string, props?: HttpApiProps) { + super(scope, id); + + const apiName = props?.apiName ?? id; + + const apiProps: CfnApiProps = { + name: apiName, + protocolType: 'HTTP', + }; + const resource = new CfnApi(this, 'Resource', apiProps); + this.httpApiId = resource.ref; + + if (props?.defaultIntegration) { + new HttpRoute(this, 'DefaultRoute', { + httpApi: this, + routeKey: HttpRouteKey.DEFAULT, + integration: props.defaultIntegration, + }); + } + + if (props?.createDefaultStage === undefined || props.createDefaultStage === true) { + this.defaultStage = new HttpStage(this, 'DefaultStage', { + httpApi: this, + autoDeploy: true, + }); + } + } + + /** + * Get the URL to the default stage of this API. + * Returns `undefined` if `createDefaultStage` is unset. + */ + public get url(): string | undefined { + return this.defaultStage ? this.defaultStage.url : undefined; + } + + /** + * Add a new stage. + */ + public addStage(id: string, options: HttpStageOptions): HttpStage { + return new HttpStage(this, id, { + httpApi: this, + ...options, + }); + } + + /** + * Add multiple routes that uses the same configuration. The routes all go to the same path, but for different + * methods. + */ + public addRoutes(options: AddRoutesOptions): HttpRoute[] { + const methods = options.methods ?? [ HttpMethod.ANY ]; + return methods.map((method) => new HttpRoute(this, `${method}${options.path}`, { + httpApi: this, + routeKey: HttpRouteKey.with(options.path, method), + integration: options.integration, + })); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts new file mode 100644 index 0000000000000..4fbb1c4e76f6a --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -0,0 +1,5 @@ +export * from './api'; +export * from './route'; +export * from './integration'; +export * from './integrations'; +export * from './stage'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts new file mode 100644 index 0000000000000..7e9672ed5a786 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -0,0 +1,152 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { CfnIntegration } from '../apigatewayv2.generated'; +import { IIntegration } from '../common'; +import { IHttpApi } from './api'; +import { HttpMethod, IHttpRoute } from './route'; + +/** + * Represents an Integration for an HTTP API. + */ +export interface IHttpIntegration extends IIntegration { + /** The HTTP API associated with this integration */ + readonly httpApi: IHttpApi; +} + +/** + * Supported integration types + */ +export enum HttpIntegrationType { + /** + * Integration type is a Lambda proxy + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + */ + LAMBDA_PROXY = 'AWS_PROXY', + /** + * Integration type is an HTTP proxy + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + */ + HTTP_PROXY = 'HTTP_PROXY', +} + +/** + * Payload format version for lambda proxy integration + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + */ +export class PayloadFormatVersion { + /** Version 1.0 */ + public static readonly VERSION_1_0 = new PayloadFormatVersion('1.0'); + /** Version 2.0 */ + public static readonly VERSION_2_0 = new PayloadFormatVersion('2.0'); + + /** + * A custom payload version. + * Typically used if there is a version number that the CDK doesn't support yet + */ + public static custom(version: string) { + return new PayloadFormatVersion(version); + } + + /** version as a string */ + public readonly version: string; + + private constructor(version: string) { + this.version = version; + } +} + +/** + * The integration properties + */ +export interface HttpIntegrationProps { + /** + * The HTTP API to which this integration should be bound. + */ + readonly httpApi: IHttpApi; + + /** + * Integration type + */ + readonly integrationType: HttpIntegrationType; + + /** + * Integration URI. + * This will be the function ARN in the case of `HttpIntegrationType.LAMBDA_PROXY`, + * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. + */ + readonly integrationUri: string; + + /** + * The HTTP method to use when calling the underlying HTTP proxy + * @default - none. required if the integration type is `HttpIntegrationType.HTTP_PROXY`. + */ + readonly method?: HttpMethod; + + /** + * The version of the payload format + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * @default - defaults to latest in the case of HttpIntegrationType.LAMBDA_PROXY`, irrelevant otherwise. + */ + readonly payloadFormatVersion?: PayloadFormatVersion; +} + +/** + * The integration for an API route. + * @resource AWS::ApiGatewayV2::Integration + */ +export class HttpIntegration extends Resource implements IHttpIntegration { + public readonly integrationId: string; + + public readonly httpApi: IHttpApi; + + constructor(scope: Construct, id: string, props: HttpIntegrationProps) { + super(scope, id); + const integ = new CfnIntegration(this, 'Resource', { + apiId: props.httpApi.httpApiId, + integrationType: props.integrationType, + integrationUri: props.integrationUri, + integrationMethod: props.method, + payloadFormatVersion: props.payloadFormatVersion?.version, + }); + this.integrationId = integ.ref; + this.httpApi = props.httpApi; + } +} + +/** + * The interface that various route integration classes will inherit. + */ +export interface IHttpRouteIntegration { + /** + * Bind this integration to the route. + */ + bind(route: IHttpRoute): HttpRouteIntegrationConfig; +} + +/** + * Config returned back as a result of the bind. + */ +export interface HttpRouteIntegrationConfig { + /** + * Integration type. + */ + readonly type: HttpIntegrationType; + + /** + * Integration URI + */ + readonly uri: string; + + /** + * The HTTP method that must be used to invoke the underlying proxy. + * Required for `HttpIntegrationType.HTTP_PROXY` + * @default - undefined + */ + readonly method?: HttpMethod; + + /** + * Payload format version in the case of lambda proxy integration + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * @default - undefined + */ + readonly payloadFormatVersion: PayloadFormatVersion; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts new file mode 100644 index 0000000000000..b514b63f8056f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts @@ -0,0 +1,35 @@ +import { HttpIntegrationType, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; +import { HttpMethod, IHttpRoute } from '../route'; + +/** + * Properties to initialize a new `HttpProxyIntegration`. + */ +export interface HttpProxyIntegrationProps { + /** + * The full-qualified HTTP URL for the HTTP integration + */ + readonly url: string + + /** + * The HTTP method that must be used to invoke the underlying HTTP proxy. + * @default HttpMethod.ANY + */ + readonly method?: HttpMethod; +} + +/** + * The HTTP Proxy integration resource for HTTP API + */ +export class HttpProxyIntegration implements IHttpRouteIntegration { + constructor(private readonly props: HttpProxyIntegrationProps) { + } + + public bind(_: IHttpRoute): HttpRouteIntegrationConfig { + return { + method: this.props.method ?? HttpMethod.ANY, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format + type: HttpIntegrationType.HTTP_PROXY, + uri: this.props.url, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/index.ts new file mode 100644 index 0000000000000..7797fe6ea8b03 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/index.ts @@ -0,0 +1,2 @@ +export * from './http'; +export * from './lambda'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts new file mode 100644 index 0000000000000..155b671fa79c2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts @@ -0,0 +1,48 @@ +import { ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { HttpIntegrationType, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; +import { IHttpRoute } from '../route'; + +/** + * Lambda Proxy integration properties + */ +export interface LambdaProxyIntegrationProps { + /** + * The handler for this integration. + */ + readonly handler: IFunction + + /** + * Version of the payload sent to the lambda handler. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * @default PayloadFormatVersion.VERSION_2_0 + */ + readonly payloadFormatVersion?: PayloadFormatVersion; +} + +/** + * The Lambda Proxy integration resource for HTTP API + */ +export class LambdaProxyIntegration implements IHttpRouteIntegration { + + constructor(private readonly props: LambdaProxyIntegrationProps) { + } + + public bind(route: IHttpRoute): HttpRouteIntegrationConfig { + this.props.handler.addPermission(`${route.node.uniqueId}-Permission`, { + principal: new ServicePrincipal('apigateway.amazonaws.com'), + sourceArn: Stack.of(route).formatArn({ + service: 'execute-api', + resource: route.httpApi.httpApiId, + resourceName: `*/*${route.path ?? ''}`, // empty string in the case of the catch-all route $default + }), + }); + + return { + type: HttpIntegrationType.LAMBDA_PROXY, + uri: this.props.handler.functionArn, + payloadFormatVersion: this.props.payloadFormatVersion ?? PayloadFormatVersion.VERSION_2_0, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts new file mode 100644 index 0000000000000..574b2c82275e8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -0,0 +1,141 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { CfnRoute, CfnRouteProps } from '../apigatewayv2.generated'; +import { IRoute } from '../common'; +import { IHttpApi } from './api'; +import { HttpIntegration, IHttpRouteIntegration } from './integration'; + +/** + * Represents a Route for an HTTP API. + */ +export interface IHttpRoute extends IRoute { + /** + * The HTTP API associated with this route. + */ + readonly httpApi: IHttpApi; + + /** + * Returns the path component of this HTTP route, `undefined` if the path is the catch-all route. + */ + readonly path?: string; +} + +/** + * Supported HTTP methods + */ +export enum HttpMethod { + /** HTTP ANY */ + ANY = 'ANY', + /** HTTP DELETE */ + DELETE = 'DELETE', + /** HTTP GET */ + GET = 'GET', + /** HTTP HEAD */ + HEAD = 'HEAD', + /** HTTP OPTIONS */ + OPTIONS = 'OPTIONS', + /** HTTP PATCH */ + PATCH = 'PATCH', + /** HTTP POST */ + POST = 'POST', + /** HTTP PUT */ + PUT = 'PUT', +} + +/** + * HTTP route in APIGateway is a combination of the HTTP method and the path component. + * This class models that combination. + */ +export class HttpRouteKey { + /** + * The catch-all route of the API, i.e., when no other routes match + */ + public static readonly DEFAULT = new HttpRouteKey('$default'); + + /** + * Create a route key with the combination of the path and the method. + * @param method default is 'ANY' + */ + public static with(path: string, method?: HttpMethod) { + if (path !== '/' && (!path.startsWith('/') || path.endsWith('/'))) { + throw new Error('path must always start with a "/" and not end with a "/"'); + } + return new HttpRouteKey(`${method ?? 'ANY'} ${path}`, path); + } + + /** + * The key to the RouteKey as recognized by APIGateway + */ + public readonly key: string; + /** + * The path part of this RouteKey. + * Returns `undefined` when `RouteKey.DEFAULT` is used. + */ + public readonly path?: string; + + private constructor(key: string, path?: string) { + this.key = key; + this.path = path; + } +} + +/** + * Options used when configuring multiple routes, at once. + * The options here are the ones that would be configured for all being set up. + */ +export interface BatchHttpRouteOptions { + /** + * The integration to be configured on this route. + */ + readonly integration: IHttpRouteIntegration; +} + +/** + * Properties to initialize a new Route + */ +export interface HttpRouteProps extends BatchHttpRouteOptions { + /** + * the API the route is associated with + */ + readonly httpApi: IHttpApi; + + /** + * The key to this route. This is a combination of an HTTP method and an HTTP path. + */ + readonly routeKey: HttpRouteKey; +} + +/** + * Route class that creates the Route for API Gateway HTTP API + * @resource AWS::ApiGatewayV2::Route + */ +export class HttpRoute extends Resource implements IHttpRoute { + public readonly routeId: string; + public readonly httpApi: IHttpApi; + public readonly path?: string; + + constructor(scope: Construct, id: string, props: HttpRouteProps) { + super(scope, id); + + this.httpApi = props.httpApi; + this.path = props.routeKey.path; + + let integration: HttpIntegration | undefined; + const config = props.integration.bind(this); + integration = new HttpIntegration(this, `${this.node.id}-Integration`, { + httpApi: props.httpApi, + integrationType: config.type, + integrationUri: config.uri, + method: config.method, + payloadFormatVersion: config.payloadFormatVersion, + }); + + const routeProps: CfnRouteProps = { + apiId: props.httpApi.httpApiId, + routeKey: props.routeKey.key, + target: `integrations/${integration.integrationId}`, + }; + + const route = new CfnRoute(this, 'Resource', routeProps); + this.routeId = route.ref; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts new file mode 100644 index 0000000000000..1335cbf839984 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -0,0 +1,65 @@ +import { Construct, Resource, Stack } from '@aws-cdk/core'; +import { CfnStage } from '../apigatewayv2.generated'; +import { CommonStageOptions, IStage } from '../common'; +import { IHttpApi } from './api'; + +const DEFAULT_STAGE_NAME = '$default'; + +/** + * Options to create a new stage for an HTTP API. + */ +export interface HttpStageOptions extends CommonStageOptions { +} + +/** + * Properties to initialize an instance of `HttpStage`. + */ +export interface HttpStageProps extends HttpStageOptions { + /** + * The HTTP API to which this stage is associated. + */ + readonly httpApi: IHttpApi; +} + +/** + * 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); + } + + public readonly stageName: string; + private httpApi: IHttpApi; + + constructor(scope: Construct, id: string, props: HttpStageProps) { + super(scope, id, { + physicalName: props.stageName ? props.stageName : DEFAULT_STAGE_NAME, + }); + + new CfnStage(this, 'Resource', { + apiId: props.httpApi.httpApiId, + stageName: this.physicalName, + autoDeploy: props.autoDeploy, + }); + + this.stageName = this.physicalName; + this.httpApi = props.httpApi; + } + + /** + * The URL to this stage. + */ + 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}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts index cbfa720284216..31ea86b4a91c2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -1,2 +1,3 @@ -// AWS::APIGatewayv2 CloudFormation Resources: export * from './apigatewayv2.generated'; +export * from './common'; +export * from './http'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 9b41125b702b7..bfb1d13b0f29f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -82,22 +82,36 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "peerDependencies": { + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { "node": ">= 10.13.0" }, + "awslint": { + "exclude": [ + "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", + "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", + "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" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/apigatewayv2.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/apigatewayv2.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/test/apigatewayv2.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts new file mode 100644 index 0000000000000..3f89ff312b14f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -0,0 +1,107 @@ +import '@aws-cdk/assert/jest'; +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { HttpApi, HttpMethod, LambdaProxyIntegration } from '../../lib'; + +describe('HttpApi', () => { + test('default', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'api'); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'HTTP', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.httpApiId), + StageName: '$default', + AutoDeploy: true, + }); + + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Route'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Integration'); + + expect(api.url).toBeDefined(); + }); + + test('unsetting createDefaultStage', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'api', { + createDefaultStage: false, + }); + + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Stage'); + expect(api.url).toBeUndefined(); + }); + + test('default integration', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api', { + defaultIntegration: new LambdaProxyIntegration({ + handler: new Function(stack, 'fn', { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }), + }), + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(httpApi.httpApiId), + RouteKey: '$default', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(httpApi.httpApiId), + }); + }); + + test('addRoutes() configures the correct routes', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api'); + + httpApi.addRoutes({ + path: '/pets', + methods: [ HttpMethod.GET, HttpMethod.PATCH ], + integration: new LambdaProxyIntegration({ + handler: new Function(stack, 'fn', { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }), + }), + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(httpApi.httpApiId), + RouteKey: 'GET /pets', + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(httpApi.httpApiId), + RouteKey: 'PATCH /pets', + }); + }); + + test('addRoutes() creates the default method', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api'); + + httpApi.addRoutes({ + path: '/pets', + integration: new LambdaProxyIntegration({ + handler: new Function(stack, 'fn', { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }), + }), + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(httpApi.httpApiId), + RouteKey: 'ANY /pets', + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts new file mode 100644 index 0000000000000..a8d69ac1009d9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts @@ -0,0 +1,41 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { HttpApi, HttpMethod, HttpProxyIntegration, HttpRoute, HttpRouteKey } from '../../../lib'; + +describe('HttpProxyIntegration', () => { + test('default', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyRoute', { + httpApi: api, + integration: new HttpProxyIntegration({ + url: 'some-target-url', + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'some-target-url', + PayloadFormatVersion: '1.0', + IntegrationMethod: 'ANY', + }); + }); + + test('method option is correctly recognized', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyRoute', { + httpApi: api, + integration: new HttpProxyIntegration({ + url: 'some-target-url', + method: HttpMethod.PATCH, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationMethod: 'PATCH', + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json new file mode 100644 index 0000000000000..475ef14f158ef --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json @@ -0,0 +1,236 @@ +{ + "Resources": { + "AlwaysSuccessServiceRole6DB8C2F6": { + "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" + ] + ] + } + ] + } + }, + "AlwaysSuccess099EAB05": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"success\" }; };" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "AlwaysSuccessServiceRole6DB8C2F6", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "AlwaysSuccessServiceRole6DB8C2F6" + ] + }, + "AlwaysSuccessinteghttpproxyLambdaProxyApiDefaultRoute17D52FE1Permission3B39FF57": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "LambdaProxyApi67594471" + }, + "/*/*" + ] + ] + } + } + }, + "LambdaProxyApi67594471": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "LambdaProxyApi", + "ProtocolType": "HTTP" + } + }, + "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "LambdaProxyApiDefaultRoute1EB30A46": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B" + } + ] + ] + } + } + }, + "LambdaProxyApiDefaultStage07C38681": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "HttpProxyApiD0217C67": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyApiDefaultRouteDefaultRouteIntegrationF2E17850": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyApiD0217C67" + }, + "IntegrationType": "HTTP_PROXY", + "IntegrationMethod": "ANY", + "IntegrationUri": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "LambdaProxyApi67594471" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + }, + "PayloadFormatVersion": "1.0" + } + }, + "HttpProxyApiDefaultRoute8AF66B5C": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyApiD0217C67" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyApiDefaultRouteDefaultRouteIntegrationF2E17850" + } + ] + ] + } + } + }, + "HttpProxyApiDefaultStageA88F9DE3": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyApiD0217C67" + }, + "StageName": "$default", + "AutoDeploy": true + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpProxyApiD0217C67" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.ts new file mode 100644 index 0000000000000..15db80661e3f1 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.ts @@ -0,0 +1,39 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { HttpApi, HttpProxyIntegration, LambdaProxyIntegration } from '../../../lib'; + +/* + * Stack verification steps: + * "curl " should return 'success' + */ + +const app = new App(); + +const stack = new Stack(app, 'integ-http-proxy'); + +// first create a lambda proxy endpoint that we can use as an HTTP proxy +const lambdaEndpoint = lambdaProxyEndpoint(stack); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyApi', { + defaultIntegration: new HttpProxyIntegration({ + url: lambdaEndpoint.url!, + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: httpEndpoint.url!, +}); + +function lambdaProxyEndpoint(s: Stack): HttpApi { + const handler = new lambda.Function(s, 'AlwaysSuccess', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, body: "success" }; };'), + }); + + return new HttpApi(s, 'LambdaProxyApi', { + defaultIntegration: new LambdaProxyIntegration({ + handler, + }), + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json new file mode 100644 index 0000000000000..ff35db0f3c0d5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json @@ -0,0 +1,168 @@ +{ + "Resources": { + "AlwaysSuccessServiceRole6DB8C2F6": { + "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" + ] + ] + } + ] + } + }, + "AlwaysSuccess099EAB05": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"success\" }; };" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "AlwaysSuccessServiceRole6DB8C2F6", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "AlwaysSuccessServiceRole6DB8C2F6" + ] + }, + "AlwaysSuccessinteglambdaproxyLambdaProxyApiDefaultRoute59CA2390Permission90573BC0": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "LambdaProxyApi67594471" + }, + "/*/*" + ] + ] + } + } + }, + "LambdaProxyApi67594471": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "LambdaProxyApi", + "ProtocolType": "HTTP" + } + }, + "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "LambdaProxyApiDefaultRoute1EB30A46": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B" + } + ] + ] + } + } + }, + "LambdaProxyApiDefaultStage07C38681": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "StageName": "$default", + "AutoDeploy": true + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "LambdaProxyApi67594471" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.ts new file mode 100644 index 0000000000000..2114f94729edf --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.ts @@ -0,0 +1,28 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { HttpApi, LambdaProxyIntegration } from '../../../lib'; + +/* + * Stack verification steps: + * "curl " should return 'success' + */ + +const app = new App(); + +const stack = new Stack(app, 'integ-lambda-proxy'); + +const handler = new lambda.Function(stack, 'AlwaysSuccess', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, body: "success" }; };'), +}); + +const endpoint = new HttpApi(stack, 'LambdaProxyApi', { + defaultIntegration: new LambdaProxyIntegration({ + handler, + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: endpoint.url!, +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts new file mode 100644 index 0000000000000..9984b8fdd9bff --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts @@ -0,0 +1,50 @@ +import '@aws-cdk/assert/jest'; +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { HttpApi, HttpRoute, HttpRouteKey, LambdaProxyIntegration, PayloadFormatVersion } from '../../../lib'; + +describe('LambdaProxyIntegration', () => { + test('default', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + const fooFn = fooFunction(stack, 'Fn'); + new HttpRoute(stack, 'LambdaProxyRoute', { + httpApi: api, + integration: new LambdaProxyIntegration({ + handler: fooFn, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationUri: stack.resolve(fooFn.functionArn), + PayloadFormatVersion: '2.0', + }); + }); + + test('payloadFormatVersion selection', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'LambdaProxyRoute', { + httpApi: api, + integration: new LambdaProxyIntegration({ + handler: fooFunction(stack, 'Fn'), + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + PayloadFormatVersion: '1.0', + }); + }); +}); + +function fooFunction(stack: Stack, id: string) { + return new Function(stack, id, { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts new file mode 100644 index 0000000000000..25df12528e63e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -0,0 +1,66 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { HttpApi, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteIntegration, + PayloadFormatVersion } from '../../lib'; + +describe('HttpRoute', () => { + test('default', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(httpApi.httpApiId), + RouteKey: 'GET /books', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'HttpRouteHttpRouteIntegration6EE0FE47', + }, + ], + ], + }, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(httpApi.httpApiId), + }); + }); + + test('integration is configured correctly', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(httpApi.httpApiId), + IntegrationType: 'HTTP_PROXY', + PayloadFormatVersion: '2.0', + IntegrationUri: 'some-uri', + }); + }); +}); + +class DummyIntegration implements IHttpRouteIntegration { + public bind(): HttpRouteIntegrationConfig { + return { + type: HttpIntegrationType.HTTP_PROXY, + payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, + uri: 'some-uri', + method: HttpMethod.DELETE, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts new file mode 100644 index 0000000000000..336d3a74852a4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -0,0 +1,40 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { HttpApi, HttpStage } from '../../lib'; + +describe('HttpStage', () => { + test('default', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + + new HttpStage(stack, 'Stage', { + httpApi: api, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.httpApiId), + StageName: '$default', + }); + }); + + test('url returns the correct path', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + + const defaultStage = new HttpStage(stack, 'DefaultStage', { + httpApi: api, + }); + + const betaStage = new HttpStage(stack, 'BetaStage', { + httpApi: api, + stageName: 'beta', + }); + + expect(defaultStage.url.endsWith('/')).toBe(true); + expect(betaStage.url.endsWith('/')).toBe(false); + }); +}); \ No newline at end of file