From 615ebbb630448c74f5063b2a74efc978e7b9fa6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 31 Mar 2020 17:31:31 +0200 Subject: [PATCH 1/7] feat(module): l2 construct for api gateway v2 Initial implementation of the API Gateway V2 Constructs. Implements all base APIGW constructs as L2 Constructs. fixes #5301 fixes #2872 fixes #212 --- .../aws-apigatewayv2/lib/api-mapping.ts | 95 +++++ packages/@aws-cdk/aws-apigatewayv2/lib/api.ts | 327 +++++++++++++++ .../aws-apigatewayv2/lib/authorizer.ts | 187 +++++++++ .../aws-apigatewayv2/lib/deployment.ts | 168 ++++++++ .../aws-apigatewayv2/lib/domain-name.ts | 142 +++++++ .../@aws-cdk/aws-apigatewayv2/lib/index.ts | 13 + .../lib/integration-response.ts | 131 ++++++ .../aws-apigatewayv2/lib/integration.ts | 385 ++++++++++++++++++ .../lib/integrations/index.ts | 1 + .../lib/integrations/lambda-integration.ts | 68 ++++ .../aws-apigatewayv2/lib/json-schema.ts | 352 ++++++++++++++++ .../@aws-cdk/aws-apigatewayv2/lib/model.ts | 176 ++++++++ .../aws-apigatewayv2/lib/route-response.ts | 125 ++++++ .../@aws-cdk/aws-apigatewayv2/lib/route.ts | 279 +++++++++++++ .../@aws-cdk/aws-apigatewayv2/lib/stage.ts | 229 +++++++++++ .../@aws-cdk/aws-apigatewayv2/package.json | 31 ++ .../aws-apigatewayv2/test/api-mapping.test.ts | 27 ++ .../aws-apigatewayv2/test/api.test.ts | 112 +++++ .../test/apigatewayv2.test.ts | 6 - .../aws-apigatewayv2/test/authorizer.test.ts | 32 ++ .../aws-apigatewayv2/test/domain-name.test.ts | 27 ++ .../aws-apigatewayv2/test/integration.test.ts | 93 +++++ .../aws-apigatewayv2/test/route.test.ts | 81 ++++ .../aws-apigatewayv2/test/stage.test.ts | 31 ++ 24 files changed, 3112 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/model.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/route.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/apigatewayv2.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts new file mode 100644 index 0000000000000..3d2967fa12bae --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts @@ -0,0 +1,95 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { IApi } from './api'; +import { CfnApiMapping } from './apigatewayv2.generated'; +import { IDomainName } from './domain-name'; +import { IStage } from './stage'; + +/** + * Defines the contract for an Api Gateway V2 Api Mapping. + */ +export interface IApiMapping extends IResource { + /** + * The ID of this API Gateway Api Mapping. + * @attribute + */ + readonly apiMappingId: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Api Mapping. + * + * This interface is used by the helper methods in `Api` and the sub-classes + */ +export interface ApiMappingOptions { + /** + * The API mapping key. + * + * @default - no API mapping key + */ + readonly apiMappingKey?: string; + + /** + * The associated domain name + */ + readonly domainName: IDomainName | string; + + /** + * The API stage. + */ + readonly stage: IStage; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Api Mapping. + */ +export interface ApiMappingProps extends ApiMappingOptions { + /** + * Defines the api for this deployment. + */ + readonly api: IApi; +} + +/** + * An Api Mapping for an API. An API mapping relates a path of your custom domain name to a stage of your API. + * + * A custom domain name can have multiple API mappings, but the paths can't overlap. + * + * A custom domain can map only to APIs of the same protocol type. + */ +export class ApiMapping extends Resource implements IApiMapping { + + /** + * Creates a new imported API + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param apiMappingId Identifier of the API Mapping + */ + public static fromApiMappingId(scope: Construct, id: string, apiMappingId: string): IApiMapping { + class Import extends Resource implements IApiMapping { + public readonly apiMappingId = apiMappingId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Api Mapping. + */ + public readonly apiMappingId: string; + + protected resource: CfnApiMapping; + + constructor(scope: Construct, id: string, props: ApiMappingProps) { + super(scope, id); + + this.resource = new CfnApiMapping(this, 'Resource', { + ...props, + apiId: props.api.apiId, + domainName: ((typeof(props.domainName) === "string") ? props.domainName : props.domainName.domainName), + stage: props.stage.stageName + }); + this.apiMappingId = this.resource.ref; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts new file mode 100644 index 0000000000000..d170bb6101eed --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts @@ -0,0 +1,327 @@ +import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; + +import { CfnApi } from './apigatewayv2.generated'; +import { Deployment } from './deployment'; +import { Integration } from './integration'; +import { LambdaIntegration, LambdaIntegrationOptions } from './integrations/lambda-integration'; +import { JsonSchema } from './json-schema'; +import { Model, ModelOptions } from './model'; +import { IRoute, KnownRouteKey } from './route'; +import { IStage, Stage, StageOptions } from './stage'; + +/** + * Available protocols for ApiGateway V2 APIs (currently only 'WEBSOCKET' is supported) + */ +export enum ProtocolType { + /** + * WebSocket API + */ + WEBSOCKET = "WEBSOCKET", + + /** + * HTTP API + */ + HTTP = "HTTP" +} + +/** + * Defines the contract for an Api Gateway V2 Api. + */ +export interface IApi extends IResource { + /** + * The ID of this API Gateway Api. + * @attribute + */ + readonly apiId: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Api. + */ +export interface ApiProps { + /** + * Indicates if a Deployment should be automatically created for this API, + * and recreated when the API model (route, integration) changes. + * + * Since API Gateway deployments are immutable, When this option is enabled + * (by default), an AWS::ApiGatewayV2::Deployment resource will automatically + * created with a logical ID that hashes the API model (methods, resources + * and options). This means that when the model changes, the logical ID of + * this CloudFormation resource will change, and a new deployment will be + * created. + * + * If this is set, `latestDeployment` will refer to the `Deployment` object + * and `deploymentStage` will refer to a `Stage` that points to this + * deployment. To customize the stage options, use the `deployOptions` + * property. + * + * A CloudFormation Output will also be defined with the root URL endpoint + * of this REST API. + * + * @default true + */ + readonly deploy?: boolean; + + /** + * Options for the API Gateway stage that will always point to the latest + * deployment when `deploy` is enabled. If `deploy` is disabled, + * this value cannot be set. + * + * @default - default options + */ + readonly deployOptions?: StageOptions; + + /** + * Retains old deployment resources when the API changes. This allows + * manually reverting stages to point to old deployments via the AWS + * Console. + * + * @default false + */ + readonly retainDeployments?: boolean; + + /** + * A name for the API Gateway Api resource. + * + * @default - ID of the Api construct. + */ + readonly apiName?: string; + + /** + * Available protocols for ApiGateway V2 APIs (currently only 'WEBSOCKET' is supported) + * + * @default 'WEBSOCKET' + */ + readonly protocolType?: ProtocolType | string; + + /** + * Expression used to select the route for this API + * + * @default '${request.body.action}' + */ + readonly routeSelectionExpression?: KnownRouteKey | string; + + /** + * Expression used to select the Api Key to use for metering + * + * @default - No Api Key + */ + readonly apiKeySelectionExpression?: string; + + /** + * A description of the purpose of this API Gateway Api resource. + * + * @default - No description. + */ + readonly description?: string; + + /** + * Indicates whether schema validation will be disabled for this Api + * + * @default false + */ + readonly disableSchemaValidation?: boolean; + + /** + * Indicates the version number for this Api + * + * @default false + */ + readonly version?: string; +} + +/** + * Represents a WebSocket API in Amazon API Gateway v2. + * + * Use `addModel` and `addLambdaIntegration` to configure the API model. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + */ +export class Api extends Resource implements IApi { + + /** + * Creates a new imported API + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param apiId Identifier of the API + */ + public static fromApiId(scope: Construct, id: string, apiId: string): IApi { + class Import extends Resource implements IApi { + public readonly apiId = apiId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Api. + */ + public readonly apiId: string; + + /** + * API Gateway stage that points to the latest deployment (if defined). + */ + public deploymentStage?: Stage; + + protected resource: CfnApi; + protected deployment?: Deployment; + + constructor(scope: Construct, id: string, props?: ApiProps) { + if (props === undefined) { + props = {}; + } + super(scope, id, { + physicalName: props.apiName || id, + }); + + this.resource = new CfnApi(this, 'Resource', { + ...props, + protocolType: props.protocolType || ProtocolType.WEBSOCKET, + routeSelectionExpression: props.routeSelectionExpression || '${request.body.action}', + name: this.physicalName + }); + this.apiId = this.resource.ref; + + const deploy = props.deploy === undefined ? true : props.deploy; + if (deploy) { + // encode the stage name into the construct id, so if we change the stage name, it will recreate a new stage. + // stage name is part of the endpoint, so that makes sense. + const stageName = (props.deployOptions && props.deployOptions.stageName) || 'prod'; + + this.deployment = new Deployment(this, 'Deployment', { + api: this, + description: 'Automatically created by the Api construct', + + // No stageName specified, this will be defined by the stage directly, as it will reference the deployment + retainDeployments: props.retainDeployments + }); + + this.deploymentStage = new Stage(this, `Stage.${stageName}`, { + ...props.deployOptions, + deployment: this.deployment, + api: this, + stageName, + description: 'Automatically created by the Api construct' + }); + } else { + if (props.deployOptions) { + throw new Error(`Cannot set 'deployOptions' if 'deploy' is disabled`); + } + } + } + + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * This will be undefined if `deploy` is false. + */ + public get latestDeployment() { + return this.deployment; + } + + /** + * Defines an API Gateway Lambda integration. + * @param id The construct id + * @param props Lambda integration options + */ + public addLambdaIntegration(id: string, props: LambdaIntegrationOptions): Integration { + return new LambdaIntegration(this, id, { ...props, api: this }); + } + + /** + * Defines a model for this Api Gateway. + * @param schema The model schema + * @param props The model integration options + */ + public addModel(schema: JsonSchema, props?: ModelOptions): Model { + return new Model(this, `Model.${schema.title}`, { + ...props, + modelName: schema.title, + api: this, + schema + }); + } + + /** + * Returns the ARN for a specific route and stage. + * + * @param route The route for this ARN ('*' if not defined) + * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') + */ + public executeApiArn(route?: IRoute, stage?: IStage) { + const stack = Stack.of(this); + const apiId = this.apiId; + const routeKey = ((route === undefined) ? '*' : route.key); + const stageName = ((stage === undefined) ? + ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : + stage.stageName); + return stack.formatArn({ + service: 'execute-api', + resource: apiId, + sep: '/', + resourceName: `${stageName}/${routeKey}` + }); + } + + /** + * Returns the ARN for a specific connection. + * + * @param connectionId The identifier of this connection ('*' if not defined) + * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') + */ + public connectionsApiArn(connectionId: string = "*", stage?: IStage) { + const stack = Stack.of(this); + const apiId = this.apiId; + const stageName = ((stage === undefined) ? + ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : + stage.stageName); + return stack.formatArn({ + service: 'execute-api', + resource: apiId, + sep: '/', + resourceName: `${stageName}/POST/${connectionId}` + }); + } + + /** + * Returns the client URL for this Api, for a specific stage. + * + * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. + * @param stage The stage for this URL (if not defined, defaults to the deployment stage) + */ + public clientUrl(stage?: IStage): string { + const stack = Stack.of(this); + let stageName: string | undefined; + if (stage === undefined) { + if (this.deploymentStage === undefined) { + throw Error("No stage defined for this Api"); + } + stageName = this.deploymentStage.stageName; + } else { + stageName = stage.stageName; + } + return `wss://${this.apiId}.execute-api.${stack.region}.amazonaws.com/${stageName}`; + } + + /** + * Returns the connections URL for this Api, for a specific stage. + * + * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. + * @param stage The stage for this URL (if not defined, defaults to the deployment stage) + */ + public connectionsUrl(stage?: IStage): string { + const stack = Stack.of(this); + let stageName: string | undefined; + if (stage === undefined) { + if (this.deploymentStage === undefined) { + throw Error("No stage defined for this Api"); + } + stageName = this.deploymentStage.stageName; + } else { + stageName = stage.stageName; + } + return `https://${this.apiId}.execute-api.${stack.region}.amazonaws.com/${stageName}/@connections`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts new file mode 100644 index 0000000000000..9cfcdc951ea4b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts @@ -0,0 +1,187 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { IApi } from './api'; +import { CfnAuthorizer } from './apigatewayv2.generated'; + +/** + * The authorizer type + */ +export enum AuthorizerType { + /** + * For WebSocket APIs, specify REQUEST for a Lambda function using incoming request parameters + */ + REQUEST = "REQUEST", + + /** + * For HTTP APIs, specify JWT to use JSON Web Tokens + */ + JWT = "JWT" +} + +/** + * Specifies the configuration of a `JWT` authorizer. + * + * Required for the `JWT` authorizer type. + * + * Supported only for HTTP APIs. + */ +export interface JwtConfiguration { + /** + * A list of the intended recipients of the `JWT`. + * + * A valid `JWT` must provide an `aud` that matches at least one entry in this list. + * + * See RFC 7519. + * + * Supported only for HTTP APIs + * + * @default - no specified audience + */ + readonly audience?: string[]; + + /** + * The base domain of the identity provider that issues JSON Web Tokens. + * + * For example, an Amazon Cognito user pool has the following format: `https://cognito-idp.{region}.amazonaws.com/{userPoolId}`. + * + * Required for the `JWT` authorizer type. + * + * Supported only for HTTP APIs. + * + * @default - no issuer + */ + readonly issuer?: string; +} + +/** + * Defines the contract for an Api Gateway V2 Authorizer. + */ +export interface IAuthorizer extends IResource { + /** + * The ID of this API Gateway Api Mapping. + * @attribute + */ + readonly authorizerId: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Authorizer. + * + * This interface is used by the helper methods in `Api` and the sub-classes + */ +export interface AuthorizerOptions { + /** + * Specifies the required credentials as an IAM role for API Gateway to invoke the authorizer. + * + * To specify an IAM role for API Gateway to assume, use the role's Amazon Resource Name (ARN). + * + * To use resource-based permissions on the Lambda function, specify null. Supported only for `REQUEST` authorizers. + * + * @default - no credentials + */ + readonly authorizerCredentialsArn?: string; + + /** + * The authorizer type. + */ + readonly authorizerType: AuthorizerType; + + /** + * The authorizer's Uniform Resource Identifier (URI). + * + * For `REQUEST` authorizers, this must be a well-formed Lambda function URI, for example, + * `arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:{acct_id}:function:{function_name}/invocations`. + * + * In general, the URI has this form: `arn:aws:apigateway:{region}:lambda:path/{service_api}`, where `{region}` is the same as the region + * hosting the Lambda function, path indicates that the remaining substring in the URI should be treated as the path to the resource, + * including the initial `/`. For Lambda functions, this is usually of the form `/2015-03-31/functions/{function_arn}/invocations`. + */ + readonly authorizerUri: string; + + /** + * The identity source for which authorization is requested. + * + * For a `REQUEST` authorizer, this is optional. The value is a set of one or more mapping expressions of the specified request parameters. + * Currently, the identity source can be headers, query string parameters, stage variables, and context parameters. + * For example, if an Auth header and a Name query string parameter are defined as identity sources, this value is `route.request.header.Auth`, + * `route.request.querystring.Name`. These parameters will be used to perform runtime validation for Lambda-based authorizers by verifying all + * of the identity-related request parameters are present in the request, not null, and non-empty. Only when this is true does the authorizer + * invoke the authorizer Lambda function. Otherwise, it returns a 401 Unauthorized response without calling the Lambda function. + * + * For JWT, a single entry that specifies where to extract the JSON Web Token (JWT) from inbound requests. Currently only header-based and query + * parameter-based selections are supported, for example `$request.header.Authorization`. + * + * @default - no identity source found + */ + readonly identitySource?: string[]; + + /** + * The JWTConfiguration property specifies the configuration of a JWT authorizer. + * + * Required for the JWT authorizer type. + * + * Supported only for HTTP APIs. + * + * @default - only required for HTTP APIs + */ + readonly jwtConfiguration?: JwtConfiguration; + + /** + * The name of the authorizer. + */ + readonly authorizerName: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Authorizer. + */ +export interface AuthorizerProps extends AuthorizerOptions { + /** + * Defines the api for this deployment. + */ + readonly api: IApi; +} + +/** + * An Api Mapping for an API. An API mapping relates a path of your custom domain name to a stage of your API. + * + * A custom domain name can have multiple API mappings, but the paths can't overlap. + * + * A custom domain can map only to APIs of the same protocol type. + */ +export class Authorizer extends Resource implements IAuthorizer { + + /** + * Creates a new imported API + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param authorizerId Identifier of the API Mapping + */ + public static fromAuthorizerId(scope: Construct, id: string, authorizerId: string): IAuthorizer { + class Import extends Resource implements IAuthorizer { + public readonly authorizerId = authorizerId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Api Mapping. + */ + public readonly authorizerId: string; + + protected resource: CfnAuthorizer; + + constructor(scope: Construct, id: string, props: AuthorizerProps) { + super(scope, id); + + this.resource = new CfnAuthorizer(this, 'Resource', { + ...props, + identitySource: (props.identitySource ? props.identitySource : []), + apiId: props.api.apiId, + name: props.authorizerName + }); + this.authorizerId = this.resource.ref; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts new file mode 100644 index 0000000000000..1e98dd62aedf4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts @@ -0,0 +1,168 @@ +import { CfnResource, Construct, IResource, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; + +import { IApi } from './api'; +import { CfnDeployment } from './apigatewayv2.generated'; + +import { createHash } from 'crypto'; + +/** + * Defines the contract for an Api Gateway V2 Deployment. + */ +export interface IDeployment extends IResource { + /** + * The ID of this API Gateway Deployment. + * @attribute + */ + readonly deploymentId: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Deployment. + */ +export interface DeploymentProps { + /** + * Defines the api for this deployment. + */ + readonly api: IApi; + + /** + * A description for this Deployment. + * + * @default - No description. + */ + readonly description?: string; + + /** + * The stage name. Stage names can only contain alphanumeric characters, hyphens, and underscores. Maximum length is 128 characters. + * + * @default - All stages. + */ + readonly stageName?: string; + + /** + * Retains old deployment resources when the API changes. This allows + * manually reverting stages to point to old deployments via the AWS + * Console. + * + * @default false + */ + readonly retainDeployments?: boolean; +} + +/** + * A Deployment of an API. + * + * An immutable representation of an Api resource that can be called by users + * using Stages. A deployment must be associated with a Stage for it to be + * callable over the Internet. + * + * Normally, you don't need to define deployments manually. The Api + * construct manages a Deployment resource that represents the latest model. It + * can be accessed through `api.latestDeployment` (unless `deploy: false` is + * set when defining the `Api`). + * + * If you manually define this resource, you will need to know that since + * deployments are immutable, as long as the resource's logical ID doesn't + * change, the deployment will represent the snapshot in time in which the + * resource was created. This means that if you modify the RestApi model (i.e. + * add methods or resources), these changes will not be reflected unless a new + * deployment resource is created. + * + * To achieve this behavior, the method `addToLogicalId(data)` can be used to + * augment the logical ID generated for the deployment resource such that it + * will include arbitrary data. This is done automatically for the + * `api.latestDeployment` deployment. + * + * Furthermore, since a deployment does not reference any of the API + * resources and methods, CloudFormation will likely provision it before these + * resources are created, which means that it will represent a "half-baked" + * model. Use the `registerDependency(dep)` method to circumvent that. This is done + * automatically for the `api.latestDeployment` deployment. + */ +export class Deployment extends Resource implements IDeployment { + /** + * Creates a new imported API Deployment + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param deploymentId Identifier of the Deployment + */ + public static fromDeploymentId(scope: Construct, id: string, deploymentId: string): IDeployment { + class Import extends Resource implements IDeployment { + public readonly deploymentId = deploymentId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Deployment. + */ + public readonly deploymentId: string; + + protected resource: CfnDeployment; + private hashComponents = new Array(); + private originalLogicalId: string; + + constructor(scope: Construct, id: string, props: DeploymentProps) { + super(scope, id); + + this.resource = new CfnDeployment(this, 'Resource', { + apiId: props.api.apiId, + description: props.description, + stageName: props.stageName + }); + + if ((props.retainDeployments === undefined) || (props.retainDeployments === true)) { + this.resource.applyRemovalPolicy(RemovalPolicy.RETAIN); + } + this.deploymentId = Lazy.stringValue({ produce: () => this.resource.ref }); + this.originalLogicalId = Stack.of(this).getLogicalId(this.resource); + } + + /** + * Adds a component to the hash that determines this Deployment resource's + * logical ID. + * + * This should be called by constructs of the API Gateway model that want to + * invalidate the deployment when their settings change. The component will + * be resolved during synthesis so tokens are welcome. + * + * @param data The data to add to this hash + */ + public addToLogicalId(data: any) { + if (this.node.locked) { + throw new Error('Cannot modify the logical ID when the construct is locked'); + } + + this.hashComponents.push(data); + } + + /** + * Adds a dependency to the Deployment node, avoiding partial deployments. + * + * This is done automatically for the `api.latestDeployment` deployment. + * + * @param dependency The construct to add in the dependency tree + */ + public registerDependency(dependency: CfnResource) { + this.resource.addDependsOn(dependency); + } + + /** + * Hooks into synthesis to calculate a logical ID that hashes all the components + * add via `addToLogicalId`. + */ + protected prepare() { + const stack = Stack.of(this); + + // if hash components were added to the deployment, we use them to calculate + // a logical ID for the deployment resource. + if (this.hashComponents.length > 0) { + const md5 = createHash('md5'); + this.hashComponents.map(c => stack.resolve(c)).forEach(c => md5.update(JSON.stringify(c))); + this.resource.overrideLogicalId(this.originalLogicalId + md5.digest("hex").substr(0, 8).toUpperCase()); + } + super.prepare(); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts new file mode 100644 index 0000000000000..20ae3792f9bed --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts @@ -0,0 +1,142 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { CfnDomainName } from './apigatewayv2.generated'; + +/** + * Represents an endpoint type + */ +export enum EndpointType { + /** + * Regional endpoint + */ + REGIONAL = "REGIONAL", + + /** + * Edge endpoint + */ + EDGE = "EDGE" +} + +/** + * Specifies the configuration for a an API's domain name. + */ +export interface DomainNameConfiguration { + /** + * An AWS-managed certificate that will be used by the edge-optimized endpoint for this domain name. + * AWS Certificate Manager is the only supported source. + * + * @default - uses `certificateName` if defined, or no certificate + */ + readonly certificateArn?: string; + + /** + * The user-friendly name of the certificate that will be used by the edge-optimized endpoint for this domain name. + * + * @default - uses `certificateArn` if defined, or no certificate + */ + readonly certificateName?: string; + + /** + * The endpoint type. + * + * @default 'REGIONAL' + */ + readonly endpointType?: EndpointType; +} + +/** + * Defines the attributes for an Api Gateway V2 Domain Name. + */ +export interface DomainNameAttributes { + /** + * The custom domain name for your API in Amazon API Gateway. + */ + readonly domainName: string; +} + +/** + * Defines the contract for an Api Gateway V2 Domain Name. + */ +export interface IDomainName extends IResource { + /** + * The custom domain name for your API in Amazon API Gateway. + * @attribute + */ + readonly domainName: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Api Mapping. + */ +export interface DomainNameProps { + /** + * The custom domain name for your API in Amazon API Gateway. + */ + readonly domainName: string; + + /** + * The domain name configurations. + * + * @default - no specific configuration + */ + readonly domainNameConfigurations?: DomainNameConfiguration[]; + // TODO: Tags +} + +/** + * A Domain Name for an API. An API mapping relates a path of your custom domain name to a stage of your API. + * + * A custom domain name can have multiple API mappings, but the paths can't overlap. + * + * A custom domain can map only to APIs of the same protocol type. + */ +export class DomainName extends Resource implements IDomainName { + + /** + * Creates a new imported Domain Name + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param attrs domain name attributes + */ + public static fromDomainNameAttributes(scope: Construct, id: string, attrs: DomainNameAttributes): IDomainName { + class Import extends Resource implements IDomainName { + public readonly domainName = attrs.domainName; + } + + return new Import(scope, id); + } + + /** + * The custom domain name for your API in Amazon API Gateway. + */ + public readonly domainName: string; + + /** + * The domain name associated with the regional endpoint for this custom domain name. + * You set up this association by adding a DNS record that points the custom domain name to this regional domain name. + * + * @attribute + */ + public readonly regionalDomainName: string; + + /** + * The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint. + * + * @attribute + */ + public readonly regionalHostedZoneId: string; + + private resource: CfnDomainName; + + constructor(scope: Construct, id: string, props: DomainNameProps) { + super(scope, id); + + this.resource = new CfnDomainName(this, 'Resource', { + ...props + }); + this.domainName = this.resource.ref; + this.regionalDomainName = this.resource.attrRegionalDomainName; + this.regionalHostedZoneId = this.resource.attrRegionalHostedZoneId; + } +} \ 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..8b231848b079f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -1,2 +1,15 @@ // AWS::APIGatewayv2 CloudFormation Resources: export * from './apigatewayv2.generated'; +export * from './api'; +export * from './api-mapping'; +export * from './authorizer'; +export * from './deployment'; +export * from './domain-name'; +export * from './integration'; +export * from './integration-response'; +export * from './integrations'; +export * from './json-schema'; +export * from './model'; +export * from './route'; +export * from './route-response'; +export * from './stage'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts new file mode 100644 index 0000000000000..f1dc711afc006 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts @@ -0,0 +1,131 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { Api, IApi } from './api'; +import { CfnIntegrationResponse } from './apigatewayv2.generated'; +import { ContentHandlingStrategy, IIntegration, KnownTemplateKey } from './integration'; + +/** + * Defines a set of common response patterns known to the system + */ +export enum KnownIntegrationResponseKey { + /** + * Default response, when no other pattern matches + */ + DEFAULT = "$default", + + /** + * Empty response + */ + EMPTY = "empty", + + /** + * Error response + */ + ERROR = "error" +} + +/** + * Defines the contract for an Api Gateway V2 Deployment. + */ +export interface IIntegrationResponse extends IResource { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface IntegrationResponseOptions { + /** + * Specifies how to handle response payload content type conversions. + * + * Supported only for WebSocket APIs. + * + * @default - Pass through unmodified + */ + readonly contentHandlingStrategy?: ContentHandlingStrategy | string; + + /** + * A key-value map specifying response parameters that are passed to the method response from the backend. + * + * The key is a method response header parameter name and the mapped value is an integration response header value, + * a static value enclosed within a pair of single quotes, or a JSON expression from the integration response body. + * + * The mapping key must match the pattern of `method.response.header.{name}`, where name is a valid and unique header name. + * + * The mapped non-static value must match the pattern of `integration.response.header.{name}` or `integration.response.body.{JSON-expression}`, + * where `{name}` is a valid and unique response header name and `{JSON-expression}` is a valid JSON expression without the `$` prefix + * + * @default - no parameter used + */ + readonly responseParameters?: { [key: string]: string }; + + /** + * The collection of response templates for the integration response as a string-to-string map of key-value pairs. + * + * Response templates are represented as a key/value map, with a content-type as the key and a template as the value. + * + * @default - no templates used + */ + readonly responseTemplates?: { [key: string]: string }; + + /** + * The template selection expression for the integration response. + * + * Supported only for WebSocket APIs. + * + * @default - no template selected + */ + readonly templateSelectionExpression?: KnownTemplateKey | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + */ +export interface IntegrationResponseProps extends IntegrationResponseOptions { + /** + * Defines the api for this response. + */ + readonly api: IApi; + + /** + * Defines the parent integration for this response. + */ + readonly integration: IIntegration; + + /** + * The integration response key. + */ + readonly key: KnownIntegrationResponseKey | string; +} + +/** + * A response for an integration for an API in Amazon API Gateway v2. + */ +export class IntegrationResponse extends Resource implements IIntegrationResponse { + protected resource: CfnIntegrationResponse; + + constructor(scope: Construct, id: string, props: IntegrationResponseProps) { + super(scope, id); + + this.resource = new CfnIntegrationResponse(this, 'Resource', { + ...props, + apiId: props.api.apiId, + integrationId: props.integration.integrationId, + integrationResponseKey: props.key + }); + + if (props.api instanceof Api) { + if (props.api.latestDeployment) { + props.api.latestDeployment.addToLogicalId({ + ...props, + api: props.api.apiId, + integration: props.integration.integrationId, + id, + integrationResponseKey: props.key + }); + props.api.latestDeployment.registerDependency(this.resource); + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts new file mode 100644 index 0000000000000..454762cb16d35 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts @@ -0,0 +1,385 @@ +import { Construct, Duration, IResource, Resource } from '@aws-cdk/core'; + +import { Api, IApi } from './api'; +import { CfnIntegration } from './apigatewayv2.generated'; +import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from './integration-response'; +import { IRoute, KnownRouteKey, Route, RouteOptions } from './route'; + +/** + * The type of the network connection to the integration endpoint. + */ +export enum ConnectionType { + /** + * Internet connectivity through the public routable internet + */ + INTERNET = "INTERNET", + + /** + * Private connections between API Gateway and resources in a VPC + */ + VPC_LINK = " VPC_LINK" +} + +/** + * The integration type of an integration. + */ +export enum IntegrationType { + /** + * Integration of the route or method request with an AWS service action, including the Lambda function-invoking action. + * With the Lambda function-invoking action, this is referred to as the Lambda custom integration. + * With any other AWS service action, this is known as AWS integration. + */ + AWS = "AWS", + + /** + * Integration of the route or method request with the Lambda function-invoking action with the client request passed through as-is. + * This integration is also referred to as Lambda proxy integration. + */ + AWS_PROXY = "AWS_PROXY", + + /** + * Integration of the route or method request with an HTTP endpoint. + * This integration is also referred to as HTTP custom integration. + */ + HTTP = "HTTP", + + /** + * Integration of the route or method request with an HTTP endpoint, with the client request passed through as-is. + * This is also referred to as HTTP proxy integration. + */ + HTTP_PROXY = "HTTP_PROXY", + + /** + * Integration of the route or method request with API Gateway as a "loopback" endpoint without invoking any backend. + */ + MOCK = "MOCK" +} + +/** + * Specifies how to handle response payload content type conversions. Supported values are CONVERT_TO_BINARY and CONVERT_TO_TEXT. + * + * If this property is not defined, the response payload will be passed through from the integration response + * to the route response or method response without modification. + */ +export enum ContentHandlingStrategy { + /** + * Converts a response payload from a Base64-encoded string to the corresponding binary blob + */ + CONVERT_TO_BINARY = "CONVERT_TO_BINARY", + + /** + * Converts a response payload from a binary blob to a Base64-encoded string + */ + CONVERT_TO_TEXT = "CONVERT_TO_TEXT" +} + +/** + * Specifies the pass-through behavior for incoming requests based on the + * Content-Type header in the request, and the available mapping templates + * specified as the requestTemplates property on the Integration resource. + */ +export enum PassthroughBehavior { + /** + * Passes the request body for unmapped content types through to the + * integration backend without transformation + */ + WHEN_NO_MATCH = "WHEN_NO_MATCH", + /** + * Allows pass-through when the integration has no content types mapped + * to templates. However, if there is at least one content type defined, + * unmapped content types will be rejected with an HTTP 415 Unsupported Media Type response + */ + WHEN_NO_TEMPLATES = "WHEN_NO_TEMPLATES", + /** + * Rejects unmapped content types with an HTTP 415 Unsupported Media Type response + */ + NEVER = "NEVER" +} + +/** + * Defines a set of common template patterns known to the system + */ +export enum KnownTemplateKey { + /** + * Default template, when no other pattern matches + */ + DEFAULT = "$default" +} + +/** + * Specifies the integration's HTTP method type (only GET is supported for WebSocket) + */ +export enum IntegrationMethod { + /** + * GET HTTP Method + * + * Only method supported for WebSocket + */ + GET = "GET", + + /** + * POST HTTP Method + * + * Not supported for WebSocket + */ + POST = "POST", + + /** + * PUT HTTP Method + * + * Not supported for WebSocket + */ + PUT = "PUT", + + /** + * DELETE HTTP Method + * + * Not supported for WebSocket + */ + DELETE = "DELETE", + + /** + * OPTIONS HTTP Method + * + * Not supported for WebSocket + */ + OPTIONS = "OPTIONS", + + /** + * HEAD HTTP Method + * + * Not supported for WebSocket + */ + HEAD = "HEAD", + + /** + * PATCH HTTP Method + * + * Not supported for WebSocket + */ + PATCH = "PATCH" +} + +/** + * Defines the contract for an Api Gateway V2 Deployment. + */ +export interface IIntegration extends IResource { + /** + * The ID of this API Gateway Integration. + * @attribute + */ + readonly integrationId: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + * + * This interface is used by the helper methods in `Api` and the sub-classes + */ +export interface IntegrationOptions { + /** + * The type of the network connection to the integration endpoint. + * + * @default 'INTERNET' + */ + readonly connectionType?: ConnectionType | string; + + /** + * The integration type of an integration. + * + * @default - Pass through unmodified + */ + readonly contentHandlingStrategy?: ContentHandlingStrategy | string; + + /** + * Specifies the credentials required for the integration, if any. + * + * For AWS integrations, three options are available. + * - To specify an IAM Role for API Gateway to assume, use the role's Amazon Resource Name (ARN). + * - To require that the caller's identity be passed through from the request, specify the string `arn:aws:iam::*:user/*`. + * - To use resource-based permissions on supported AWS services, leave `undefined`. + * + * @default - resource-based permissions on supported AWS services + */ + readonly credentialsArn?: string; + + /** + * The description of the integration. + * + * @default - No description. + */ + readonly description?: string; + + /** + * Specifies the pass-through behavior for incoming requests based on the `Content-Type` header in the request, + * and the available mapping templates specified as the `requestTemplates` property on the `Integration` resource. + * + * @default - the response payload will be passed through from the integration response to the route response or method response unmodified + */ + readonly passthroughBehavior?: PassthroughBehavior | string; + + /** + * A key-value map specifying request parameters that are passed from the method request to the backend. + * The key is an integration request parameter name and the associated value is a method request parameter value or static value + * that must be enclosed within single quotes and pre-encoded as required by the backend. + * + * The method request parameter value must match the pattern of `method.request.{location}.{name}`, where `{location}` is + * `querystring`, `path`, or `header`; and `{name}` must be a valid and unique method request parameter name. + * + * Supported only for WebSocket APIs + * + * @default - no parameter used + */ + readonly requestParameters?: { [key: string]: string }; + + /** + * Represents a map of Velocity templates that are applied on the request payload based on the value of + * the `Content-Type` header sent by the client. The content type value is the key in this map, and the + * template is the value. + * + * Supported only for WebSocket APIs. + * + * @default - no templates used + */ + readonly requestTemplates?: { [key: string]: string }; + + /** + * The template selection expression for the integration. + * + * Supported only for WebSocket APIs. + * + * @default - no template selected + */ + readonly templateSelectionExpression?: KnownTemplateKey | string; + + /** + * Custom timeout between 50 and 29,000 milliseconds for WebSocket APIs and between 50 and 30,000 milliseconds for HTTP APIs. + * + * @default - timeout is 29 seconds for WebSocket APIs and 30 seconds for HTTP APIs. + */ + readonly timeout?: Duration; + + /** + * Specifies the integration's HTTP method type. + * + * @default - 'GET' + */ + readonly integrationMethod?: IntegrationMethod | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + */ +export interface IntegrationProps extends IntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IApi; + + /** + * The integration type of an integration. + */ + readonly type: IntegrationType | string; + + /** + * For a Lambda integration, specify the URI of a Lambda function. + * For an HTTP integration, specify a fully-qualified URL. + * For an HTTP API private integration, specify the ARN of an ALB listener, NLB listener, or AWS Cloud Map service. + * + * If you specify the ARN of an AWS Cloud Map service, API Gateway uses `DiscoverInstances` to identify resources. + * + * You can use query parameters to target specific resources. + * + * For private integrations, all resources must be owned by the same AWS account. + * + * This is directly handled by the specialized classes in `integrations/` + */ + readonly uri: string; +} + +/** + * An integration for an API in Amazon API Gateway v2. + * + * Use `addResponse` and `addRoute` to configure integration. + */ +export abstract class Integration extends Resource implements IIntegration { + /** + * The ID of this API Gateway Integration. + */ + public readonly integrationId: string; + + protected api: IApi; + protected resource: CfnIntegration; + + constructor(scope: Construct, id: string, props: IntegrationProps) { + super(scope, id); + this.api = props.api; + this.resource = new CfnIntegration(this, 'Resource', { + ...props, + timeoutInMillis: (props.timeout ? props.timeout.toMilliseconds() : undefined), + apiId: props.api.apiId, + integrationType: props.type, + integrationUri: props.uri + }); + + this.integrationId = this.resource.ref; + + if (props.api instanceof Api) { + if (props.api.latestDeployment) { + props.api.latestDeployment.addToLogicalId({ + ...props, + api: props.api.apiId, + id, + integrationType: props.type, + integrationUri: props.uri + }); + props.api.latestDeployment.registerDependency(this.resource); + } + } + } + + /** + * Adds a set of permission for a defined route. + * + * This is done automatically for routes created with the helper methods + * + * @param _route the route to define for the permissions + */ + public addPermissionsForRoute(_route: IRoute) { + // Override to define permissions for this integration + } + + /** + * Creates a new response for this integration. + * + * @param key the key (predefined or not) that will select this response + * @param props the properties for this response + */ + public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { + return new IntegrationResponse(this, `Response.${key}`, { + ...props, + api: this.api, + integration: this, + key + }); + } + + /** + * Creates a new route for this integration. + * + * @param key the route key (predefined or not) that will select this integration + * @param props the properties for this response + */ + public addRoute(key: KnownRouteKey | string, props?: RouteOptions): Route { + const route = new Route(this, `Route.${key}`, { + ...props, + api: this.api, + integration: this, + key + }); + + this.addPermissionsForRoute(route); + + return route; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts new file mode 100644 index 0000000000000..fa6637730bce9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts @@ -0,0 +1 @@ +export * from './lambda-integration'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts new file mode 100644 index 0000000000000..7c3c117d7233e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts @@ -0,0 +1,68 @@ +import { ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { Construct, Stack } from '@aws-cdk/core'; + +import { Api, IApi } from '../api'; +import { Integration, IntegrationOptions, IntegrationType } from '../integration'; +import { IRoute } from '../route'; + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface LambdaIntegrationOptions extends IntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; + + /** + * The Lambda function handler for this integration + */ + readonly handler: IFunction; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + */ +export interface LambdaIntegrationProps extends LambdaIntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IApi; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class LambdaIntegration extends Integration { + protected handler: IFunction; + + constructor(scope: Construct, id: string, props: LambdaIntegrationProps) { + const stack = Stack.of(scope); + + // This is not a standard ARN as it does not have the account-id part in it + const uri = `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`; + super(scope, id, { + ...props, + type: props.proxy ? IntegrationType.AWS_PROXY : IntegrationType.AWS, + uri + }); + this.handler = props.handler; + } + + public addPermissionsForRoute(route: IRoute) { + if (this.api instanceof Api) { + const sourceArn = this.api.executeApiArn(route); + this.handler.addPermission(`ApiPermission.${route.node.uniqueId}`, { + principal: new ServicePrincipal('apigateway.amazonaws.com'), + sourceArn + }); + } else { + throw new Error("This function is only supported on non-imported APIs"); + } + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts new file mode 100644 index 0000000000000..bdd8deb9935e7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts @@ -0,0 +1,352 @@ +/** + * Defines the version of the JSON Schema to use + */ +export enum JsonSchemaVersion { + /** + * In API Gateway models are defined using the JSON schema draft 4. + * @see https://tools.ietf.org/html/draft-zyp-json-schema-04 + */ + DRAFT4 = 'http://json-schema.org/draft-04/schema#', + + /** + * JSON schema Draft 7. + * + * In API Gateway models are defined using the JSON schema draft 4. + * @see https://tools.ietf.org/html/draft-zyp-json-schema-07 + */ + DRAFT7 = 'http://json-schema.org/draft-07/schema#' +} + +/** + * Defines a type in a JSON Schema + */ +export enum JsonSchemaType { + /** + * This type only allows null values + */ + NULL = "null", + + /** + * Boolean property + */ + BOOLEAN = "boolean", + + /** + * Object property, will have properties defined + */ + OBJECT = "object", + + /** + * Array object, will have an item type defined + */ + ARRAY = "array", + + /** + * Number property + */ + NUMBER = "number", + + /** + * Integer property (inherited from Number with extra constraints) + */ + INTEGER = "integer", + + /** + * String property + */ + STRING = "string" +} + +/** + * Represents a JSON schema definition of the structure of a + * REST API model. Copied from npm module jsonschema. + * + * @see http://json-schema.org/ + * @see https://github.com/tdegrunt/jsonschema + */ +export interface JsonSchema { + // Special keywords + /** + * Defines the version for this schema + * + * @default - JSON Schema version 4 will be used + */ + readonly schema?: JsonSchemaVersion; + + /** + * Defined an identifier for this schema + * + * @default - no identifier used + */ + readonly id?: string; + + /** + * Defines this schema as a reference to another schema + * + * @default - no references + */ + readonly ref?: string; + + // Common properties + /** + * Type of the element being described + * + * @default - untyped element (or leverages ref) + */ + readonly type?: JsonSchemaType | JsonSchemaType[]; + + /** + * Title of this schema + * + * @default - untitled schema + */ + readonly title?: string; + + /** + * Description of this schema + * + * @default - no description + */ + readonly description?: string; + + /** + * This schema is an enumeration + * + * @default - no values + */ + readonly 'enum'?: any[]; + + /** + * Format constraint on this element + * + * @default - no format constraint + */ + readonly format?: string; + + /** + * Contains the definitions of the properties + * + * @default - no definitions + */ + readonly definitions?: { [name: string]: JsonSchema }; + + // Number or Integer + /** + * Forces this number to be a multiple of a this property + * + * @default - no constraint + */ + readonly multipleOf?: number; + /** + * Upper bound (included) for this number + * + * @default - no constraint + */ + readonly maximum?: number; + /** + * Upper bound (excluded) for this number + * + * @default - no constraint + */ + readonly exclusiveMaximum?: boolean; + /** + * Lower bound (included) for this number + * + * @default - no constraint + */ + readonly minimum?: number; + /** + * Lower bound (excluded) for this number + * + * @default - no constraint + */ + readonly exclusiveMinimum?: boolean; + + // String + /** + * Maximum string length + * + * @default - no constraint + */ + readonly maxLength?: number; + /** + * Minimum string length + * + * @default - no constraint + */ + readonly minLength?: number; + /** + * String pattern to be enforced + * + * @default - no constraint + */ + readonly pattern?: string; + + // Array + /** + * Defines the types for the elements of this array + * + * @default - no constraint + */ + readonly items?: JsonSchema | JsonSchema[]; + /** + * Defines additional item types for this array + * + * @default - no constraint + */ + readonly additionalItems?: JsonSchema[]; + /** + * Maximum number of items in this array + * + * @default - no constraint + */ + readonly maxItems?: number; + /** + * Minimum number of items in this array + * + * @default - no constraint + */ + readonly minItems?: number; + /** + * Items in this array must be unique + * + * @default - no constraint + */ + readonly uniqueItems?: boolean; + /** + * Validates that the array contains specific items + * + * @default - no constraint + */ + readonly contains?: JsonSchema | JsonSchema[]; + + // Object + /** + * Maximum number of properties in this object + * + * @default - no constraint + */ + readonly maxProperties?: number; + /** + * Minimum number of properties in this object + * + * @default - no constraint + */ + readonly minProperties?: number; + /** + * Required properties + * + * @default - no constraint + */ + readonly required?: string[]; + /** + * Type definitions for the properties of the object + * + * @default - no constraint + */ + readonly properties?: { [name: string]: JsonSchema }; + /** + * Allows additional properties + * + * @default - no constraint + */ + readonly additionalProperties?: boolean; + /** + * Validated patterns for additional properties + * + * @default - no constraint + */ + readonly patternProperties?: { [name: string]: JsonSchema }; + /** + * Defines dependencies for this element + * + * @default - no constraint + */ + readonly dependencies?: { [name: string]: JsonSchema | string[] }; + /** + * If the instance is an object, this keyword validates if every + * property name in the instance validates against the provided schema. + * + * Note the property name that the schema is testing will always be a string. + * + * @default - no constraint + */ + readonly propertyNames?: JsonSchema; + + // Conditional + /** + * An instance validates successfully against this keyword if it validates successfully against + * all schemas defined by this keyword's value. + * + * @default - no constraint + */ + readonly allOf?: JsonSchema[]; + /** + * An instance validates successfully against this keyword if it validates successfully against + * at least one schema defined by this keyword's value. + * + * @default - no constraint + */ + readonly anyOf?: JsonSchema[]; + /** + * An instance validates successfully against this keyword if it validates successfully against + * exactly one schema defined by this keyword's value. + * + * @default - no constraint + */ + readonly oneOf?: JsonSchema[]; + /** + * An instance is valid against this keyword if it fails to validate successfully against the + * schema defined by this keyword. + * + * @default - no constraint + */ + readonly not?: JsonSchema; +} + +/** + * Utility class for Json Mapping + */ +export class JsonSchemaMapper { + /** + * Transforms naming of some properties to prefix with a $, where needed + * according to the JSON schema spec + * @param schema The JsonSchema object to transform for CloudFormation output + */ + public static toCfnJsonSchema(schema: JsonSchema): any { + const result = JsonSchemaMapper._toCfnJsonSchema(schema); + if (! ("$schema" in result)) { + result.$schema = JsonSchemaVersion.DRAFT4; + } + return result; + } + + private static readonly SchemaPropsWithPrefix: { [key: string]: string } = { + schema: '$schema', + ref: '$ref', + id: '$id' + }; + // The value indicates whether direct children should be key-mapped. + private static readonly SchemaPropsWithUserDefinedChildren: { [key: string]: boolean } = { + definitions: true, + properties: true, + patternProperties: true, + dependencies: true, + }; + + private static _toCfnJsonSchema(schema: any, preserveKeys = false): any { + if (schema == null || typeof schema !== 'object') { + return schema; + } + if (Array.isArray(schema)) { + return schema.map(entry => JsonSchemaMapper._toCfnJsonSchema(entry)); + } + return Object.assign({}, ...Object.entries(schema).map(([key, value]) => { + const mapKey = !preserveKeys && (key in JsonSchemaMapper.SchemaPropsWithPrefix); + const newKey = mapKey ? JsonSchemaMapper.SchemaPropsWithPrefix[key] : key; + // If keys were preserved, don't consider SchemaPropsWithUserDefinedChildren for those keys (they are user-defined!) + const newValue = JsonSchemaMapper._toCfnJsonSchema(value, !preserveKeys && JsonSchemaMapper.SchemaPropsWithUserDefinedChildren[key]); + return { [newKey]: newValue }; + })); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts new file mode 100644 index 0000000000000..6a4a89cc8ff7e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts @@ -0,0 +1,176 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { Api, IApi } from './api'; +import { CfnModel } from './apigatewayv2.generated'; +import { JsonSchema, JsonSchemaMapper } from './json-schema'; + +/** + * Defines a set of common model patterns known to the system + */ +export enum KnownModelKey { + /** + * Default model, when no other pattern matches + */ + DEFAULT = "$default", + + /** + * Default model, when no other pattern matches + */ + EMPTY = "" +} + +/** + * Defines a set of common content types for APIs + */ +export enum KnownContentTypes { + /** + * JSON request or response (default) + */ + JSON = "application/json", + /** + * XML request or response + */ + XML = "application/xml", + /** + * Pnain text request or response + */ + TEXT = "text/plain", + /** + * URL encoded web form + */ + FORM_URL_ENCODED = "application/x-www-form-urlencoded", + /** + * Data from a web form + */ + FORM_DATA = "multipart/form-data" +} + +/** + * Defines the attributes for an Api Gateway V2 Model. + */ +export interface ModelAttributes { + /** + * The ID of this API Gateway Model. + */ + readonly modelId: string; + + /** + * The name of this API Gateway Model. + */ + readonly modelName: string; +} + +/** + * Defines the contract for an Api Gateway V2 Model. + */ +export interface IModel extends IResource { + /** + * The ID of this API Gateway Model. + * @attribute + */ + readonly modelId: string; + + /** + * The name of this API Gateway Model. + * @attribute + */ + readonly modelName: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Model. + * + * This interface is used by the helper methods in `Api` + */ +export interface ModelOptions { + /** + * The content-type for the model, for example, `application/json`. + * + * @default "application/json" + */ + readonly contentType?: KnownContentTypes | string; + + /** + * The name of the model. + * + * @default - the physical id of the model + */ + readonly modelName?: string; + + /** + * The description of the model. + * + * @default - no description + */ + readonly description?: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Model. + */ +export interface ModelProps extends ModelOptions { + /** + * Defines the api for this response. + */ + readonly api: IApi; + + /** + * The schema for the model. For `application/json` models, this should be JSON schema draft 4 model. + */ + readonly schema: JsonSchema; +} + +/** + * A model for an API in Amazon API Gateway v2. + */ +export class Model extends Resource implements IModel { + /** + * Creates a new imported Model + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param attrs attributes of the API Model + */ + public static fromModelAttributes(scope: Construct, id: string, attrs: ModelAttributes): IModel { + class Import extends Resource implements IModel { + public readonly modelId = attrs.modelId; + public readonly modelName = attrs.modelName; + } + + return new Import(scope, id); + } + + public readonly modelId: string; + public readonly modelName: string; + protected resource: CfnModel; + + constructor(scope: Construct, id: string, props: ModelProps) { + super(scope, id, { + physicalName: props.modelName || id, + }); + + this.modelName = this.physicalName; + this.resource = new CfnModel(this, 'Resource', { + ...props, + contentType: props.contentType || KnownContentTypes.JSON, + apiId: props.api.apiId, + name: this.modelName, + schema: JsonSchemaMapper.toCfnJsonSchema(props.schema) + }); + this.modelId = this.resource.ref; + + if (props.api instanceof Api) { + if (props.api.latestDeployment) { + props.api.latestDeployment.addToLogicalId({ + ...props, + id, + api: props.api.apiId, + contentType: props.contentType, + name: this.modelName, + schema: props.schema + }); + props.api.latestDeployment.registerDependency(this.resource); + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts new file mode 100644 index 0000000000000..fc37c02e6149a --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts @@ -0,0 +1,125 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { Api, IApi } from './api'; +import { CfnRouteResponse } from './apigatewayv2.generated'; +import { IModel, KnownModelKey } from './model'; +import { IRoute } from './route'; + +/** + * Defines a set of common response patterns known to the system + */ +export enum KnownRouteResponseKey { + /** + * Default response, when no other pattern matches + */ + DEFAULT = "$default", + + /** + * Empty response + */ + EMPTY = "empty", + + /** + * Error response + */ + ERROR = "error" +} + +/** + * Defines the contract for an Api Gateway V2 Route Response. + */ +export interface IRouteResponse extends IResource { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Route Response. + * + * This interface is used by the helper methods in `Route` + */ +export interface RouteResponseOptions { + /** + * The route response parameters. + * + * @default - no parameters + */ + readonly responseParameters?: { [key: string]: string }; + + /** + * The model selection expression for the route response. + * + * Supported only for WebSocket APIs. + * + * @default - no models + */ + readonly responseModels?: { [key: string]: IModel | string }; + + /** + * The model selection expression for the route response. + * + * Supported only for WebSocket APIs. + * + * @default - no selection expression + */ + readonly modelSelectionExpression?: KnownModelKey | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Route Response. + */ +export interface RouteResponseProps extends RouteResponseOptions { + /** + * Defines the route for this response. + */ + readonly route: IRoute; + + /** + * Defines the api for this response. + */ + readonly api: IApi; + + /** + * The route response key. + */ + readonly key: KnownRouteResponseKey | string; +} + +/** + * A response for a route for an API in Amazon API Gateway v2. + */ +export class RouteResponse extends Resource implements IRouteResponse { + protected resource: CfnRouteResponse; + + constructor(scope: Construct, id: string, props: RouteResponseProps) { + super(scope, id, { + physicalName: props.key || id, + }); + + let responseModels: { [key: string]: string } | undefined; + if (props.responseModels !== undefined) { + responseModels = Object.assign({}, ...Object.entries(props.responseModels).map((e) => { + return ({ [e[0]]: (typeof(e[1]) === "string" ? e[1] : e[1].modelName) }); + })); + } + this.resource = new CfnRouteResponse(this, 'Resource', { + ...props, + apiId: props.api.apiId, + routeId: props.route.routeId, + routeResponseKey: props.key, + responseModels + }); + + if (props.api instanceof Api) { + if (props.api.latestDeployment) { + props.api.latestDeployment.addToLogicalId({ + ...props, + id, + api: props.api.apiId, + route: props.route.routeId, + routeResponseKey: props.key, + responseModels + }); + props.api.latestDeployment.registerDependency(this.resource); + } + } + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts new file mode 100644 index 0000000000000..ddcf0885be2dc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts @@ -0,0 +1,279 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { Api, IApi } from './api'; +import { CfnRoute } from './apigatewayv2.generated'; +import { IAuthorizer } from './authorizer'; +import { IIntegration } from './integration'; +import { IModel, KnownModelKey } from './model'; +import { KnownRouteResponseKey, RouteResponse, RouteResponseOptions } from './route-response'; + +/** + * Available authorization providers for ApiGateway V2 APIs + */ +export enum AuthorizationType { + /** + * Open access (Web Socket, HTTP APIs). + */ + NONE = "NONE", + /** + * Use AWS IAM permissions (Web Socket APIs). + */ + IAM = "AWS_IAM", + /** + * Use a custom authorizer (Web Socket APIs). + */ + CUSTOM = "CUSTOM", + /** + * Use an AWS Cognito user pool (Web Socket APIs). + */ + COGNITO = "COGNITO_USER_POOLS", + /** + * Use JSON Web Tokens (HTTP APIs). + */ + JWT = "JWT" +} + +/** + * Defines a set of common route keys known to the system + */ +export enum KnownRouteKey { + /** + * Default route, when no other pattern matches + */ + DEFAULT = "$default", + /** + * This route is a reserved route, used when a client establishes a connection to the WebSocket API + */ + CONNECT = "$connect", + /** + * This route is a reserved route, used when a client disconnects from the WebSocket API + */ + DISCONNECT = "$disconnect" +} + +/** + * Defines the attributes for an Api Gateway V2 Route. + */ +export interface RouteAttributes { + /** + * The ID of this API Gateway Route. + */ + readonly routeId: string; + + /** + * The key of this API Gateway Route. + */ + readonly key: string; +} + +/** + * Defines the contract for an Api Gateway V2 Route. + */ +export interface IRoute extends IResource { + /** + * The ID of this API Gateway Route. + * @attribute + */ + readonly routeId: string; + + /** + * The key of this API Gateway Route. + * @attribute + */ + readonly key: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Route. + * + * This interface is used by the helper methods in `Api` + */ +export interface RouteOptions { + /** + * Specifies whether an API key is required for the route. + * + * Supported only for WebSocket APIs. + * + * @default false + */ + readonly apiKeyRequired?: boolean; + + /** + * The authorization scopes supported by this route. + * + * @default - no authorization scope + */ + readonly authorizationScopes?: string[]; + + /** + * The authorization type for the route. + * + * @default 'NONE' + */ + readonly authorizationType?: AuthorizationType | string; + + /** + * The identifier of the Authorizer resource to be associated with this route. + * + * The authorizer identifier is generated by API Gateway when you created the authorizer. + * + * @default - no authorizer + */ + readonly authorizerId?: IAuthorizer; + + /** + * The model selection expression for the route. + * + * Supported only for WebSocket APIs. + * + * @default - no selection key + */ + readonly modelSelectionExpression?: KnownModelKey | string; + + /** + * The operation name for the route. + * + * @default - no operation name + */ + readonly operationName?: string; + + /** + * The request models for the route. + * + * Supported only for WebSocket APIs. + * + * @default - no models (for example passthrough) + */ + readonly requestModels?: { [key: string]: IModel | string }; + + /** + * The request parameters for the route. + * + * Supported only for WebSocket APIs. + * + * @default - no parameters + */ + readonly requestParameters?: { [key: string]: boolean }; + + /** + * The route response selection expression for the route. + * + * Supported only for WebSocket APIs. + * + * @default - no selection expression + */ + readonly routeResponseSelectionExpression?: KnownRouteResponseKey | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Route. + */ +export interface RouteProps extends RouteOptions { + /** + * The route key for the route. + */ + readonly key: KnownRouteKey | string; + + /** + * Defines the api for this route. + */ + readonly api: IApi; + + /** + * Defines the integration for this route. + */ + readonly integration: IIntegration; +} + +/** + * An route for an API in Amazon API Gateway v2. + * + * Use `addResponse` to configure routes. + */ +export class Route extends Resource implements IRoute { + /** + * Creates a new imported API Deployment + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param attrs Attributes of the Route + */ + public static fromRouteAttributes(scope: Construct, id: string, attrs: RouteAttributes): IRoute { + class Import extends Resource implements IRoute { + public readonly routeId = attrs.routeId; + public readonly key = attrs.key; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Route. + */ + public readonly routeId: string; + + /** + * The key of this API Gateway Route. + */ + public readonly key: string; + + protected api: IApi; + protected resource: CfnRoute; + + constructor(scope: Construct, id: string, props: RouteProps) { + super(scope, id); + this.api = props.api; + this.key = props.key; + + let authorizerId: string | undefined; + if (props.authorizerId !== undefined) { + authorizerId = props.authorizerId.authorizerId; + } + let requestModels: { [key: string]: string } | undefined; + if (props.requestModels !== undefined) { + requestModels = Object.assign({}, ...Object.entries(props.requestModels).map((e) => { + return ({ [e[0]]: (typeof(e[1]) === "string" ? e[1] : e[1].modelName) }); + })); + } + + this.resource = new CfnRoute(this, 'Resource', { + ...props, + apiKeyRequired: props.apiKeyRequired, + apiId: this.api.apiId, + routeKey: props.key, + target: `integrations/${props.integration.integrationId}`, + requestModels, + authorizerId + }); + this.routeId = this.resource.ref; + + if (props.api instanceof Api) { + if (props.api.latestDeployment) { + props.api.latestDeployment.addToLogicalId({ + ...props, + id, + routeKey: this.key, + target: `integrations/${props.integration.integrationId}`, + requestModels, + authorizerId + }); + props.api.latestDeployment.registerDependency(this.resource); + } + } + } + + /** + * Creates a new response for this route. + * + * @param key the key (predefined or not) that will select this response + * @param props the properties for this response + */ + public addResponse(key: KnownRouteResponseKey | string, props?: RouteResponseOptions): RouteResponse { + return new RouteResponse(this, `Response.${key}`, { + ...props, + route: this, + api: this.api, + key + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts new file mode 100644 index 0000000000000..87dbd91a6efcc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts @@ -0,0 +1,229 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +import { IApi } from './api'; +import { CfnStage } from './apigatewayv2.generated'; +import { Deployment, IDeployment } from './deployment'; + +/** + * Specifies the logging level for this route. This property affects the log entries pushed to Amazon CloudWatch Logs. + */ +export enum LoggingLevel { + /** + * Displays all log information + */ + INFO = "INFO", + + /** + * Only displays errors + */ + ERROR = "ERROR", + + /** + * Logging is turned off + */ + OFF = "OFF" +} + +/** + * Route settings for the stage. + */ +export interface RouteSettings { + /** + * Specifies whether (true) or not (false) data trace logging is enabled for this route. + * + * This property affects the log entries pushed to Amazon CloudWatch Logs. + * + * Supported only for WebSocket APIs. + * + * @default false + */ + readonly dataTraceEnabled?: boolean; + + /** + * Specifies whether detailed metrics are enabled. + * + * @default false + */ + readonly detailedMetricsEnabled?: boolean; + + /** + * Specifies the logging level for this route.This property affects the log entries pushed to Amazon CloudWatch Logs. + * + * Supported only for WebSocket APIs. + * + * @default - default logging level + */ + readonly loggingLevel?: LoggingLevel | string; + + /** + * Specifies the throttling burst limit. + * + * @default - default throttling + */ + readonly throttlingBurstLimit?: number; + + /** + * Specifies the throttling rate limit. + * + * @default - default throttling + */ + readonly throttlingRateLimit?: number; +} + +/** + * Settings for logging access in a stage. + */ +export interface AccessLogSettings { + /** + * The ARN of the CloudWatch Logs log group to receive access logs. + * + * @default - do not use CloudWatch Logs + */ + readonly destinationArn?: string; + + /** + * A single line format of the access logs of data, as specified by selected $context variables. + * The format must include at least $context.requestId. + * + * @default - default format + */ + readonly format?: string; +} + +/** + * Defines the contract for an Api Gateway V2 Stage. + */ +export interface IStage extends IResource { + /** + * The name of this API Gateway Stage. + * @attribute + */ + readonly stageName: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Stage. + */ +export interface StageOptions { + /** + * The stage name. Stage names can only contain alphanumeric characters, hyphens, and underscores. Maximum length is 128 characters. + */ + readonly stageName: string; + + /** + * Specifies whether updates to an API automatically trigger a new deployment. + * + * @default false + */ + readonly autoDeploy?: boolean; + + /** + * Settings for logging access in this stage + * + * @default - default nog settings + */ + readonly accessLogSettings?: AccessLogSettings; + + /** + * The identifier of a client certificate for a Stage. + * + * Supported only for WebSocket APIs. + * + * @default - no certificate + */ + readonly clientCertificateId?: string; + + /** + * The default route settings for the stage. + * + * @default - default values + */ + readonly defaultRouteSettings?: RouteSettings; + + /** + * Route settings for the stage. + * + * @default - default route settings + */ + readonly routeSettings?: { [key: string]: RouteSettings }; + + /** + * The description for the API stage. + * + * @default - no description + */ + readonly description?: string; + + /** + * A map that defines the stage variables for a Stage. + * Variable names can have alphanumeric and underscore + * characters, and the values must match [A-Za-z0-9-._~:/?#&=,]+. + * + * @default - no stage variables + */ + readonly stageVariables?: { [key: string]: string }; + + // TODO: Tags +} + +/** + * Defines the properties required for defining an Api Gateway V2 Stage. + */ +export interface StageProps extends StageOptions { + /** + * Defines the api for this stage. + */ + readonly api: IApi; + + /** + * The deployment for the API stage. Can't be updated if autoDeploy is enabled. + */ + readonly deployment: IDeployment; +} + +/** + * A stage for a route for an API in Amazon API Gateway v2. + */ +export class Stage extends Resource implements IStage { + /** + * Creates a new imported Stage + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param stageName Identifier of the API + */ + 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); + } + + /** + * The name of this API Gateway Stage. + */ + public readonly stageName: string; + + protected resource: CfnStage; + + constructor(scope: Construct, id: string, props: StageProps) { + super(scope, id); + + this.resource = new CfnStage(this, 'Resource', { + ...props, + apiId: props.api.apiId, + deploymentId: props.deployment.deploymentId + }); + this.stageName = this.resource.ref; + + if (props.deployment instanceof Deployment) { + props.deployment.addToLogicalId({ + ...props, + api: props.api.apiId, + deployment: props.deployment.deploymentId, + id + }); + } + } +} \ 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 67ff4a8b3c802..c3c5c28033510 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -87,15 +87,46 @@ }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "constructs": "^2.0.0" }, "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "constructs": "^2.0.0" }, "engines": { "node": ">= 10.3.0" }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-apigatewayv2.ApiMappingProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.DeploymentProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.IntegrationResponseProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.LambdaIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.RouteProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.RouteResponseProps", + "no-unused-type:@aws-cdk/aws-apigatewayv2.AuthorizationType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.ConnectionType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.ContentHandlingStrategy", + "no-unused-type:@aws-cdk/aws-apigatewayv2.IntegrationMethod", + "no-unused-type:@aws-cdk/aws-apigatewayv2.IntegrationType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownContentTypes", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownIntegrationResponseKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownModelKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownRouteKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownRouteResponseKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownTemplateKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.LoggingLevel", + "no-unused-type:@aws-cdk/aws-apigatewayv2.PassthroughBehavior", + "no-unused-type:@aws-cdk/aws-apigatewayv2.ProtocolType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.EndpointType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.AuthorizerType", + "resource-interface:@aws-cdk/aws-apigatewayv2.LambdaIntegration" + ] + }, "stability": "experimental", "awscdkio": { "announce": false diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts new file mode 100644 index 0000000000000..11f0d25f1d85b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts @@ -0,0 +1,27 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('minimal setup', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api'); + const domainName = new apigw.DomainName(stack, 'domain-name', { domainName: 'test.example.com' }); + new apigw.ApiMapping(stack, 'mapping', { + stage: api.deploymentStage!, + domainName, + api + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::ApiMapping", { + ApiId: { Ref: "myapi4C7BF186" }, + Stage: { Ref: "myapiStageprod07E02E1F" }, + DomainName: { Ref: "domainname1131E743" } + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts new file mode 100644 index 0000000000000..d6e0de55b4c9c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts @@ -0,0 +1,112 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('minimal setup', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new apigw.Api(stack, 'my-api'); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Api", { + Name: 'my-api', + ProtocolType: apigw.ProtocolType.WEBSOCKET, + RouteSelectionExpression: '${request.body.action}' + })); + + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Deployment", { + ApiId: { Ref: "myapi4C7BF186" } + })); + + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Stage", { + ApiId: { Ref: "myapi4C7BF186" }, + StageName: "prod", + DeploymentId: { Ref: "myapiDeployment92F2CB492D341D1B" }, + })); +}); + +test('minimal setup (no deploy)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new apigw.Api(stack, 'my-api', { + deploy: false + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Api", { + Name: 'my-api' + })); + + cdkExpect(stack).notTo(haveResource("AWS::ApiGatewayV2::Deployment")); + cdkExpect(stack).notTo(haveResource("AWS::ApiGatewayV2::Stage")); +}); + +test('minimal setup (no deploy, error)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => { + return new apigw.Api(stack, 'my-api', { + deploy: false, + deployOptions: { + stageName: 'testStage' + } + }); + }).toThrow(); +}); + +test('URLs and ARNs', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api'); + const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); + const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { + key: 'routeKey', + routeId: 'routeId' + }); + + // THEN + expect(stack.resolve(api.clientUrl())).toEqual({ "Fn::Join": [ "", [ "wss://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/", { Ref: "myapiStageprod07E02E1F" } ] ] }); + expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "wss://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev" ] ] }); + + expect(stack.resolve(api.connectionsUrl())).toEqual({ "Fn::Join": [ "", [ "https://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/", { Ref: "myapiStageprod07E02E1F" }, "/@connections" ] ] }); + expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "https://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev/@connections" ] ] }); + + expect(stack.resolve(api.executeApiArn())).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/*" ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/routeKey" ] ] }); + expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/*" ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/routeKey" ] ] }); + + expect(stack.resolve(api.connectionsApiArn())).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/POST/*" ] ] }); + expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/POST/my-connection" ] ] }); + expect(stack.resolve(api.connectionsApiArn(undefined, importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/POST/*" ] ] }); + expect(stack.resolve(api.connectionsApiArn('my-connection', importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/POST/my-connection" ] ] }); +}); + +test('URLs and ARNs (no deploy)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); + + // THEN + expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "wss://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev" ] ] }); + expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "https://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev/@connections" ] ] }); + + expect(() => stack.resolve(api.clientUrl())).toThrow(); + expect(() => stack.resolve(api.connectionsUrl())).toThrow(); +}); \ No newline at end of file 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/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts new file mode 100644 index 0000000000000..b9bf191187a44 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts @@ -0,0 +1,32 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('minimal setup', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + const functionArn = stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'}); + new apigw.Authorizer(stack, 'authorizer', { + authorizerName: 'my-authorizer', + authorizerType: apigw.AuthorizerType.JWT, + authorizerUri: `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${functionArn}/invocations`, + api + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Authorizer", { + ApiId: { Ref: "myapi4C7BF186" }, + Name: "my-authorizer", + AuthorizerType: "JWT", + AuthorizerUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] }, + IdentitySource: [] + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts new file mode 100644 index 0000000000000..77ced1c2825d5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts @@ -0,0 +1,27 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('minimal setup', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new apigw.DomainName(stack, 'domain-name', { + domainName: 'test.example.com', + domainNameConfigurations: [ + { + endpointType: apigw.EndpointType.EDGE + } + ] + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::DomainName", { + DomainName: 'test.example.com', + DomainNameConfigurations: [ { EndpointType: "EDGE" } ] + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts new file mode 100644 index 0000000000000..dab9e92cf7307 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts @@ -0,0 +1,93 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('Lambda integration', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})) + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Integration", { + ApiId: { Ref: "myapi4C7BF186" }, + IntegrationType: apigw.IntegrationType.AWS, + IntegrationUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] } + })); +}); + +test('Lambda integration (with extra params)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), + connectionType: apigw.ConnectionType.INTERNET, + integrationMethod: apigw.IntegrationMethod.GET + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Integration", { + ApiId: { Ref: "myapi4C7BF186" }, + IntegrationType: apigw.IntegrationType.AWS, + IntegrationUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] }, + IntegrationMethod: apigw.IntegrationMethod.GET, + ConnectionType: apigw.ConnectionType.INTERNET + })); +}); + +test('Lambda integration (proxy)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), + proxy: true + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Integration", { + ApiId: { Ref: "myapi4C7BF186" }, + IntegrationType: apigw.IntegrationType.AWS_PROXY, + IntegrationUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] } + })); +}); + +test('Integration response', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + const integration = api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})) + }); + integration.addResponse(apigw.KnownIntegrationResponseKey.DEFAULT); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::IntegrationResponse", { + ApiId: { Ref: "myapi4C7BF186" }, + IntegrationId: { Ref: "myapimyFunction27BC3796" }, + IntegrationResponseKey: apigw.KnownIntegrationResponseKey.DEFAULT + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts new file mode 100644 index 0000000000000..27c7e3c188af0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts @@ -0,0 +1,81 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('route', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + const integration = api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), + }); + integration.addRoute(apigw.KnownRouteKey.CONNECT, { + modelSelectionExpression: apigw.KnownModelKey.DEFAULT, + requestModels: { + [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: "statusInputModel", type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }) + }, + routeResponseSelectionExpression: apigw.KnownRouteResponseKey.DEFAULT + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Route", { + ApiId: { Ref: "myapi4C7BF186" }, + RouteKey: "$connect", + Target: { "Fn::Join": ["", [ "integrations/", { Ref: "myapimyFunction27BC3796" } ] ] }, + ModelSelectionExpression: "$default", + RequestModels: { + $default: "statusInputModel" + } + })); + + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Model", { + ApiId: { Ref: "myapi4C7BF186" }, + ContentType: apigw.KnownContentTypes.JSON, + Name: "statusInputModel" + })); +}); + +test('route response', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + const integration = api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`) + }); + const route = integration.addRoute(apigw.KnownRouteKey.CONNECT, {}); + route.addResponse(apigw.KnownRouteKey.CONNECT, { + modelSelectionExpression: apigw.KnownModelKey.DEFAULT, + responseModels: { + [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: "statusResponse", type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }) + } + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::RouteResponse", { + ApiId: { Ref: "myapi4C7BF186" }, + RouteId: { Ref: "myapimyFunctionRouteconnectA2AF3242" }, + RouteResponseKey: "$connect", + ModelSelectionExpression: "$default", + ResponseModels: { + $default: "statusResponse" + } + })); + + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Model", { + ApiId: { Ref: "myapi4C7BF186" }, + ContentType: apigw.KnownContentTypes.JSON, + Name: "statusResponse" + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts new file mode 100644 index 0000000000000..f64bd52b54a14 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts @@ -0,0 +1,31 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../lib'; + +// tslint:disable:max-line-length + +test('minimal setup', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + deploy: false + }); + const deployment = new apigw.Deployment(stack, 'deployment', { + api + }); + new apigw.Stage(stack, 'stage', { + api, + deployment, + stageName: 'dev' + }); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Stage", { + ApiId: { Ref: "myapi4C7BF186" }, + StageName: "dev", + DeploymentId: { Ref: "deployment33381975F8795BE8" }, + })); +}); \ No newline at end of file From 338912e75b86c30a3063744a3367fb42743da670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 31 Mar 2020 22:00:03 +0200 Subject: [PATCH 2/7] fix(aws-apigatewayv2): add missing api gateway fields for HTTP apis --- packages/@aws-cdk/aws-apigatewayv2/README.md | 92 +++++++- packages/@aws-cdk/aws-apigatewayv2/lib/api.ts | 211 +++++++++++++++++- .../@aws-cdk/aws-apigatewayv2/lib/route.ts | 19 ++ .../@aws-cdk/aws-apigatewayv2/package.json | 2 + .../aws-apigatewayv2/test/api-mapping.test.ts | 2 +- .../aws-apigatewayv2/test/api.test.ts | 12 +- .../aws-apigatewayv2/test/authorizer.test.ts | 2 + .../aws-apigatewayv2/test/integration.test.ts | 8 + .../aws-apigatewayv2/test/route.test.ts | 26 +++ .../aws-apigatewayv2/test/stage.test.ts | 2 + 10 files changed, 362 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 6b7dbb4c56f69..cdf2a414aa198 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -17,8 +17,96 @@ --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +Amazon API Gateway V2 is a fully managed service that makes it easy for developers +to publish, maintain, monitor, and secure Web Socket or HTTP APIs at any scale. Create an API to +access data, business logic, or functionality from your back-end services, such +as applications running on Amazon Elastic Compute Cloud (Amazon EC2), code +running on AWS Lambda, or any web application. + +### Defining APIs + +APIs are defined through adding routes, and integrating them with AWS Services or APIs. +Currently this module supports HTTP APIs and Web Socket APIs. + +For example a Web Socket API with a "$connect" route (handling user connection) backed by +an AWS Lambda function would be defined as follows: + +```ts +const api = new apigatewayv2.Api(this, 'books-api', { + protocolType: apigatewayv2.ProtocolType.HTTP +}); + +const backend = new lambda.Function(...); +const integration = api.addLambdaIntegration('myFunction', { + handler: backend +}); + +integration.addRoute('POST /'); +``` + +You can also supply `proxy: false`, in which case you will have to explicitly +define the API model: + +```ts +const backend = new lambda.Function(...); +const integration = api.addLambdaIntegration('myFunction', { + handler: backend, + proxy: false +}); +``` + +### Integration Targets + +Methods are associated with backend integrations, which are invoked when this +method is called. API Gateway supports the following integrations: + + * `LambdaIntegration` - can be used to invoke an AWS Lambda function. + +The following example shows how to integrate the `GET /book/{book_id}` method to +an AWS Lambda function: + +```ts +const getBookHandler = new lambda.Function(...); +const getBookIntegration = api.addLambdaIntegration('myFunction', { + handler: getBookHandler +}); +``` + +The following example shows how to use an API Key with a usage plan: + +```ts +const hello = new lambda.Function(...); + +const api = new apigatewayv2.Api(this, 'hello-api', { + protocolType: apigatewayv2.ProtocolType.WEBSOCKET, + apiKeySelectionExpression: '$request.header.x-api-key' +}); +``` + +### Working with models + +When you work with Lambda integrations that are not Proxy integrations, you +have to define your models and mappings for the request, response, and integration. ```ts -import apigatewayv2 = require('@aws-cdk/aws-apigatewayv2'); +const hello = new lambda.Function(...); + +const api = new apigateway.RestApi(this, 'hello-api', { + protocolType: apigatewayv2.ProtocolType.HTTP +}); + +const integration = api.addLambdaIntegration('myFunction', { + handler: hello +}); +integration.addRoute(apigw.KnownRouteKey.CONNECT, { + modelSelectionExpression: apigw.KnownModelKey.DEFAULT, + requestModels: { + $default: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: "statusInputModel", type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }) + }, + routeResponseSelectionExpression: apigw.KnownRouteResponseKey.DEFAULT +}); ``` + +---- + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts index d170bb6101eed..b6d33a65b195d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts @@ -6,7 +6,7 @@ import { Integration } from './integration'; import { LambdaIntegration, LambdaIntegrationOptions } from './integrations/lambda-integration'; import { JsonSchema } from './json-schema'; import { Model, ModelOptions } from './model'; -import { IRoute, KnownRouteKey } from './route'; +import { IRoute, KnownRouteSelectionExpression } from './route'; import { IStage, Stage, StageOptions } from './stage'; /** @@ -24,6 +24,118 @@ export enum ProtocolType { HTTP = "HTTP" } +/** + * Specifies how to interpret the base path of the API during import + */ +export enum BasePath { + /** + * Ignores the base path + */ + IGNORE = "ignore", + + /** + * Prepends the base path to the API path + */ + PREPEND = "prepend", + + /** + * Splits the base path from the API path + */ + SPLIT = "split" +} + +/** + * This expression is evaluated when the service determines the given request should proceed + * only if the client provides a valid API key. + */ +export enum KnownApiKeySelectionExpression { + /** + * Uses the `x-api-key` header to get the API Key + */ + HEADER_X_API_KEY = "$request.header.x-api-key", + + /** + * Uses the `usageIdentifier` property of the current authorizer to get the API Key + */ + AUTHORIZER_USAGE_IDENTIFIER = " $context.authorizer.usageIdentifierKey" +} + +/** + * The BodyS3Location property specifies an S3 location from which to import an OpenAPI definition. + */ +export interface BodyS3Location { + /** + * The S3 bucket that contains the OpenAPI definition to import. + */ + readonly bucket: string; + + /** + * The Etag of the S3 object. + * + * @default - no etag verification + */ + readonly etag?: string; + + /** + * The key of the S3 object. + */ + readonly key: string; + + /** + * The version of the S3 object. + * + * @default - get latest version + */ + readonly version?: string; +} + +/** + * The CorsConfiguration property specifies a CORS configuration for an API. + */ +export interface CorsConfiguration { + /** + * Specifies whether credentials are included in the CORS request. + * + * @default false + */ + readonly allowCredentials?: boolean; + + /** + * Represents a collection of allowed headers. + * + * @default - no headers allowed + */ + readonly allowHeaders?: string[]; + + /** + * Represents a collection of allowed HTTP methods. + * + * @default - no allowed methods + */ + readonly allowMethods: string[]; + + /** + * Represents a collection of allowed origins. + * + * @default - get allowed origins + */ + readonly allowOrigins?: string[]; + + /** + * Represents a collection of exposed headers. + * + * @default - get allowed origins + */ + readonly exposeHeaders?: string[]; + + /** + * The number of seconds that the browser should cache preflight request results. + * + * @default - get allowed origins + */ + readonly maxAge?: number; +} + /** * Defines the contract for an Api Gateway V2 Api. */ @@ -88,25 +200,65 @@ export interface ApiProps { readonly apiName?: string; /** - * Available protocols for ApiGateway V2 APIs (currently only 'WEBSOCKET' is supported) + * Specifies how to interpret the base path of the API during import. + * + * Supported only for HTTP APIs. + * @default 'ignore' + */ + readonly basePath?: BasePath; + + /** + * The OpenAPI definition. * - * @default 'WEBSOCKET' + * To import an HTTP API, you must specify `body` or `bodyS3Location`. + * + * Supported only for HTTP APIs. + * @default - `bodyS3Location` if defined, or no import + */ + readonly body?: string; + + /** + * The S3 location of an OpenAPI definition. + * + * To import an HTTP API, you must specify `body` or `bodyS3Location`. + * + * Supported only for HTTP APIs. + * @default - `body` if defined, or no import + */ + readonly bodyS3Location?: BodyS3Location; + + /** + * The S3 location of an OpenAPI definition. + * + * To import an HTTP API, you must specify `body` or `bodyS3Location`. + * + * Supported only for HTTP APIs. + * @default - `body` if defined, or no import + */ + readonly corsConfiguration?: CorsConfiguration; + + /** + * Available protocols for ApiGateway V2 APIs + * + * @default - required unless you specify an OpenAPI definition */ readonly protocolType?: ProtocolType | string; /** * Expression used to select the route for this API * - * @default '${request.body.action}' + * @default - '${request.method} ${request.path}' for HTTP APIs, required for Web Socket APIs */ - readonly routeSelectionExpression?: KnownRouteKey | string; + readonly routeSelectionExpression?: KnownRouteSelectionExpression | string; /** * Expression used to select the Api Key to use for metering * + * Supported only for WebSocket APIs + * * @default - No Api Key */ - readonly apiKeySelectionExpression?: string; + readonly apiKeySelectionExpression?: KnownApiKeySelectionExpression | string; /** * A description of the purpose of this API Gateway Api resource. @@ -128,6 +280,13 @@ export interface ApiProps { * @default false */ readonly version?: string; + + /** + * Specifies whether to rollback the API creation (`true`) or not (`false`) when a warning is encountered + * + * @default false + */ + readonly failOnWarnings?: boolean; } /** @@ -176,10 +335,46 @@ export class Api extends Resource implements IApi { physicalName: props.apiName || id, }); + if (props.protocolType === undefined && props.body === null && props.bodyS3Location === null) { + throw new Error("You must specify a protocol type, or import an Open API definition (directly or from S3)"); + } + + switch (props.protocolType) { + case ProtocolType.WEBSOCKET: { + if (props.basePath !== undefined) { + throw new Error('"basePath" is only supported with HTTP APIs'); + } + if (props.body !== undefined) { + throw new Error('"body" is only supported with HTTP APIs'); + } + if (props.bodyS3Location !== undefined) { + throw new Error('"bodyS3Location" is only supported with HTTP APIs'); + } + if (props.corsConfiguration !== undefined) { + throw new Error('"corsConfiguration" is only supported with HTTP APIs'); + } + if (props.routeSelectionExpression === undefined) { + throw new Error('"routeSelectionExpression" is required for Web Socket APIs'); + } + break; + } + case ProtocolType.HTTP: + case undefined: { + if (props.apiKeySelectionExpression !== undefined) { + throw new Error('"apiKeySelectionExpression" is only supported with Web Socket APIs'); + } + if (props.disableSchemaValidation !== undefined) { + throw new Error('"disableSchemaValidation" is only supported with Web Socket APIs'); + } + if (props.routeSelectionExpression !== undefined && props.apiKeySelectionExpression !== KnownRouteSelectionExpression.METHOD_PATH) { + throw new Error('"routeSelectionExpression" has a single supported value for HTTP APIs: "${request.method} ${request.path}"'); + } + break; + } + } + this.resource = new CfnApi(this, 'Resource', { ...props, - protocolType: props.protocolType || ProtocolType.WEBSOCKET, - routeSelectionExpression: props.routeSelectionExpression || '${request.body.action}', name: this.physicalName }); this.apiId = this.resource.ref; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts index ddcf0885be2dc..3108ec9ea86d3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts @@ -51,6 +51,25 @@ export enum KnownRouteKey { DISCONNECT = "$disconnect" } +/** + * Known expressions for selecting a route in an API + */ +export enum KnownRouteSelectionExpression { + /** + * Selects the route key from the request context + * + * Supported only for WebSocket APIs. + */ + CONTEXT_ROUTE_KEY = '${context.routeKey}', + + /** + * A string starting with the method ans containing the request path + * + * Only supported value for HTTP APIs, if not provided, will be the default + */ + METHOD_PATH = '${request.method} ${request.path}' +} + /** * Defines the attributes for an Api Gateway V2 Route. */ diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index c3c5c28033510..b054309f6afe5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -124,6 +124,8 @@ "no-unused-type:@aws-cdk/aws-apigatewayv2.ProtocolType", "no-unused-type:@aws-cdk/aws-apigatewayv2.EndpointType", "no-unused-type:@aws-cdk/aws-apigatewayv2.AuthorizerType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownApiKeySelectionExpression", + "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownRouteSelectionExpression", "resource-interface:@aws-cdk/aws-apigatewayv2.LambdaIntegration" ] }, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts index 11f0d25f1d85b..b943c456aca07 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts @@ -10,7 +10,7 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api'); + const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); const domainName = new apigw.DomainName(stack, 'domain-name', { domainName: 'test.example.com' }); new apigw.ApiMapping(stack, 'mapping', { stage: api.deploymentStage!, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts index d6e0de55b4c9c..cb53f6f708a13 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts @@ -10,13 +10,13 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - new apigw.Api(stack, 'my-api'); + new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); // THEN cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Api", { Name: 'my-api', ProtocolType: apigw.ProtocolType.WEBSOCKET, - RouteSelectionExpression: '${request.body.action}' + RouteSelectionExpression: '${context.routeKey}' })); cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Deployment", { @@ -36,6 +36,8 @@ test('minimal setup (no deploy)', () => { // WHEN new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); @@ -55,6 +57,8 @@ test('minimal setup (no deploy, error)', () => { // WHEN expect(() => { return new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, deployOptions: { stageName: 'testStage' @@ -68,7 +72,7 @@ test('URLs and ARNs', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api'); + const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { key: 'routeKey', @@ -99,6 +103,8 @@ test('URLs and ARNs (no deploy)', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts index b9bf191187a44..dbbf5e28081fa 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts @@ -11,6 +11,8 @@ test('minimal setup', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); const functionArn = stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts index dab9e92cf7307..1cb20e6476b4f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts @@ -12,6 +12,8 @@ test('Lambda integration', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); api.addLambdaIntegration('myFunction', { @@ -32,6 +34,8 @@ test('Lambda integration (with extra params)', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); api.addLambdaIntegration('myFunction', { @@ -56,6 +60,8 @@ test('Lambda integration (proxy)', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); api.addLambdaIntegration('myFunction', { @@ -77,6 +83,8 @@ test('Integration response', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); const integration = api.addLambdaIntegration('myFunction', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts index 27c7e3c188af0..4932d53b6bf88 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts @@ -12,6 +12,8 @@ test('route', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); const integration = api.addLambdaIntegration('myFunction', { @@ -43,12 +45,36 @@ test('route', () => { })); }); +test('route (no model)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.HTTP, + deploy: false + }); + const integration = api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), + }); + integration.addRoute('POST /'); + + // THEN + cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Route", { + ApiId: { Ref: "myapi4C7BF186" }, + RouteKey: "POST /", + Target: { "Fn::Join": ["", [ "integrations/", { Ref: "myapimyFunction27BC3796" } ] ] } + })); +}); + test('route response', () => { // GIVEN const stack = new Stack(); // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); const integration = api.addLambdaIntegration('myFunction', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts index f64bd52b54a14..24dd6e52664a6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts @@ -11,6 +11,8 @@ test('minimal setup', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false }); const deployment = new apigw.Deployment(stack, 'deployment', { From fb59ea30a920c4bbe705cac576ebd839353a6c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Sat, 25 Apr 2020 14:38:02 +0200 Subject: [PATCH 3/7] chore(aws-apigatewayv2): update to support new linter rules Support ESLint rules: - [comma-dangle]: Missing trailing comma - [quotes]: Strings must use singlequote' --- .../aws-apigatewayv2/lib/api-mapping.ts | 4 +- packages/@aws-cdk/aws-apigatewayv2/lib/api.ts | 36 +++++----- .../aws-apigatewayv2/lib/authorizer.ts | 6 +- .../aws-apigatewayv2/lib/deployment.ts | 4 +- .../aws-apigatewayv2/lib/domain-name.ts | 6 +- .../lib/integration-response.ts | 10 +-- .../aws-apigatewayv2/lib/integration.ts | 48 ++++++------- .../lib/integrations/lambda-integration.ts | 6 +- .../aws-apigatewayv2/lib/json-schema.ts | 18 ++--- .../@aws-cdk/aws-apigatewayv2/lib/model.ts | 18 ++--- .../aws-apigatewayv2/lib/route-response.ts | 12 ++-- .../@aws-cdk/aws-apigatewayv2/lib/route.ts | 24 +++---- .../@aws-cdk/aws-apigatewayv2/lib/stage.ts | 10 +-- .../aws-apigatewayv2/test/api-mapping.test.ts | 10 +-- .../aws-apigatewayv2/test/api.test.ts | 67 ++++++++++--------- .../aws-apigatewayv2/test/authorizer.test.ts | 16 ++--- .../aws-apigatewayv2/test/domain-name.test.ts | 10 +-- .../aws-apigatewayv2/test/integration.test.ts | 44 ++++++------ .../aws-apigatewayv2/test/route.test.ts | 64 +++++++++--------- .../aws-apigatewayv2/test/stage.test.ts | 14 ++-- 20 files changed, 215 insertions(+), 212 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts index 3d2967fa12bae..3d7800672bb8d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts @@ -87,8 +87,8 @@ export class ApiMapping extends Resource implements IApiMapping { this.resource = new CfnApiMapping(this, 'Resource', { ...props, apiId: props.api.apiId, - domainName: ((typeof(props.domainName) === "string") ? props.domainName : props.domainName.domainName), - stage: props.stage.stageName + domainName: ((typeof(props.domainName) === 'string') ? props.domainName : props.domainName.domainName), + stage: props.stage.stageName, }); this.apiMappingId = this.resource.ref; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts index b6d33a65b195d..52aa3015470c9 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts @@ -16,12 +16,12 @@ export enum ProtocolType { /** * WebSocket API */ - WEBSOCKET = "WEBSOCKET", + WEBSOCKET = 'WEBSOCKET', /** * HTTP API */ - HTTP = "HTTP" + HTTP = 'HTTP' } /** @@ -31,17 +31,17 @@ export enum BasePath { /** * Ignores the base path */ - IGNORE = "ignore", + IGNORE = 'ignore', /** * Prepends the base path to the API path */ - PREPEND = "prepend", + PREPEND = 'prepend', /** * Splits the base path from the API path */ - SPLIT = "split" + SPLIT = 'split' } /** @@ -52,12 +52,12 @@ export enum KnownApiKeySelectionExpression { /** * Uses the `x-api-key` header to get the API Key */ - HEADER_X_API_KEY = "$request.header.x-api-key", + HEADER_X_API_KEY = '$request.header.x-api-key', /** * Uses the `usageIdentifier` property of the current authorizer to get the API Key */ - AUTHORIZER_USAGE_IDENTIFIER = " $context.authorizer.usageIdentifierKey" + AUTHORIZER_USAGE_IDENTIFIER = '$context.authorizer.usageIdentifierKey' } /** @@ -336,7 +336,7 @@ export class Api extends Resource implements IApi { }); if (props.protocolType === undefined && props.body === null && props.bodyS3Location === null) { - throw new Error("You must specify a protocol type, or import an Open API definition (directly or from S3)"); + throw new Error('You must specify a protocol type, or import an Open API definition (directly or from S3)'); } switch (props.protocolType) { @@ -375,7 +375,7 @@ export class Api extends Resource implements IApi { this.resource = new CfnApi(this, 'Resource', { ...props, - name: this.physicalName + name: this.physicalName, }); this.apiId = this.resource.ref; @@ -390,7 +390,7 @@ export class Api extends Resource implements IApi { description: 'Automatically created by the Api construct', // No stageName specified, this will be defined by the stage directly, as it will reference the deployment - retainDeployments: props.retainDeployments + retainDeployments: props.retainDeployments, }); this.deploymentStage = new Stage(this, `Stage.${stageName}`, { @@ -398,11 +398,11 @@ export class Api extends Resource implements IApi { deployment: this.deployment, api: this, stageName, - description: 'Automatically created by the Api construct' + description: 'Automatically created by the Api construct', }); } else { if (props.deployOptions) { - throw new Error(`Cannot set 'deployOptions' if 'deploy' is disabled`); + throw new Error('Cannot set "deployOptions" if "deploy" is disabled'); } } } @@ -435,7 +435,7 @@ export class Api extends Resource implements IApi { ...props, modelName: schema.title, api: this, - schema + schema, }); } @@ -456,7 +456,7 @@ export class Api extends Resource implements IApi { service: 'execute-api', resource: apiId, sep: '/', - resourceName: `${stageName}/${routeKey}` + resourceName: `${stageName}/${routeKey}`, }); } @@ -466,7 +466,7 @@ export class Api extends Resource implements IApi { * @param connectionId The identifier of this connection ('*' if not defined) * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') */ - public connectionsApiArn(connectionId: string = "*", stage?: IStage) { + public connectionsApiArn(connectionId: string = '*', stage?: IStage) { const stack = Stack.of(this); const apiId = this.apiId; const stageName = ((stage === undefined) ? @@ -476,7 +476,7 @@ export class Api extends Resource implements IApi { service: 'execute-api', resource: apiId, sep: '/', - resourceName: `${stageName}/POST/${connectionId}` + resourceName: `${stageName}/POST/${connectionId}`, }); } @@ -491,7 +491,7 @@ export class Api extends Resource implements IApi { let stageName: string | undefined; if (stage === undefined) { if (this.deploymentStage === undefined) { - throw Error("No stage defined for this Api"); + throw Error('No stage defined for this Api'); } stageName = this.deploymentStage.stageName; } else { @@ -511,7 +511,7 @@ export class Api extends Resource implements IApi { let stageName: string | undefined; if (stage === undefined) { if (this.deploymentStage === undefined) { - throw Error("No stage defined for this Api"); + throw Error('No stage defined for this Api'); } stageName = this.deploymentStage.stageName; } else { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts index 9cfcdc951ea4b..a3d4d86312722 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts @@ -10,12 +10,12 @@ export enum AuthorizerType { /** * For WebSocket APIs, specify REQUEST for a Lambda function using incoming request parameters */ - REQUEST = "REQUEST", + REQUEST = 'REQUEST', /** * For HTTP APIs, specify JWT to use JSON Web Tokens */ - JWT = "JWT" + JWT = 'JWT' } /** @@ -180,7 +180,7 @@ export class Authorizer extends Resource implements IAuthorizer { ...props, identitySource: (props.identitySource ? props.identitySource : []), apiId: props.api.apiId, - name: props.authorizerName + name: props.authorizerName, }); this.authorizerId = this.resource.ref; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts index 1e98dd62aedf4..dce7c6034086a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts @@ -110,7 +110,7 @@ export class Deployment extends Resource implements IDeployment { this.resource = new CfnDeployment(this, 'Resource', { apiId: props.api.apiId, description: props.description, - stageName: props.stageName + stageName: props.stageName, }); if ((props.retainDeployments === undefined) || (props.retainDeployments === true)) { @@ -161,7 +161,7 @@ export class Deployment extends Resource implements IDeployment { if (this.hashComponents.length > 0) { const md5 = createHash('md5'); this.hashComponents.map(c => stack.resolve(c)).forEach(c => md5.update(JSON.stringify(c))); - this.resource.overrideLogicalId(this.originalLogicalId + md5.digest("hex").substr(0, 8).toUpperCase()); + this.resource.overrideLogicalId(this.originalLogicalId + md5.digest('hex').substr(0, 8).toUpperCase()); } super.prepare(); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts index 20ae3792f9bed..c0372c8df8350 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts @@ -9,12 +9,12 @@ export enum EndpointType { /** * Regional endpoint */ - REGIONAL = "REGIONAL", + REGIONAL = 'REGIONAL', /** * Edge endpoint */ - EDGE = "EDGE" + EDGE = 'EDGE' } /** @@ -133,7 +133,7 @@ export class DomainName extends Resource implements IDomainName { super(scope, id); this.resource = new CfnDomainName(this, 'Resource', { - ...props + ...props, }); this.domainName = this.resource.ref; this.regionalDomainName = this.resource.attrRegionalDomainName; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts index f1dc711afc006..ecd8c326806cc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts @@ -11,17 +11,17 @@ export enum KnownIntegrationResponseKey { /** * Default response, when no other pattern matches */ - DEFAULT = "$default", + DEFAULT = '$default', /** * Empty response */ - EMPTY = "empty", + EMPTY = 'empty', /** * Error response */ - ERROR = "error" + ERROR = 'error' } /** @@ -112,7 +112,7 @@ export class IntegrationResponse extends Resource implements IIntegrationRespons ...props, apiId: props.api.apiId, integrationId: props.integration.integrationId, - integrationResponseKey: props.key + integrationResponseKey: props.key, }); if (props.api instanceof Api) { @@ -122,7 +122,7 @@ export class IntegrationResponse extends Resource implements IIntegrationRespons api: props.api.apiId, integration: props.integration.integrationId, id, - integrationResponseKey: props.key + integrationResponseKey: props.key, }); props.api.latestDeployment.registerDependency(this.resource); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts index 454762cb16d35..0b5c010be4693 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts @@ -12,12 +12,12 @@ export enum ConnectionType { /** * Internet connectivity through the public routable internet */ - INTERNET = "INTERNET", + INTERNET = 'INTERNET', /** * Private connections between API Gateway and resources in a VPC */ - VPC_LINK = " VPC_LINK" + VPC_LINK = 'VPC_LINK' } /** @@ -29,30 +29,30 @@ export enum IntegrationType { * With the Lambda function-invoking action, this is referred to as the Lambda custom integration. * With any other AWS service action, this is known as AWS integration. */ - AWS = "AWS", + AWS = 'AWS', /** * Integration of the route or method request with the Lambda function-invoking action with the client request passed through as-is. * This integration is also referred to as Lambda proxy integration. */ - AWS_PROXY = "AWS_PROXY", + AWS_PROXY = 'AWS_PROXY', /** * Integration of the route or method request with an HTTP endpoint. * This integration is also referred to as HTTP custom integration. */ - HTTP = "HTTP", + HTTP = 'HTTP', /** * Integration of the route or method request with an HTTP endpoint, with the client request passed through as-is. * This is also referred to as HTTP proxy integration. */ - HTTP_PROXY = "HTTP_PROXY", + HTTP_PROXY = 'HTTP_PROXY', /** * Integration of the route or method request with API Gateway as a "loopback" endpoint without invoking any backend. */ - MOCK = "MOCK" + MOCK = 'MOCK' } /** @@ -65,12 +65,12 @@ export enum ContentHandlingStrategy { /** * Converts a response payload from a Base64-encoded string to the corresponding binary blob */ - CONVERT_TO_BINARY = "CONVERT_TO_BINARY", + CONVERT_TO_BINARY = 'CONVERT_TO_BINARY', /** * Converts a response payload from a binary blob to a Base64-encoded string */ - CONVERT_TO_TEXT = "CONVERT_TO_TEXT" + CONVERT_TO_TEXT = 'CONVERT_TO_TEXT' } /** @@ -83,17 +83,17 @@ export enum PassthroughBehavior { * Passes the request body for unmapped content types through to the * integration backend without transformation */ - WHEN_NO_MATCH = "WHEN_NO_MATCH", + WHEN_NO_MATCH = 'WHEN_NO_MATCH', /** * Allows pass-through when the integration has no content types mapped * to templates. However, if there is at least one content type defined, * unmapped content types will be rejected with an HTTP 415 Unsupported Media Type response */ - WHEN_NO_TEMPLATES = "WHEN_NO_TEMPLATES", + WHEN_NO_TEMPLATES = 'WHEN_NO_TEMPLATES', /** * Rejects unmapped content types with an HTTP 415 Unsupported Media Type response */ - NEVER = "NEVER" + NEVER = 'NEVER' } /** @@ -103,7 +103,7 @@ export enum KnownTemplateKey { /** * Default template, when no other pattern matches */ - DEFAULT = "$default" + DEFAULT = '$default' } /** @@ -115,49 +115,49 @@ export enum IntegrationMethod { * * Only method supported for WebSocket */ - GET = "GET", + GET = 'GET', /** * POST HTTP Method * * Not supported for WebSocket */ - POST = "POST", + POST = 'POST', /** * PUT HTTP Method * * Not supported for WebSocket */ - PUT = "PUT", + PUT = 'PUT', /** * DELETE HTTP Method * * Not supported for WebSocket */ - DELETE = "DELETE", + DELETE = 'DELETE', /** * OPTIONS HTTP Method * * Not supported for WebSocket */ - OPTIONS = "OPTIONS", + OPTIONS = 'OPTIONS', /** * HEAD HTTP Method * * Not supported for WebSocket */ - HEAD = "HEAD", + HEAD = 'HEAD', /** * PATCH HTTP Method * * Not supported for WebSocket */ - PATCH = "PATCH" + PATCH = 'PATCH' } /** @@ -319,7 +319,7 @@ export abstract class Integration extends Resource implements IIntegration { timeoutInMillis: (props.timeout ? props.timeout.toMilliseconds() : undefined), apiId: props.api.apiId, integrationType: props.type, - integrationUri: props.uri + integrationUri: props.uri, }); this.integrationId = this.resource.ref; @@ -331,7 +331,7 @@ export abstract class Integration extends Resource implements IIntegration { api: props.api.apiId, id, integrationType: props.type, - integrationUri: props.uri + integrationUri: props.uri, }); props.api.latestDeployment.registerDependency(this.resource); } @@ -360,7 +360,7 @@ export abstract class Integration extends Resource implements IIntegration { ...props, api: this.api, integration: this, - key + key, }); } @@ -375,7 +375,7 @@ export abstract class Integration extends Resource implements IIntegration { ...props, api: this.api, integration: this, - key + key, }); this.addPermissionsForRoute(route); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts index 7c3c117d7233e..4b548bdacc2c5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts @@ -49,7 +49,7 @@ export class LambdaIntegration extends Integration { super(scope, id, { ...props, type: props.proxy ? IntegrationType.AWS_PROXY : IntegrationType.AWS, - uri + uri, }); this.handler = props.handler; } @@ -59,10 +59,10 @@ export class LambdaIntegration extends Integration { const sourceArn = this.api.executeApiArn(route); this.handler.addPermission(`ApiPermission.${route.node.uniqueId}`, { principal: new ServicePrincipal('apigateway.amazonaws.com'), - sourceArn + sourceArn, }); } else { - throw new Error("This function is only supported on non-imported APIs"); + throw new Error('This function is only supported on non-imported APIs'); } } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts index bdd8deb9935e7..8bbc2fcc0a7be 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts @@ -24,37 +24,37 @@ export enum JsonSchemaType { /** * This type only allows null values */ - NULL = "null", + NULL = 'null', /** * Boolean property */ - BOOLEAN = "boolean", + BOOLEAN = 'boolean', /** * Object property, will have properties defined */ - OBJECT = "object", + OBJECT = 'object', /** * Array object, will have an item type defined */ - ARRAY = "array", + ARRAY = 'array', /** * Number property */ - NUMBER = "number", + NUMBER = 'number', /** * Integer property (inherited from Number with extra constraints) */ - INTEGER = "integer", + INTEGER = 'integer', /** * String property */ - STRING = "string" + STRING = 'string' } /** @@ -315,7 +315,7 @@ export class JsonSchemaMapper { */ public static toCfnJsonSchema(schema: JsonSchema): any { const result = JsonSchemaMapper._toCfnJsonSchema(schema); - if (! ("$schema" in result)) { + if (! ('$schema' in result)) { result.$schema = JsonSchemaVersion.DRAFT4; } return result; @@ -324,7 +324,7 @@ export class JsonSchemaMapper { private static readonly SchemaPropsWithPrefix: { [key: string]: string } = { schema: '$schema', ref: '$ref', - id: '$id' + id: '$id', }; // The value indicates whether direct children should be key-mapped. private static readonly SchemaPropsWithUserDefinedChildren: { [key: string]: boolean } = { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts index 6a4a89cc8ff7e..4a010de899349 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts @@ -11,12 +11,12 @@ export enum KnownModelKey { /** * Default model, when no other pattern matches */ - DEFAULT = "$default", + DEFAULT = '$default', /** * Default model, when no other pattern matches */ - EMPTY = "" + EMPTY = '' } /** @@ -26,23 +26,23 @@ export enum KnownContentTypes { /** * JSON request or response (default) */ - JSON = "application/json", + JSON = 'application/json', /** * XML request or response */ - XML = "application/xml", + XML = 'application/xml', /** * Pnain text request or response */ - TEXT = "text/plain", + TEXT = 'text/plain', /** * URL encoded web form */ - FORM_URL_ENCODED = "application/x-www-form-urlencoded", + FORM_URL_ENCODED = 'application/x-www-form-urlencoded', /** * Data from a web form */ - FORM_DATA = "multipart/form-data" + FORM_DATA = 'multipart/form-data' } /** @@ -155,7 +155,7 @@ export class Model extends Resource implements IModel { contentType: props.contentType || KnownContentTypes.JSON, apiId: props.api.apiId, name: this.modelName, - schema: JsonSchemaMapper.toCfnJsonSchema(props.schema) + schema: JsonSchemaMapper.toCfnJsonSchema(props.schema), }); this.modelId = this.resource.ref; @@ -167,7 +167,7 @@ export class Model extends Resource implements IModel { api: props.api.apiId, contentType: props.contentType, name: this.modelName, - schema: props.schema + schema: props.schema, }); props.api.latestDeployment.registerDependency(this.resource); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts index fc37c02e6149a..9f0d490c5c025 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts @@ -12,17 +12,17 @@ export enum KnownRouteResponseKey { /** * Default response, when no other pattern matches */ - DEFAULT = "$default", + DEFAULT = '$default', /** * Empty response */ - EMPTY = "empty", + EMPTY = 'empty', /** * Error response */ - ERROR = "error" + ERROR = 'error' } /** @@ -97,7 +97,7 @@ export class RouteResponse extends Resource implements IRouteResponse { let responseModels: { [key: string]: string } | undefined; if (props.responseModels !== undefined) { responseModels = Object.assign({}, ...Object.entries(props.responseModels).map((e) => { - return ({ [e[0]]: (typeof(e[1]) === "string" ? e[1] : e[1].modelName) }); + return ({ [e[0]]: (typeof(e[1]) === 'string' ? e[1] : e[1].modelName) }); })); } this.resource = new CfnRouteResponse(this, 'Resource', { @@ -105,7 +105,7 @@ export class RouteResponse extends Resource implements IRouteResponse { apiId: props.api.apiId, routeId: props.route.routeId, routeResponseKey: props.key, - responseModels + responseModels, }); if (props.api instanceof Api) { @@ -116,7 +116,7 @@ export class RouteResponse extends Resource implements IRouteResponse { api: props.api.apiId, route: props.route.routeId, routeResponseKey: props.key, - responseModels + responseModels, }); props.api.latestDeployment.registerDependency(this.resource); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts index 3108ec9ea86d3..253880aacf385 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts @@ -14,23 +14,23 @@ export enum AuthorizationType { /** * Open access (Web Socket, HTTP APIs). */ - NONE = "NONE", + NONE = 'NONE', /** * Use AWS IAM permissions (Web Socket APIs). */ - IAM = "AWS_IAM", + IAM = 'AWS_IAM', /** * Use a custom authorizer (Web Socket APIs). */ - CUSTOM = "CUSTOM", + CUSTOM = 'CUSTOM', /** * Use an AWS Cognito user pool (Web Socket APIs). */ - COGNITO = "COGNITO_USER_POOLS", + COGNITO = 'COGNITO_USER_POOLS', /** * Use JSON Web Tokens (HTTP APIs). */ - JWT = "JWT" + JWT = 'JWT' } /** @@ -40,15 +40,15 @@ export enum KnownRouteKey { /** * Default route, when no other pattern matches */ - DEFAULT = "$default", + DEFAULT = '$default', /** * This route is a reserved route, used when a client establishes a connection to the WebSocket API */ - CONNECT = "$connect", + CONNECT = '$connect', /** * This route is a reserved route, used when a client disconnects from the WebSocket API */ - DISCONNECT = "$disconnect" + DISCONNECT = '$disconnect' } /** @@ -251,7 +251,7 @@ export class Route extends Resource implements IRoute { let requestModels: { [key: string]: string } | undefined; if (props.requestModels !== undefined) { requestModels = Object.assign({}, ...Object.entries(props.requestModels).map((e) => { - return ({ [e[0]]: (typeof(e[1]) === "string" ? e[1] : e[1].modelName) }); + return ({ [e[0]]: (typeof(e[1]) === 'string' ? e[1] : e[1].modelName) }); })); } @@ -262,7 +262,7 @@ export class Route extends Resource implements IRoute { routeKey: props.key, target: `integrations/${props.integration.integrationId}`, requestModels, - authorizerId + authorizerId, }); this.routeId = this.resource.ref; @@ -274,7 +274,7 @@ export class Route extends Resource implements IRoute { routeKey: this.key, target: `integrations/${props.integration.integrationId}`, requestModels, - authorizerId + authorizerId, }); props.api.latestDeployment.registerDependency(this.resource); } @@ -292,7 +292,7 @@ export class Route extends Resource implements IRoute { ...props, route: this, api: this.api, - key + key, }); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts index 87dbd91a6efcc..bf9550d4952ef 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts @@ -11,17 +11,17 @@ export enum LoggingLevel { /** * Displays all log information */ - INFO = "INFO", + INFO = 'INFO', /** * Only displays errors */ - ERROR = "ERROR", + ERROR = 'ERROR', /** * Logging is turned off */ - OFF = "OFF" + OFF = 'OFF' } /** @@ -213,7 +213,7 @@ export class Stage extends Resource implements IStage { this.resource = new CfnStage(this, 'Resource', { ...props, apiId: props.api.apiId, - deploymentId: props.deployment.deploymentId + deploymentId: props.deployment.deploymentId, }); this.stageName = this.resource.ref; @@ -222,7 +222,7 @@ export class Stage extends Resource implements IStage { ...props, api: props.api.apiId, deployment: props.deployment.deploymentId, - id + id, }); } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts index b943c456aca07..94f1b762a13ca 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts @@ -15,13 +15,13 @@ test('minimal setup', () => { new apigw.ApiMapping(stack, 'mapping', { stage: api.deploymentStage!, domainName, - api + api, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::ApiMapping", { - ApiId: { Ref: "myapi4C7BF186" }, - Stage: { Ref: "myapiStageprod07E02E1F" }, - DomainName: { Ref: "domainname1131E743" } + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::ApiMapping', { + ApiId: { Ref: 'myapi4C7BF186' }, + Stage: { Ref: 'myapiStageprod07E02E1F' }, + DomainName: { Ref: 'domainname1131E743' }, })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts index cb53f6f708a13..ac0287f8e9bf7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts @@ -10,23 +10,26 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); + new apigw.Api(stack, 'my-api', { + protocolType: apigw.ProtocolType.WEBSOCKET, + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Api", { + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { Name: 'my-api', ProtocolType: apigw.ProtocolType.WEBSOCKET, - RouteSelectionExpression: '${context.routeKey}' + RouteSelectionExpression: '${context.routeKey}', })); - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Deployment", { - ApiId: { Ref: "myapi4C7BF186" } + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Deployment', { + ApiId: { Ref: 'myapi4C7BF186' }, })); - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Stage", { - ApiId: { Ref: "myapi4C7BF186" }, - StageName: "prod", - DeploymentId: { Ref: "myapiDeployment92F2CB492D341D1B" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Stage', { + ApiId: { Ref: 'myapi4C7BF186' }, + StageName: 'prod', + DeploymentId: { Ref: 'myapiDeployment92F2CB492D341D1B' }, })); }); @@ -38,16 +41,16 @@ test('minimal setup (no deploy)', () => { new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Api", { - Name: 'my-api' + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { + Name: 'my-api', })); - cdkExpect(stack).notTo(haveResource("AWS::ApiGatewayV2::Deployment")); - cdkExpect(stack).notTo(haveResource("AWS::ApiGatewayV2::Stage")); + cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Deployment')); + cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Stage')); }); test('minimal setup (no deploy, error)', () => { @@ -61,8 +64,8 @@ test('minimal setup (no deploy, error)', () => { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, deployOptions: { - stageName: 'testStage' - } + stageName: 'testStage', + }, }); }).toThrow(); }); @@ -76,25 +79,25 @@ test('URLs and ARNs', () => { const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { key: 'routeKey', - routeId: 'routeId' + routeId: 'routeId', }); // THEN - expect(stack.resolve(api.clientUrl())).toEqual({ "Fn::Join": [ "", [ "wss://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/", { Ref: "myapiStageprod07E02E1F" } ] ] }); - expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "wss://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev" ] ] }); + expect(stack.resolve(api.clientUrl())).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiStageprod07E02E1F' } ] ] }); + expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); - expect(stack.resolve(api.connectionsUrl())).toEqual({ "Fn::Join": [ "", [ "https://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/", { Ref: "myapiStageprod07E02E1F" }, "/@connections" ] ] }); - expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "https://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev/@connections" ] ] }); + expect(stack.resolve(api.connectionsUrl())).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiStageprod07E02E1F' }, '/@connections' ] ] }); + expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev/@connections' ] ] }); - expect(stack.resolve(api.executeApiArn())).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/*" ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/routeKey" ] ] }); - expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/*" ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/routeKey" ] ] }); + expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/*' ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/*' ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); - expect(stack.resolve(api.connectionsApiArn())).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/POST/*" ] ] }); - expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/", { Ref: "myapiStageprod07E02E1F" }, "/POST/my-connection" ] ] }); - expect(stack.resolve(api.connectionsApiArn(undefined, importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/POST/*" ] ] }); - expect(stack.resolve(api.connectionsApiArn('my-connection', importedStage))).toEqual({ "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":execute-api:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", { Ref: "myapi4C7BF186" }, "/dev/POST/my-connection" ] ] }); + expect(stack.resolve(api.connectionsApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/POST/*' ] ] }); + expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/POST/my-connection' ] ] }); + expect(stack.resolve(api.connectionsApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/POST/*' ] ] }); + expect(stack.resolve(api.connectionsApiArn('my-connection', importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/POST/my-connection' ] ] }); }); test('URLs and ARNs (no deploy)', () => { @@ -105,13 +108,13 @@ test('URLs and ARNs (no deploy)', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); // THEN - expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "wss://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev" ] ] }); - expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ "Fn::Join": [ "", [ "https://", { Ref: "myapi4C7BF186" }, ".execute-api.", { Ref: "AWS::Region" }, ".amazonaws.com/dev/@connections" ] ] }); + expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); + expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev/@connections' ] ] }); expect(() => stack.resolve(api.clientUrl())).toThrow(); expect(() => stack.resolve(api.connectionsUrl())).toThrow(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts index dbbf5e28081fa..94cc82f3634c0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts @@ -13,22 +13,22 @@ test('minimal setup', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); const functionArn = stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'}); new apigw.Authorizer(stack, 'authorizer', { authorizerName: 'my-authorizer', authorizerType: apigw.AuthorizerType.JWT, authorizerUri: `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${functionArn}/invocations`, - api + api, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Authorizer", { - ApiId: { Ref: "myapi4C7BF186" }, - Name: "my-authorizer", - AuthorizerType: "JWT", - AuthorizerUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] }, - IdentitySource: [] + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Authorizer', { + ApiId: { Ref: 'myapi4C7BF186' }, + Name: 'my-authorizer', + AuthorizerType: 'JWT', + AuthorizerUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, + IdentitySource: [], })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts index 77ced1c2825d5..e16ca867ac18f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts @@ -14,14 +14,14 @@ test('minimal setup', () => { domainName: 'test.example.com', domainNameConfigurations: [ { - endpointType: apigw.EndpointType.EDGE - } - ] + endpointType: apigw.EndpointType.EDGE, + }, + ], }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::DomainName", { + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::DomainName', { DomainName: 'test.example.com', - DomainNameConfigurations: [ { EndpointType: "EDGE" } ] + DomainNameConfigurations: [ { EndpointType: 'EDGE' } ], })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts index 1cb20e6476b4f..edde8272a2e77 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts @@ -14,17 +14,17 @@ test('Lambda integration', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})) + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Integration", { - ApiId: { Ref: "myapi4C7BF186" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, IntegrationType: apigw.IntegrationType.AWS, - IntegrationUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] } + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, })); }); @@ -36,21 +36,21 @@ test('Lambda integration (with extra params)', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), connectionType: apigw.ConnectionType.INTERNET, - integrationMethod: apigw.IntegrationMethod.GET + integrationMethod: apigw.IntegrationMethod.GET, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Integration", { - ApiId: { Ref: "myapi4C7BF186" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, IntegrationType: apigw.IntegrationType.AWS, - IntegrationUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] }, + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, IntegrationMethod: apigw.IntegrationMethod.GET, - ConnectionType: apigw.ConnectionType.INTERNET + ConnectionType: apigw.ConnectionType.INTERNET, })); }); @@ -62,18 +62,18 @@ test('Lambda integration (proxy)', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), - proxy: true + proxy: true, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Integration", { - ApiId: { Ref: "myapi4C7BF186" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, IntegrationType: apigw.IntegrationType.AWS_PROXY, - IntegrationUri: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":apigateway:", { Ref: "AWS::Region" }, ":lambda:path/2015-03-31/functions/arn:", { Ref: "AWS::Partition" }, ":lambda:", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":function:my-function/invocations"]] } + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, })); }); @@ -85,17 +85,17 @@ test('Integration response', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})) + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), }); integration.addResponse(apigw.KnownIntegrationResponseKey.DEFAULT); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::IntegrationResponse", { - ApiId: { Ref: "myapi4C7BF186" }, - IntegrationId: { Ref: "myapimyFunction27BC3796" }, - IntegrationResponseKey: apigw.KnownIntegrationResponseKey.DEFAULT + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationId: { Ref: 'myapimyFunction27BC3796' }, + IntegrationResponseKey: apigw.KnownIntegrationResponseKey.DEFAULT, })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts index 4932d53b6bf88..8855b6c3637ea 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts @@ -14,7 +14,7 @@ test('route', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), @@ -22,26 +22,26 @@ test('route', () => { integration.addRoute(apigw.KnownRouteKey.CONNECT, { modelSelectionExpression: apigw.KnownModelKey.DEFAULT, requestModels: { - [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: "statusInputModel", type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }) + [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusInputModel', type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }), }, - routeResponseSelectionExpression: apigw.KnownRouteResponseKey.DEFAULT + routeResponseSelectionExpression: apigw.KnownRouteResponseKey.DEFAULT, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Route", { - ApiId: { Ref: "myapi4C7BF186" }, - RouteKey: "$connect", - Target: { "Fn::Join": ["", [ "integrations/", { Ref: "myapimyFunction27BC3796" } ] ] }, - ModelSelectionExpression: "$default", + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteKey: '$connect', + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunction27BC3796' } ] ] }, + ModelSelectionExpression: '$default', RequestModels: { - $default: "statusInputModel" - } + $default: 'statusInputModel', + }, })); - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Model", { - ApiId: { Ref: "myapi4C7BF186" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { + ApiId: { Ref: 'myapi4C7BF186' }, ContentType: apigw.KnownContentTypes.JSON, - Name: "statusInputModel" + Name: 'statusInputModel', })); }); @@ -52,7 +52,7 @@ test('route (no model)', () => { // WHEN const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.HTTP, - deploy: false + deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), @@ -60,10 +60,10 @@ test('route (no model)', () => { integration.addRoute('POST /'); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Route", { - ApiId: { Ref: "myapi4C7BF186" }, - RouteKey: "POST /", - Target: { "Fn::Join": ["", [ "integrations/", { Ref: "myapimyFunction27BC3796" } ] ] } + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteKey: 'POST /', + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunction27BC3796' } ] ] }, })); }); @@ -75,33 +75,33 @@ test('route response', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`) + handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), }); const route = integration.addRoute(apigw.KnownRouteKey.CONNECT, {}); route.addResponse(apigw.KnownRouteKey.CONNECT, { modelSelectionExpression: apigw.KnownModelKey.DEFAULT, responseModels: { - [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: "statusResponse", type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }) - } + [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }), + }, }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::RouteResponse", { - ApiId: { Ref: "myapi4C7BF186" }, - RouteId: { Ref: "myapimyFunctionRouteconnectA2AF3242" }, - RouteResponseKey: "$connect", - ModelSelectionExpression: "$default", + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::RouteResponse', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteId: { Ref: 'myapimyFunctionRouteconnectA2AF3242' }, + RouteResponseKey: '$connect', + ModelSelectionExpression: '$default', ResponseModels: { - $default: "statusResponse" - } + $default: 'statusResponse', + }, })); - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Model", { - ApiId: { Ref: "myapi4C7BF186" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { + ApiId: { Ref: 'myapi4C7BF186' }, ContentType: apigw.KnownContentTypes.JSON, - Name: "statusResponse" + Name: 'statusResponse', })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts index 24dd6e52664a6..119c1f4601359 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts @@ -13,21 +13,21 @@ test('minimal setup', () => { const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false + deploy: false, }); const deployment = new apigw.Deployment(stack, 'deployment', { - api + api, }); new apigw.Stage(stack, 'stage', { api, deployment, - stageName: 'dev' + stageName: 'dev', }); // THEN - cdkExpect(stack).to(haveResource("AWS::ApiGatewayV2::Stage", { - ApiId: { Ref: "myapi4C7BF186" }, - StageName: "dev", - DeploymentId: { Ref: "deployment33381975F8795BE8" }, + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Stage', { + ApiId: { Ref: 'myapi4C7BF186' }, + StageName: 'dev', + DeploymentId: { Ref: 'deployment33381975F8795BE8' }, })); }); \ No newline at end of file From fd9191937f8bf5dca58dc445abad26deb364018f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 28 Apr 2020 14:38:03 +0200 Subject: [PATCH 4/7] fix(aws-apigatewayv2): integrate HTTP APIs and WebSocket APIs --- packages/@aws-cdk/aws-apigatewayv2/README.md | 271 +++++++++- .../aws-apigatewayv2/lib/api-mapping.ts | 2 +- packages/@aws-cdk/aws-apigatewayv2/lib/api.ts | 97 ++-- .../aws-apigatewayv2/lib/authorizer.ts | 7 +- .../aws-apigatewayv2/lib/deployment.ts | 54 +- .../aws-apigatewayv2/lib/domain-name.ts | 4 +- .../@aws-cdk/aws-apigatewayv2/lib/http-api.ts | 470 ++++++++++++++++++ .../@aws-cdk/aws-apigatewayv2/lib/index.ts | 2 + .../lib/integration-response.ts | 21 +- .../aws-apigatewayv2/lib/integration.ts | 260 ++++++---- .../lib/integrations/http-integration.ts | 100 ++++ .../lib/integrations/index.ts | 3 + .../lib/integrations/lambda-integration.ts | 83 +++- .../lib/integrations/mock-integration.ts | 68 +++ .../lib/integrations/service-integration.ts | 99 ++++ .../@aws-cdk/aws-apigatewayv2/lib/model.ts | 17 +- .../aws-apigatewayv2/lib/route-response.ts | 20 +- .../@aws-cdk/aws-apigatewayv2/lib/route.ts | 167 +++++-- .../@aws-cdk/aws-apigatewayv2/lib/stage.ts | 22 +- .../aws-apigatewayv2/lib/web-socket-api.ts | 380 ++++++++++++++ .../@aws-cdk/aws-apigatewayv2/package.json | 6 + .../aws-apigatewayv2/test/api-mapping.test.ts | 7 +- .../aws-apigatewayv2/test/api.test.ts | 120 ++++- .../test/integ.http-api.expected.json | 425 ++++++++++++++++ .../aws-apigatewayv2/test/integ.http-api.ts | 51 ++ .../test/integ.web-socket-api.expected.json | 246 +++++++++ .../test/integ.web-socket-api.ts | 60 +++ .../aws-apigatewayv2/test/integration.test.ts | 124 ++++- .../aws-apigatewayv2/test/route.test.ts | 36 +- .../aws-apigatewayv2/test/stage.test.ts | 2 +- 30 files changed, 2828 insertions(+), 396 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/http-api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 093a1f4571cc9..f1f965780eea2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -20,30 +20,49 @@ running on AWS Lambda, or any web application. APIs are defined through adding routes, and integrating them with AWS Services or APIs. Currently this module supports HTTP APIs and Web Socket APIs. -For example a Web Socket API with a "$connect" route (handling user connection) backed by +The module defines a broad 'Api' L2 Resource that maps closely to the L1 CloudFormation resource +but also provides a 'HttpApi' and 'WebSocketApi' constructs that provide additional support such +as clear definitions of what is allowed in HTTP or WebSocket APIs, and helper methods +to accelerate development. We recommend leveragint these constructs. + +For example a Web Socket API with a "$default" route (handling user connection) backed by an AWS Lambda function would be defined as follows: ```ts -const api = new apigatewayv2.Api(this, 'books-api', { - protocolType: apigatewayv2.ProtocolType.HTTP +const api = new apigatewayv2.WebSocketApi(this, 'books-api', { + // Gets the context from the WebSocket header + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, }); const backend = new lambda.Function(...); const integration = api.addLambdaIntegration('myFunction', { - handler: backend + handler: backend, }); -integration.addRoute('POST /'); +api.addRoute('$connect', integration); ``` -You can also supply `proxy: false`, in which case you will have to explicitly -define the API model: +An HTTP API to get a book by its ID would be provided as: ```ts +const api = new apigatewayv2.HttpApi(this, 'books-api'); + const backend = new lambda.Function(...); const integration = api.addLambdaIntegration('myFunction', { handler: backend, - proxy: false +}); + +api.addRoute('GET /book/{book_id}', integration); +``` + +APIs also support the automated creation of a '$default' (catch-all) route, +automatically, through the 'defaultHandler' property. + +```ts +const backend = new lambda.Function(...); + +const api = new apigatewayv2.HttpApi(this, 'books-api', { + defaultTarget: { handler: backend }, }); ``` @@ -53,6 +72,9 @@ Methods are associated with backend integrations, which are invoked when this method is called. API Gateway supports the following integrations: * `LambdaIntegration` - can be used to invoke an AWS Lambda function. + * `ServiceIntegration` - can be used to invoke an AWS Service. + * `HttpIntegration` - can be used to invoke an HTTP API ont he backend. + * `MockIntegration` - can be used to leverage Mocks for Web Scket APIs. The following example shows how to integrate the `GET /book/{book_id}` method to an AWS Lambda function: @@ -69,13 +91,13 @@ The following example shows how to use an API Key with a usage plan: ```ts const hello = new lambda.Function(...); -const api = new apigatewayv2.Api(this, 'hello-api', { - protocolType: apigatewayv2.ProtocolType.WEBSOCKET, - apiKeySelectionExpression: '$request.header.x-api-key' +const api = new apigatewayv2.WebSocketApi(this, 'hello-api', { + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + apiKeySelectionExpression: apigw.KnownApiKeySelectionExpression.HEADER_X_API_KEY, }); ``` -### Working with models +### Working with models in WebSocket APIs When you work with Lambda integrations that are not Proxy integrations, you have to define your models and mappings for the request, response, and integration. @@ -83,8 +105,8 @@ have to define your models and mappings for the request, response, and integrati ```ts const hello = new lambda.Function(...); -const api = new apigateway.RestApi(this, 'hello-api', { - protocolType: apigatewayv2.ProtocolType.HTTP +const api = new apigateway.WebSocketApi(this, 'hello-api', { + routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, }); const integration = api.addLambdaIntegration('myFunction', { @@ -98,6 +120,227 @@ integration.addRoute(apigw.KnownRouteKey.CONNECT, { routeResponseSelectionExpression: apigw.KnownRouteResponseKey.DEFAULT }); ``` +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + + +## 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. + +## API + +Amazon API Gateway supports `HTTP APIs`, `WebSocket APIs` and `REST APIs`. For more information about `WebSocket APIs` and `HTTP APIs`, +see [About WebSocket APIs in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) +and [HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html). + +For more information about `REST APIs`, see [Working with REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). +To create [REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html) with AWS CDK, use `@aws-cdk/aws-apigateway` instead. + + +### HTTP API + +HTTP APIs enable you to create RESTful APIs that integrate with AWS Lambda functions or to any routable HTTP endpoint. + +HTTP API supports both Lambda proxy integration and HTTP proxy integration. +See [Working with AWS Lambda Proxy Integrations for HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) +and [Working with HTTP Proxy Integrations for HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-http.html) +for more information. + + + +Use `HttpApi` to create HTTP APIs with Lambda or HTTP proxy integration. The $default route will be created as well that acts as a catch-all for requests that don’t match any other routes. + + +```ts +// Create a vanilla HTTP API with no integration targets +new apigatewayv2.HttpApi(stack, 'HttpApi1'); + +// Create a HTTP API with Lambda Proxy Integration as its $default route +const httpApi2 = new apigatewayv2.HttpApi(stack, 'HttpApi2', { + targetHandler: handler +}); + +// Create a HTTP API with HTTP Proxy Integration as its $default route +const httpApi3 = new apigatewayv2.HttpApi(stack, 'HttpApi3', { + targetUrl: checkIpUrl +}); +``` + +## Route + +Routes direct incoming API requests to backend resources. Routes consist of two parts: an HTTP method and a resource path—for example, +`GET /pets`. You can define specific HTTP methods for your route, or use the `ANY` method to match all methods that you haven't defined for a resource. +You can create a `$default route` that acts as a catch-all for requests that don’t match any other routes. See +[Working with Routes for HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html). + +When you create HTTP APIs with either `Lambda Proxy Integration` or `HTTP Proxy Integration`, the `$default route` will be created as well. + + +## Defining APIs + +APIs are defined as a hierarchy of routes. `addLambdaRoute` and `addHttpRoute` can be used to build this hierarchy. The root resource is `api.root`. + +For example, the following code defines an API that includes the following HTTP endpoints: ANY /, GET /books, POST /books, GET /books/{book_id}, DELETE /books/{book_id}. + +```ts +const checkIpUrl = 'https://checkip.amazonaws.com'; +const awsUrl = 'https://aws.amazon.com'; + +const api = new apigatewayv2.HttpApi(this, 'HttpApi'); +api.root + +api.root + // HTTP GET /foo + .addLambdaRoute('foo', 'Foo', { + target: handler, + method: apigatewayv2.HttpMethod.GET + }) + // HTTP ANY /foo/aws + .addHttpRoute('aws', 'AwsPage', { + targetUrl: awsUrl, + method: apigatewayv2.HttpMethod.ANY + }) + // HTTP ANY /foo/aws/checkip + .addHttpRoute('checkip', 'CheckIp', { + targetUrl: checkIpUrl, + method: apigatewayv2.HttpMethod.ANY + }); +``` + +To create a specific route directly rather than building it from the root, just create the `Route` resource with `targetHandler`, `targetUrl` or `integration`. + +```ts +// create a specific 'GET /some/very/deep/route/path' route with Lambda proxy integration for an existing HTTP API +const someDeepRoute = new apigatewayv2.Route(stack, 'SomeDeepRoute', { + api: httpApi, + httpPath: '/some/very/deep/route/path', + targetHandler +}); + +// with HTTP proxy integration +const someDeepRoute = new apigatewayv2.Route(stack, 'SomeDeepRoute', { + api: httpApi, + httpPath: '/some/very/deep/route/path', + targetUrl +}); + +// with existing integration resource +const someDeepRoute = new apigatewayv2.Route(stack, 'SomeDeepRoute', { + api: httpApi, + httpPath: '/some/very/deep/route/path', + integration +}); +``` + +You may also `addLambdaRoute` or `addHttpRoute` from the `HttpAppi` resource. + +```ts +// addLambdaRoute or addHttpRoute from the HttpApi resource +httpApi.addLambdaRoute('/foo/bar', 'FooBar', { + target: handler, + method: apigatewayv2.HttpMethod.GET +}); + +httpApi.addHttpRoute('/foo/bar', 'FooBar', { + targetUrl: awsUrl, + method: apigatewayv2.HttpMethod.ANY +}); +``` + +## Integration + +Integrations connect a route to backend resources. HTTP APIs support Lambda proxy and HTTP proxy integrations. +For example, you can configure a POST request to the /signup route of your API to integrate with a Lambda function +that handles signing up customers. + +Use `Integration` to create the integration resources + +```ts +// create API +const api = new apigatewayv2.HttpApi(stack, 'HttpApi', { + targetHandler +}); + +// create Integration +api.addServiceIntegration('IntegRootHandler', { + integrationMethod: apigatewayv2.HttpMethod.ANY, + url: awsUrl, + proxy: true, +}); + +// create a specific route with the integration above for the API +api.addRoute({ path: '/some/very/deep/route/path' }, integration); + +``` + +You may use `LambdaProxyIntegraion` or `HttpProxyIntegration` to easily create the integrations. + +```ts +// create a Lambda proxy integration +api.addLambdaIntegration('IntegRootHandler', { + targetHandler, + proxy: true, +}); + +// or create a Http proxy integration +api.addHttpIntegration('IntegRootHandler', { + targetUrl, + proxy: true, +}); +``` + +## Samples + +```ts +// create a HTTP API with HTTP proxy integration as the $default route +const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi', { + targetUrl: checkIpUrl +}); +// print the API URL +new cdk.CfnOutput(stack, 'URL', { value: httpApi.url} ); + +// create another HTTP API with Lambda proxy integration as the $default route +const httpApi2 = new apigatewayv2.HttpApi(stack, 'HttpApi2', { + targetHandler: handler +}); +// print the API URL +new cdk.CfnOutput(stack, 'URL2', { value: httpApi2.url }); + +// create a root route for the API with the integration we created above and assign the route resource +// as a 'root' property to the API +httpApi2.root = httpApi2.addLambdaRoute('/', 'RootRoute', { + target: rootHandler +}) + +// Now, extend the route tree from the root +httpApi2.root + // HTTP ANY /foo + .addLambdaRoute('foo', 'Foo', { + target: handler, + }) + // HTTP ANY /foo/aws + .addHttpRoute('aws', 'AwsPage', { + targetUrl: awsUrl, + method: apigatewayv2.HttpMethod.ANY + }) + // HTTP GET /foo/aws/checkip + .addHttpRoute('checkip', 'CheckIp', { + targetUrl: checkIpUrl, + method: apigatewayv2.HttpMethod.GET + }); + +// And create a specific route for it as well +// HTTP ANY /some/very/deep/route/path +httpApi2.addLambdaRoute('/some/very/deep/route/path', 'FooBar', { + target: handler +}); +``` ---- diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts index 3d7800672bb8d..62c3eadbcd298 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts @@ -85,10 +85,10 @@ export class ApiMapping extends Resource implements IApiMapping { super(scope, id); this.resource = new CfnApiMapping(this, 'Resource', { - ...props, apiId: props.api.apiId, domainName: ((typeof(props.domainName) === 'string') ? props.domainName : props.domainName.domainName), stage: props.stage.stageName, + apiMappingKey: props.apiMappingKey, }); this.apiMappingId = this.resource.ref; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts index 52aa3015470c9..288ddc6ca4d42 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts @@ -2,10 +2,6 @@ import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; import { CfnApi } from './apigatewayv2.generated'; import { Deployment } from './deployment'; -import { Integration } from './integration'; -import { LambdaIntegration, LambdaIntegrationOptions } from './integrations/lambda-integration'; -import { JsonSchema } from './json-schema'; -import { Model, ModelOptions } from './model'; import { IRoute, KnownRouteSelectionExpression } from './route'; import { IStage, Stage, StageOptions } from './stage'; @@ -324,8 +320,12 @@ export class Api extends Resource implements IApi { */ public deploymentStage?: Stage; + /** + * API Gateway deployment (if defined). + */ + public deployment?: Deployment; + protected resource: CfnApi; - protected deployment?: Deployment; constructor(scope: Construct, id: string, props?: ApiProps) { if (props === undefined) { @@ -366,7 +366,7 @@ export class Api extends Resource implements IApi { if (props.disableSchemaValidation !== undefined) { throw new Error('"disableSchemaValidation" is only supported with Web Socket APIs'); } - if (props.routeSelectionExpression !== undefined && props.apiKeySelectionExpression !== KnownRouteSelectionExpression.METHOD_PATH) { + if (props.routeSelectionExpression !== undefined && props.routeSelectionExpression !== KnownRouteSelectionExpression.METHOD_PATH) { throw new Error('"routeSelectionExpression" has a single supported value for HTTP APIs: "${request.method} ${request.path}"'); } break; @@ -374,31 +374,42 @@ export class Api extends Resource implements IApi { } this.resource = new CfnApi(this, 'Resource', { - ...props, name: this.physicalName, + apiKeySelectionExpression: props.apiKeySelectionExpression, + basePath: props.basePath, + body: props.body, + bodyS3Location: props.bodyS3Location, + corsConfiguration: props.corsConfiguration, + description: props.description, + disableSchemaValidation: props.disableSchemaValidation, + failOnWarnings: props.failOnWarnings, + protocolType: props.protocolType, + routeSelectionExpression: props.routeSelectionExpression, + // TODO: tags: props.tags, + version: props.version, }); this.apiId = this.resource.ref; const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { - // encode the stage name into the construct id, so if we change the stage name, it will recreate a new stage. - // stage name is part of the endpoint, so that makes sense. const stageName = (props.deployOptions && props.deployOptions.stageName) || 'prod'; this.deployment = new Deployment(this, 'Deployment', { api: this, description: 'Automatically created by the Api construct', - - // No stageName specified, this will be defined by the stage directly, as it will reference the deployment - retainDeployments: props.retainDeployments, }); - this.deploymentStage = new Stage(this, `Stage.${stageName}`, { - ...props.deployOptions, + this.deploymentStage = new Stage(this, 'DefaultStage', { deployment: this.deployment, api: this, stageName, description: 'Automatically created by the Api construct', + accessLogSettings: props.deployOptions?.accessLogSettings, + autoDeploy: props.deployOptions?.autoDeploy, + clientCertificateId: props.deployOptions?.clientCertificateId, + defaultRouteSettings: props.deployOptions?.defaultRouteSettings, + routeSettings: props.deployOptions?.routeSettings, + stageVariables: props.deployOptions?.stageVariables, }); } else { if (props.deployOptions) { @@ -407,38 +418,6 @@ export class Api extends Resource implements IApi { } } - /** - * API Gateway deployment that represents the latest changes of the API. - * This resource will be automatically updated every time the REST API model changes. - * This will be undefined if `deploy` is false. - */ - public get latestDeployment() { - return this.deployment; - } - - /** - * Defines an API Gateway Lambda integration. - * @param id The construct id - * @param props Lambda integration options - */ - public addLambdaIntegration(id: string, props: LambdaIntegrationOptions): Integration { - return new LambdaIntegration(this, id, { ...props, api: this }); - } - - /** - * Defines a model for this Api Gateway. - * @param schema The model schema - * @param props The model integration options - */ - public addModel(schema: JsonSchema, props?: ModelOptions): Model { - return new Model(this, `Model.${schema.title}`, { - ...props, - modelName: schema.title, - api: this, - schema, - }); - } - /** * Returns the ARN for a specific route and stage. * @@ -466,7 +445,7 @@ export class Api extends Resource implements IApi { * @param connectionId The identifier of this connection ('*' if not defined) * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') */ - public connectionsApiArn(connectionId: string = '*', stage?: IStage) { + public webSocketConnectionsApiArn(connectionId: string = '*', stage?: IStage) { const stack = Stack.of(this); const apiId = this.apiId; const stageName = ((stage === undefined) ? @@ -486,7 +465,27 @@ export class Api extends Resource implements IApi { * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. * @param stage The stage for this URL (if not defined, defaults to the deployment stage) */ - public clientUrl(stage?: IStage): string { + public httpsClientUrl(stage?: IStage): string { + const stack = Stack.of(this); + let stageName: string | undefined; + if (stage === undefined) { + if (this.deploymentStage === undefined) { + throw Error('No stage defined for this Api'); + } + stageName = this.deploymentStage.stageName; + } else { + stageName = stage.stageName; + } + return `https://${this.apiId}.execute-api.${stack.region}.amazonaws.com/${stageName}`; + } + + /** + * Returns the client URL for this Api, for a specific stage. + * + * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. + * @param stage The stage for this URL (if not defined, defaults to the deployment stage) + */ + public webSocketClientUrl(stage?: IStage): string { const stack = Stack.of(this); let stageName: string | undefined; if (stage === undefined) { @@ -506,7 +505,7 @@ export class Api extends Resource implements IApi { * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. * @param stage The stage for this URL (if not defined, defaults to the deployment stage) */ - public connectionsUrl(stage?: IStage): string { + public webSocketConnectionsUrl(stage?: IStage): string { const stack = Stack.of(this); let stageName: string | undefined; if (stage === undefined) { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts index a3d4d86312722..a64a13a0c9932 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts @@ -177,10 +177,15 @@ export class Authorizer extends Resource implements IAuthorizer { super(scope, id); this.resource = new CfnAuthorizer(this, 'Resource', { - ...props, identitySource: (props.identitySource ? props.identitySource : []), apiId: props.api.apiId, name: props.authorizerName, + authorizerType: props.authorizerType, + authorizerCredentialsArn: props.authorizerCredentialsArn, + // TODO: authorizerResultTtlInSeconds: props.authorizerResultTtl.toSeconds(), + authorizerUri: props.authorizerUri, + // TODO: identityValidationExpression: props.identityValidationExpression + jwtConfiguration: props.jwtConfiguration, }); this.authorizerId = this.resource.ref; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts index dce7c6034086a..567c5ed987255 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts @@ -1,10 +1,8 @@ -import { CfnResource, Construct, IResource, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; +import { CfnResource, Construct, IResource, Lazy, Resource } from '@aws-cdk/core'; import { IApi } from './api'; import { CfnDeployment } from './apigatewayv2.generated'; -import { createHash } from 'crypto'; - /** * Defines the contract for an Api Gateway V2 Deployment. */ @@ -38,15 +36,6 @@ export interface DeploymentProps { * @default - All stages. */ readonly stageName?: string; - - /** - * Retains old deployment resources when the API changes. This allows - * manually reverting stages to point to old deployments via the AWS - * Console. - * - * @default false - */ - readonly retainDeployments?: boolean; } /** @@ -101,8 +90,6 @@ export class Deployment extends Resource implements IDeployment { public readonly deploymentId: string; protected resource: CfnDeployment; - private hashComponents = new Array(); - private originalLogicalId: string; constructor(scope: Construct, id: string, props: DeploymentProps) { super(scope, id); @@ -113,29 +100,7 @@ export class Deployment extends Resource implements IDeployment { stageName: props.stageName, }); - if ((props.retainDeployments === undefined) || (props.retainDeployments === true)) { - this.resource.applyRemovalPolicy(RemovalPolicy.RETAIN); - } this.deploymentId = Lazy.stringValue({ produce: () => this.resource.ref }); - this.originalLogicalId = Stack.of(this).getLogicalId(this.resource); - } - - /** - * Adds a component to the hash that determines this Deployment resource's - * logical ID. - * - * This should be called by constructs of the API Gateway model that want to - * invalidate the deployment when their settings change. The component will - * be resolved during synthesis so tokens are welcome. - * - * @param data The data to add to this hash - */ - public addToLogicalId(data: any) { - if (this.node.locked) { - throw new Error('Cannot modify the logical ID when the construct is locked'); - } - - this.hashComponents.push(data); } /** @@ -148,21 +113,4 @@ export class Deployment extends Resource implements IDeployment { public registerDependency(dependency: CfnResource) { this.resource.addDependsOn(dependency); } - - /** - * Hooks into synthesis to calculate a logical ID that hashes all the components - * add via `addToLogicalId`. - */ - protected prepare() { - const stack = Stack.of(this); - - // if hash components were added to the deployment, we use them to calculate - // a logical ID for the deployment resource. - if (this.hashComponents.length > 0) { - const md5 = createHash('md5'); - this.hashComponents.map(c => stack.resolve(c)).forEach(c => md5.update(JSON.stringify(c))); - this.resource.overrideLogicalId(this.originalLogicalId + md5.digest('hex').substr(0, 8).toUpperCase()); - } - super.prepare(); - } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts index c0372c8df8350..899ccbf58abfd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts @@ -133,7 +133,9 @@ export class DomainName extends Resource implements IDomainName { super(scope, id); this.resource = new CfnDomainName(this, 'Resource', { - ...props, + domainName: props.domainName, + domainNameConfigurations: props.domainNameConfigurations, + // TODO: tags: props.tags }); this.domainName = this.resource.ref; this.regionalDomainName = this.resource.attrRegionalDomainName; 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..524440c67fdeb --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http-api.ts @@ -0,0 +1,470 @@ +import { IFunction } from '@aws-cdk/aws-lambda'; +import { CfnResource, Construct, IConstruct } from '@aws-cdk/core'; + +import { Api, BasePath, BodyS3Location, CorsConfiguration, ProtocolType } from './api'; +import { ApiMapping, ApiMappingOptions } from './api-mapping'; +import { HttpApiIntegrationMethod, Integration } from './integration'; +import { HttpApiHttpIntegrationOptions, HttpIntegration } from './integrations/http-integration'; +import { HttpApiLambdaIntegrationOptions, LambdaIntegration } from './integrations/lambda-integration'; +import { HttpApiServiceIntegrationOptions, ServiceIntegration } from './integrations/service-integration'; +import { HttpApiRouteOptions, IRoute, KnownRouteKey, KnownRouteSelectionExpression, Route } from './route'; +import { IStage, StageOptions } from './stage'; + +/** + * Specifies the integration's HTTP method type (only GET is supported for WebSocket) + */ +export enum HttpMethod { + /** + * All HTTP Methods are supported + */ + ANY = 'ANY', + + /** + * GET HTTP Method + */ + GET = 'GET', + + /** + * POST HTTP Method + */ + POST = 'POST', + + /** + * PUT HTTP Method + */ + PUT = 'PUT', + + /** + * DELETE HTTP Method + */ + DELETE = 'DELETE', + + /** + * OPTIONS HTTP Method + */ + OPTIONS = 'OPTIONS', + + /** + * HEAD HTTP Method + */ + HEAD = 'HEAD', + + /** + * PATCH HTTP Method + */ + PATCH = 'PATCH' +} + +/** + * Defines where an Open API Definition is stored + */ +export interface HttpApiBody { + /** + * Stored inline in this declaration. + * + * If this is not defined, `bodyS3Location` has to be defined + * + * @default - Use S3 Location + */ + readonly body?: string; + /** + * Stored in an Amazon S3 Object. + * + * If this is not defined, `body` has to be defined + * + * @default - Use inline definition + */ + readonly bodyS3Location?: BodyS3Location; +} + +/** + * Defines a default handler for the Api + * + * One of the properties need to be defined + */ +export interface HttpApiDefaultTarget { + /** + * Use an AWS Lambda function + * + * If this is not defined, `uri` has to be defined + * + * @default - use one of the other properties + */ + readonly handler?: IFunction; + + /** + * Use a URI for the handler. + * If a string is provided, it will test is the string starts with + * - 'http://' or 'https://': it creates an http + * - 'arn:': it will create an AWS Serice integration + * - it will fail for any other value + * + * If this is not defined, `handler` has to be defined + * + * @default - Use inline definition + */ + readonly uri?: string; +} + +/** + * Defines the contract for an Api Gateway V2 HTTP Api. + */ +export interface IHttpApi extends IConstruct { + /** + * The ID of this API Gateway Api. + * @attribute + */ + readonly apiId: string; +} + +/** + * Defines the properties of a Web Socket API + */ +export interface HttpApiProps { + /** + * Indicates if a Deployment should be automatically created for this API, + * and recreated when the API model (route, integration) changes. + * + * Since API Gateway deployments are immutable, When this option is enabled + * (by default), an AWS::ApiGatewayV2::Deployment resource will automatically + * created with a logical ID that hashes the API model (methods, resources + * and options). This means that when the model changes, the logical ID of + * this CloudFormation resource will change, and a new deployment will be + * created. + * + * If this is set, `latestDeployment` will refer to the `Deployment` object + * and `deploymentStage` will refer to a `Stage` that points to this + * deployment. To customize the stage options, use the `deployOptions` + * property. + * + * A CloudFormation Output will also be defined with the root URL endpoint + * of this REST API. + * + * @default true + */ + readonly deploy?: boolean; + + /** + * Options for the API Gateway stage that will always point to the latest + * deployment when `deploy` is enabled. If `deploy` is disabled, + * this value cannot be set. + * + * @default - default options + */ + readonly deployOptions?: StageOptions; + + /** + * Retains old deployment resources when the API changes. This allows + * manually reverting stages to point to old deployments via the AWS + * Console. + * + * @default false + */ + readonly retainDeployments?: boolean; + + /** + * A name for the API Gateway Api resource. + * + * @default - ID of the Api construct. + */ + readonly apiName?: string; + + /** + * Specifies how to interpret the base path of the API during import. + * + * @default 'ignore' + */ + readonly basePath?: BasePath; + + /** + * The OpenAPI definition. Used to import an HTTP Api. + * Use either a body definition or the location of an Amazon S3 Object + * + * @default - no import + */ + readonly body?: HttpApiBody; + + /** + * A CORS configuration. + * + * @default - CORS not activated + */ + readonly corsConfiguration?: CorsConfiguration; + + /** + * A description of the purpose of this API Gateway Api resource. + * + * @default - No description. + */ + readonly description?: string; + + /** + * Indicates whether schema validation will be disabled for this Api + * + * @default false + */ + readonly disableSchemaValidation?: boolean; + + /** + * Indicates the version number for this Api + * + * @default false + */ + readonly version?: string; + + /** + * Specifies whether to rollback the API creation (`true`) or not (`false`) when a warning is encountered + * + * @default false + */ + readonly failOnWarnings?: boolean; + + /** + * If defined, creates a default proxy target for the HTTP Api. + * + * @default - no default handler or route + */ + readonly defaultTarget?: HttpApiDefaultTarget; +} + +/** + * Represents an HTTP Route entry + */ +export interface HttpRouteEntry { + /** + * Method for thei API Route + * + * @default 'ANY' + */ + readonly method?: HttpMethod; + + /** + * Path of the route + */ + readonly path: string; +} + +export declare type HttpRouteName = KnownRouteKey | HttpRouteEntry | string; + +/** + * Represents an HTTP API in Amazon API Gateway v2. + * + * Use `addModel` and `addLambdaIntegration` to configure the API model. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + */ +export class HttpApi extends Construct implements IHttpApi { + /** + * Creates a new imported API + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param apiId Identifier of the API + */ + public static fromApiId(scope: Construct, id: string, apiId: string): IHttpApi { + class Import extends Construct implements IHttpApi { + public readonly apiId = apiId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Api. + */ + public readonly apiId: string; + + protected readonly resource: Api; + + constructor(scope: Construct, id: string, props?: HttpApiProps) { + if (props === undefined) { + props = {}; + } + super(scope, id); + + this.resource = new Api(this, 'Default', { + ...props, + apiName: props.apiName ?? id, + body: ((props.body !== undefined) && (props.body.body !== undefined) ? props.body.body : undefined), + bodyS3Location: ((props.body !== undefined) && (props.body.bodyS3Location !== undefined) ? props.body.bodyS3Location : undefined), + protocolType: ProtocolType.HTTP, + routeSelectionExpression: KnownRouteSelectionExpression.METHOD_PATH, + }); + + this.apiId = this.resource.apiId; + + if (props.defaultTarget !== undefined) { + let integration; + if (props.defaultTarget.handler !== undefined) { + integration = this.addLambdaIntegration('default', { + handler: props.defaultTarget.handler, + }); + } else if (props.defaultTarget.uri) { + const uri = props.defaultTarget.uri; + if (uri.startsWith('https://') || uri.startsWith('http://')) { + integration = this.addHttpIntegration('default', { + url: uri, + }); + } else if (uri.startsWith('arn:')) { + integration = this.addServiceIntegration('default', { + arn: uri, + }); + } else { + throw new Error('Invalid string format, must be a fully qualified ARN or a URL'); + } + } else { + throw new Error('You must specify an ARN, a URL, or a Lambda Function'); + } + + this.addRoute(KnownRouteKey.DEFAULT, integration, {}); + } + } + + /** + * API Gateway deployment used for automated deployments. + */ + public get deployment() { + return this.resource.deployment; + } + + /** + * API Gateway stage used for automated deployments. + */ + public get deploymentStage() { + return this.resource.deploymentStage; + } + + /** + * Creates a new integration for this api, using a Lambda integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addLambdaIntegration(id: string, props: HttpApiLambdaIntegrationOptions): LambdaIntegration { + const integration = new LambdaIntegration(this, `${id}.lambda.integration`, { + ...props, + payloadFormatVersion: props.payloadFormatVersion ?? '1.0', + api: this.resource, + proxy: true, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new integration for this api, using a HTTP integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addHttpIntegration(id: string, props: HttpApiHttpIntegrationOptions): HttpIntegration { + const integration = new HttpIntegration(this, `${id}.http.integration`, { + ...props, + integrationMethod: props.integrationMethod ?? HttpApiIntegrationMethod.ANY, + payloadFormatVersion: props.payloadFormatVersion ?? '1.0', + api: this.resource, + proxy: true, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new service for this api, using a Lambda integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addServiceIntegration(id: string, props: HttpApiServiceIntegrationOptions): ServiceIntegration { + const integration = new ServiceIntegration(this, `${id}.service.integration`, { + ...props, + integrationMethod: props.integrationMethod ?? HttpApiIntegrationMethod.ANY, + payloadFormatVersion: props.payloadFormatVersion ?? '1.0', + api: this.resource, + proxy: true, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new route for this api, on the specified methods. + * + * @param key the route key (predefined or not) to use + * @param integration [disable-awslint:ref-via-interface] the integration to use for this route + * @param props the properties for this route + */ + public addRoute( + key: HttpRouteName, + integration: Integration | LambdaIntegration | HttpIntegration | ServiceIntegration, + props?: HttpApiRouteOptions): Route { + const keyName = ((typeof(key) === 'object') ? `${key.method ?? 'ANY'} ${key.path}` : key); + const route = new Route(this, `${keyName}.route`, { + ...props, + api: this.resource, + integration: ((integration instanceof Integration) ? integration : integration.resource), + key: keyName, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(route.node.defaultChild as CfnResource); + } + return route; + } + + /** + * Creates a new route for this api, on the specified methods. + * + * @param keys the route keys (predefined or not) to use + * @param integration [disable-awslint:ref-via-interface] the integration to use for these routes + * @param props the properties for these routes + */ + public addRoutes( + keys: HttpRouteName[], + integration: Integration | LambdaIntegration | HttpIntegration | ServiceIntegration, + props?: HttpApiRouteOptions): Route[] { + return keys.map((key) => this.addRoute(key, integration, props)); + } + + /** + * Creates a new domain name mapping for this api. + * + * @param props the properties for this Api Mapping + */ + public addApiMapping(props: ApiMappingOptions): ApiMapping { + const mapping = new ApiMapping(this, `${props.domainName}.${props.stage}.mapping`, { + ...props, + api: this.resource, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(mapping.node.defaultChild as CfnResource); + } + return mapping; + } + + /** + * Returns the ARN for a specific route and stage. + * + * @param route The route for this ARN ('*' if not defined) + * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') + */ + public executeApiArn(route?: IRoute, stage?: IStage) { + return this.resource.executeApiArn(route, stage); + } + + /** + * Returns the client URL for this Api, for a specific stage. + * + * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. + * @param stage The stage for this URL (if not defined, defaults to the deployment stage) + */ + public clientUrl(stage?: IStage): string { + return this.resource.httpsClientUrl(stage); + } +} \ 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 8b231848b079f..e3240b2a48e78 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -5,6 +5,7 @@ export * from './api-mapping'; export * from './authorizer'; export * from './deployment'; export * from './domain-name'; +export * from './http-api'; export * from './integration'; export * from './integration-response'; export * from './integrations'; @@ -13,3 +14,4 @@ export * from './model'; export * from './route'; export * from './route-response'; export * from './stage'; +export * from './web-socket-api'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts index ecd8c326806cc..e4db256df349c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts @@ -1,6 +1,6 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { Api, IApi } from './api'; +import { IApi } from './api'; import { CfnIntegrationResponse } from './apigatewayv2.generated'; import { ContentHandlingStrategy, IIntegration, KnownTemplateKey } from './integration'; @@ -107,25 +107,14 @@ export class IntegrationResponse extends Resource implements IIntegrationRespons constructor(scope: Construct, id: string, props: IntegrationResponseProps) { super(scope, id); - this.resource = new CfnIntegrationResponse(this, 'Resource', { - ...props, apiId: props.api.apiId, integrationId: props.integration.integrationId, integrationResponseKey: props.key, + contentHandlingStrategy: props.contentHandlingStrategy, + responseParameters: props.responseParameters, + responseTemplates: props.responseTemplates, + templateSelectionExpression: props.templateSelectionExpression, }); - - if (props.api instanceof Api) { - if (props.api.latestDeployment) { - props.api.latestDeployment.addToLogicalId({ - ...props, - api: props.api.apiId, - integration: props.integration.integrationId, - id, - integrationResponseKey: props.key, - }); - props.api.latestDeployment.registerDependency(this.resource); - } - } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts index 0b5c010be4693..9a71d219bacdf 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts @@ -1,9 +1,8 @@ import { Construct, Duration, IResource, Resource } from '@aws-cdk/core'; -import { Api, IApi } from './api'; +import { IApi } from './api'; import { CfnIntegration } from './apigatewayv2.generated'; import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from './integration-response'; -import { IRoute, KnownRouteKey, Route, RouteOptions } from './route'; /** * The type of the network connection to the integration endpoint. @@ -109,57 +108,63 @@ export enum KnownTemplateKey { /** * Specifies the integration's HTTP method type (only GET is supported for WebSocket) */ -export enum IntegrationMethod { +export enum HttpApiIntegrationMethod { + /** + * All HTTP Methods are supported + */ + ANY = 'ANY', + /** * GET HTTP Method - * - * Only method supported for WebSocket */ GET = 'GET', /** * POST HTTP Method - * - * Not supported for WebSocket */ POST = 'POST', /** * PUT HTTP Method - * - * Not supported for WebSocket */ PUT = 'PUT', /** * DELETE HTTP Method - * - * Not supported for WebSocket */ DELETE = 'DELETE', /** * OPTIONS HTTP Method - * - * Not supported for WebSocket */ OPTIONS = 'OPTIONS', /** * HEAD HTTP Method - * - * Not supported for WebSocket */ HEAD = 'HEAD', /** * PATCH HTTP Method - * - * Not supported for WebSocket */ PATCH = 'PATCH' } +/** + * The TLS configuration for a private integration. If you specify a TLS configuration, + * private integration traffic uses the HTTPS protocol. + */ +export interface TlsConfig { + /** + * If you specify a server name, API Gateway uses it to verify the hostname on + * the integration's certificate. + * + * The server name is also included in the TLS handshake to support + * Server Name Indication (SNI) or virtual hosting. + */ + readonly serverNameToVerify: string; +} + /** * Defines the contract for an Api Gateway V2 Deployment. */ @@ -176,20 +181,13 @@ export interface IIntegration extends IResource { * * This interface is used by the helper methods in `Api` and the sub-classes */ -export interface IntegrationOptions { +export interface BaseIntegrationOptions { /** * The type of the network connection to the integration endpoint. * * @default 'INTERNET' */ - readonly connectionType?: ConnectionType | string; - - /** - * The integration type of an integration. - * - * @default - Pass through unmodified - */ - readonly contentHandlingStrategy?: ContentHandlingStrategy | string; + readonly connectionType?: ConnectionType; /** * Specifies the credentials required for the integration, if any. @@ -210,13 +208,34 @@ export interface IntegrationOptions { */ readonly description?: string; + /** + * Custom timeout between 50 and 29,000 milliseconds for WebSocket APIs and between 50 and 30,000 milliseconds for HTTP APIs. + * + * @default - timeout is 29 seconds for WebSocket APIs and 30 seconds for HTTP APIs. + */ + readonly timeout?: Duration; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + * + * This interface is used by the helper methods in `Api` and the sub-classes + */ +export interface IntegrationOptions extends BaseIntegrationOptions { + /** + * Specifies how to handle response payload content type conversions. + * + * @default - Pass through unmodified + */ + readonly contentHandlingStrategy?: string; + /** * Specifies the pass-through behavior for incoming requests based on the `Content-Type` header in the request, * and the available mapping templates specified as the `requestTemplates` property on the `Integration` resource. * * @default - the response payload will be passed through from the integration response to the route response or method response unmodified */ - readonly passthroughBehavior?: PassthroughBehavior | string; + readonly passthroughBehavior?: string; /** * A key-value map specifying request parameters that are passed from the method request to the backend. @@ -226,8 +245,6 @@ export interface IntegrationOptions { * The method request parameter value must match the pattern of `method.request.{location}.{name}`, where `{location}` is * `querystring`, `path`, or `header`; and `{name}` must be a valid and unique method request parameter name. * - * Supported only for WebSocket APIs - * * @default - no parameter used */ readonly requestParameters?: { [key: string]: string }; @@ -237,8 +254,6 @@ export interface IntegrationOptions { * the `Content-Type` header sent by the client. The content type value is the key in this map, and the * template is the value. * - * Supported only for WebSocket APIs. - * * @default - no templates used */ readonly requestTemplates?: { [key: string]: string }; @@ -246,25 +261,117 @@ export interface IntegrationOptions { /** * The template selection expression for the integration. * - * Supported only for WebSocket APIs. + * @default - no template selected + */ + readonly templateSelectionExpression?: string; + + /** + * The ID of the VPC link for a private integration. + * + * @default - don't use a VPC link + */ + // TODO: readonly connectionId?: string; + + /** + * Specifies the format of the payload sent to an integration.. + * + * @default '1.0' + */ + readonly payloadFormatVersion?: string; + + /** + * The TlsConfig property specifies the TLS configuration for a private integration. + * If you specify a TLS configuration, private integration traffic uses the HTTPS protocol. + * + * @default - no private TLS configuration + */ + readonly tlsConfig?: TlsConfig; + + /** + * Specifies the integration's HTTP method type. + * + * @default - 'ANY' + */ + readonly integrationMethod?: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + * + * This interface is used by the helper methods in `Api` and the sub-classes + */ +export interface WebSocketApiIntegrationOptions extends BaseIntegrationOptions { + /** + * Specifies how to handle response payload content type conversions. + * + * @default - Pass through unmodified + */ + readonly contentHandlingStrategy?: ContentHandlingStrategy | string; + + /** + * Specifies the pass-through behavior for incoming requests based on the `Content-Type` header in the request, + * and the available mapping templates specified as the `requestTemplates` property on the `Integration` resource. + * + * @default - the response payload will be passed through from the integration response to the route response or method response unmodified + */ + readonly passthroughBehavior?: PassthroughBehavior | string; + + /** + * A key-value map specifying request parameters that are passed from the method request to the backend. + * The key is an integration request parameter name and the associated value is a method request parameter value or static value + * that must be enclosed within single quotes and pre-encoded as required by the backend. + * + * The method request parameter value must match the pattern of `method.request.{location}.{name}`, where `{location}` is + * `querystring`, `path`, or `header`; and `{name}` must be a valid and unique method request parameter name. + * + * @default - no parameter used + */ + readonly requestParameters?: { [key: string]: string }; + + /** + * Represents a map of Velocity templates that are applied on the request payload based on the value of + * the `Content-Type` header sent by the client. The content type value is the key in this map, and the + * template is the value. + * + * @default - no templates used + */ + readonly requestTemplates?: { [key: string]: string }; + + /** + * The template selection expression for the integration. * * @default - no template selected */ readonly templateSelectionExpression?: KnownTemplateKey | string; +} +/** + * Defines the properties required for defining an Api Gateway V2 Integration. + * + * This interface is used by the helper methods in `Api` and the sub-classes + */ +export interface HttpApiIntegrationOptions extends BaseIntegrationOptions { /** - * Custom timeout between 50 and 29,000 milliseconds for WebSocket APIs and between 50 and 30,000 milliseconds for HTTP APIs. + * The ID of the VPC link for a private integration. * - * @default - timeout is 29 seconds for WebSocket APIs and 30 seconds for HTTP APIs. + * @default - don't use a VPC link */ - readonly timeout?: Duration; + // TODO: readonly connectionId?: string; /** - * Specifies the integration's HTTP method type. + * Specifies the format of the payload sent to an integration.. + * + * @default '1.0' + */ + readonly payloadFormatVersion?: string; + + /** + * The TlsConfig property specifies the TLS configuration for a private integration. + * If you specify a TLS configuration, private integration traffic uses the HTTPS protocol. * - * @default - 'GET' + * @default - no private TLS configuration */ - readonly integrationMethod?: IntegrationMethod | string; + readonly tlsConfig?: TlsConfig; } /** @@ -302,7 +409,22 @@ export interface IntegrationProps extends IntegrationOptions { * * Use `addResponse` and `addRoute` to configure integration. */ -export abstract class Integration extends Resource implements IIntegration { +export class Integration extends Resource implements IIntegration { + /** + * Creates a new imported API Integration + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param integrationId Identifier of the API + */ + public static fromIntegrationId(scope: Construct, id: string, integrationId: string): IIntegration { + class Import extends Resource implements IIntegration { + public readonly integrationId = integrationId; + } + + return new Import(scope, id); + } + /** * The ID of this API Gateway Integration. */ @@ -315,38 +437,25 @@ export abstract class Integration extends Resource implements IIntegration { super(scope, id); this.api = props.api; this.resource = new CfnIntegration(this, 'Resource', { - ...props, - timeoutInMillis: (props.timeout ? props.timeout.toMilliseconds() : undefined), - apiId: props.api.apiId, integrationType: props.type, integrationUri: props.uri, + // TODO: connectionId : props.connectionId, + connectionType: props.connectionType, + contentHandlingStrategy: props.contentHandlingStrategy, + credentialsArn: props.credentialsArn, + description: props.description, + integrationMethod: props.integrationMethod, + passthroughBehavior: props.passthroughBehavior, + payloadFormatVersion: props.payloadFormatVersion, + requestParameters: props.requestParameters, + requestTemplates: props.requestTemplates, + templateSelectionExpression: props.templateSelectionExpression, + tlsConfig: props.tlsConfig, + timeoutInMillis: (props.timeout ? props.timeout.toMilliseconds() : undefined), + apiId: props.api.apiId, }); this.integrationId = this.resource.ref; - - if (props.api instanceof Api) { - if (props.api.latestDeployment) { - props.api.latestDeployment.addToLogicalId({ - ...props, - api: props.api.apiId, - id, - integrationType: props.type, - integrationUri: props.uri, - }); - props.api.latestDeployment.registerDependency(this.resource); - } - } - } - - /** - * Adds a set of permission for a defined route. - * - * This is done automatically for routes created with the helper methods - * - * @param _route the route to define for the permissions - */ - public addPermissionsForRoute(_route: IRoute) { - // Override to define permissions for this integration } /** @@ -363,23 +472,4 @@ export abstract class Integration extends Resource implements IIntegration { key, }); } - - /** - * Creates a new route for this integration. - * - * @param key the route key (predefined or not) that will select this integration - * @param props the properties for this response - */ - public addRoute(key: KnownRouteKey | string, props?: RouteOptions): Route { - const route = new Route(this, `Route.${key}`, { - ...props, - api: this.api, - integration: this, - key, - }); - - this.addPermissionsForRoute(route); - - return route; - } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts new file mode 100644 index 0000000000000..fc6876954633d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts @@ -0,0 +1,100 @@ +import { Construct } from '@aws-cdk/core'; + +import { IApi } from '../api'; +import { HttpApiIntegrationMethod, HttpApiIntegrationOptions, Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; +import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface BaseHttpIntegrationOptions { + /** + * The HTTP URL for this integration + */ + readonly url: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface HttpIntegrationOptions extends IntegrationOptions, BaseHttpIntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + */ +export interface HttpIntegrationProps extends HttpIntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; + + /** + * Defines the api for this integration. + */ + readonly api: IApi; +} + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface HttpApiHttpIntegrationOptions extends HttpApiIntegrationOptions, BaseHttpIntegrationOptions { + /** + * Specifies the integration's HTTP method type. + * + * @default - 'ANY' + */ + readonly integrationMethod?: HttpApiIntegrationMethod | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketApiHttpIntegrationOptions extends WebSocketApiIntegrationOptions, BaseHttpIntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class HttpIntegration extends Construct { + /** + * L1 Integration construct + */ + public readonly resource: Integration; + + constructor(scope: Construct, id: string, props: HttpIntegrationProps) { + super(scope, id); + + this.resource = new Integration(this, 'Default', { + ...props, + type: props.proxy ? IntegrationType.HTTP_PROXY : IntegrationType.HTTP, + uri: props.url, + }); + } + + /** + * Creates a new response for this integration. + * + * @param key the key (predefined or not) that will select this response + * @param props the properties for this response + */ + public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { + return this.resource.addResponse(key, props); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts index fa6637730bce9..9338f63c873a8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts @@ -1 +1,4 @@ +export * from './http-integration'; export * from './lambda-integration'; +export * from './mock-integration'; +export * from './service-integration'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts index 4b548bdacc2c5..b4643d7b6c1f8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts @@ -3,15 +3,33 @@ import { IFunction } from '@aws-cdk/aws-lambda'; import { Construct, Stack } from '@aws-cdk/core'; import { Api, IApi } from '../api'; -import { Integration, IntegrationOptions, IntegrationType } from '../integration'; -import { IRoute } from '../route'; +import { HttpApiIntegrationOptions, Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; +import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; /** * Defines the properties required for defining an Api Gateway V2 Lambda Integration. * * This interface is used by the helper methods in `Integration` */ -export interface LambdaIntegrationOptions extends IntegrationOptions { +export interface BaseLambdaIntegrationOptions { + /** + * The Lambda function handler for this integration + */ + readonly handler: IFunction; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface LambdaIntegrationOptions extends IntegrationOptions, BaseLambdaIntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + */ +export interface LambdaIntegrationProps extends LambdaIntegrationOptions { /** * Defines if this integration is a proxy integration or not. * @@ -20,49 +38,72 @@ export interface LambdaIntegrationOptions extends IntegrationOptions { readonly proxy?: boolean; /** - * The Lambda function handler for this integration + * Defines the api for this integration. */ - readonly handler: IFunction; + readonly api: IApi; } /** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * Defines the properties required for defining an Api Gateway V2 Http API Lambda Integration. + * + * This interface is used by the helper methods in `Integration` */ -export interface LambdaIntegrationProps extends LambdaIntegrationOptions { +export interface HttpApiLambdaIntegrationOptions extends HttpApiIntegrationOptions, BaseLambdaIntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 WebSocket API Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketApiLambdaIntegrationOptions extends WebSocketApiIntegrationOptions, BaseLambdaIntegrationOptions { /** - * Defines the api for this integration. + * Defines if this integration is a proxy integration or not. + * + * @default false */ - readonly api: IApi; + readonly proxy?: boolean; } /** * An AWS Lambda integration for an API in Amazon API Gateway v2. */ -export class LambdaIntegration extends Integration { - protected handler: IFunction; +export class LambdaIntegration extends Construct { + /** + * L1 Integration construct + */ + public readonly resource: Integration; constructor(scope: Construct, id: string, props: LambdaIntegrationProps) { + super(scope, id); + const stack = Stack.of(scope); // This is not a standard ARN as it does not have the account-id part in it const uri = `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`; - super(scope, id, { + this.resource = new Integration(this, 'Default', { ...props, type: props.proxy ? IntegrationType.AWS_PROXY : IntegrationType.AWS, + integrationMethod: 'POST', uri, }); - this.handler = props.handler; - } - public addPermissionsForRoute(route: IRoute) { - if (this.api instanceof Api) { - const sourceArn = this.api.executeApiArn(route); - this.handler.addPermission(`ApiPermission.${route.node.uniqueId}`, { + if (props.api instanceof Api) { + const sourceArn = props.api.executeApiArn(); + props.handler.addPermission(`ApiPermission.${this.node.uniqueId}`, { principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn, }); - } else { - throw new Error('This function is only supported on non-imported APIs'); } } -} + + /** + * Creates a new response for this integration. + * + * @param key the key (predefined or not) that will select this response + * @param props the properties for this response + */ + public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { + return this.resource.addResponse(key, props); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts new file mode 100644 index 0000000000000..0babb1995c562 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts @@ -0,0 +1,68 @@ +import { Construct } from '@aws-cdk/core'; + +import { IApi } from '../api'; +import { Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; +import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; + +/** + * Defines the properties required for defining an Api Gateway V2 Mock Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface BaseMockIntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Mock Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface MockIntegrationOptions extends IntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Mock Integration. + */ +export interface MockIntegrationProps extends MockIntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IApi; +} + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketApiMockIntegrationOptions extends WebSocketApiIntegrationOptions, BaseMockIntegrationOptions { +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class MockIntegration extends Construct { + /** + * L1 Integration construct + */ + public readonly resource: Integration; + + constructor(scope: Construct, id: string, props: MockIntegrationProps) { + super(scope, id); + this.resource = new Integration(this, 'Default', { + ...props, + type: IntegrationType.MOCK, + uri: '', + }); + } + + /** + * Creates a new response for this integration. + * + * @param key the key (predefined or not) that will select this response + * @param props the properties for this response + */ + public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { + return this.resource.addResponse(key, props); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts new file mode 100644 index 0000000000000..965ddb64b9f79 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts @@ -0,0 +1,99 @@ +import { Construct } from '@aws-cdk/core'; + +import { IApi } from '../api'; +import { HttpApiIntegrationMethod, HttpApiIntegrationOptions, Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; +import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface BaseServiceIntegrationOptions { + /** + * The ARN of the target service for this integration + */ + readonly arn: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface ServiceIntegrationOptions extends IntegrationOptions, BaseServiceIntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + */ +export interface ServiceIntegrationProps extends ServiceIntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; + + /** + * Defines the api for this integration. + */ + readonly api: IApi; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface HttpApiServiceIntegrationOptions extends HttpApiIntegrationOptions, BaseServiceIntegrationOptions { + /** + * Specifies the integration's HTTP method type. + * + * @default - 'ANY' + */ + readonly integrationMethod?: HttpApiIntegrationMethod | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketApiServiceIntegrationOptions extends WebSocketApiIntegrationOptions, BaseServiceIntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class ServiceIntegration extends Construct { + /** + * L1 Integration construct + */ + public readonly resource: Integration; + + constructor(scope: Construct, id: string, props: ServiceIntegrationProps) { + super(scope, id); + this.resource = new Integration(this, 'Default', { + ...props, + type: props.proxy ? IntegrationType.AWS_PROXY : IntegrationType.AWS, + uri: props.arn, + }); + } + + /** + * Creates a new response for this integration. + * + * @param key the key (predefined or not) that will select this response + * @param props the properties for this response + */ + public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { + return this.resource.addResponse(key, props); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts index 4a010de899349..615032523c4c7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts @@ -1,6 +1,6 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { Api, IApi } from './api'; +import { IApi } from './api'; import { CfnModel } from './apigatewayv2.generated'; import { JsonSchema, JsonSchemaMapper } from './json-schema'; @@ -151,26 +151,11 @@ export class Model extends Resource implements IModel { this.modelName = this.physicalName; this.resource = new CfnModel(this, 'Resource', { - ...props, contentType: props.contentType || KnownContentTypes.JSON, apiId: props.api.apiId, name: this.modelName, schema: JsonSchemaMapper.toCfnJsonSchema(props.schema), }); this.modelId = this.resource.ref; - - if (props.api instanceof Api) { - if (props.api.latestDeployment) { - props.api.latestDeployment.addToLogicalId({ - ...props, - id, - api: props.api.apiId, - contentType: props.contentType, - name: this.modelName, - schema: props.schema, - }); - props.api.latestDeployment.registerDependency(this.resource); - } - } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts index 9f0d490c5c025..5eb7155938127 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts @@ -1,6 +1,6 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { Api, IApi } from './api'; +import { IApi } from './api'; import { CfnRouteResponse } from './apigatewayv2.generated'; import { IModel, KnownModelKey } from './model'; import { IRoute } from './route'; @@ -100,26 +100,14 @@ export class RouteResponse extends Resource implements IRouteResponse { return ({ [e[0]]: (typeof(e[1]) === 'string' ? e[1] : e[1].modelName) }); })); } + this.resource = new CfnRouteResponse(this, 'Resource', { - ...props, apiId: props.api.apiId, routeId: props.route.routeId, routeResponseKey: props.key, responseModels, + modelSelectionExpression: props.modelSelectionExpression, + responseParameters: props.responseParameters, }); - - if (props.api instanceof Api) { - if (props.api.latestDeployment) { - props.api.latestDeployment.addToLogicalId({ - ...props, - id, - api: props.api.apiId, - route: props.route.routeId, - routeResponseKey: props.key, - responseModels, - }); - props.api.latestDeployment.registerDependency(this.resource); - } - } } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts index 253880aacf385..69b4129382e25 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts @@ -1,16 +1,30 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { Api, IApi } from './api'; +import { IApi } from './api'; import { CfnRoute } from './apigatewayv2.generated'; import { IAuthorizer } from './authorizer'; import { IIntegration } from './integration'; import { IModel, KnownModelKey } from './model'; import { KnownRouteResponseKey, RouteResponse, RouteResponseOptions } from './route-response'; +/** + * Available authorization providers for ApiGateway V2 HTTP APIs + */ +export enum HttpApiAuthorizationType { + /** + * Open access (Web Socket, HTTP APIs). + */ + NONE = 'NONE', + /** + * Use JSON Web Tokens (HTTP APIs). + */ + JWT = 'JWT' +} + /** * Available authorization providers for ApiGateway V2 APIs */ -export enum AuthorizationType { +export enum WebSocketApiAuthorizationType { /** * Open access (Web Socket, HTTP APIs). */ @@ -26,11 +40,7 @@ export enum AuthorizationType { /** * Use an AWS Cognito user pool (Web Socket APIs). */ - COGNITO = 'COGNITO_USER_POOLS', - /** - * Use JSON Web Tokens (HTTP APIs). - */ - JWT = 'JWT' + COGNITO = 'COGNITO_USER_POOLS' } /** @@ -99,7 +109,7 @@ export interface IRoute extends IResource { * The key of this API Gateway Route. * @attribute */ - readonly key: string; + readonly key: KnownRouteKey | string; } /** @@ -107,59 +117,109 @@ export interface IRoute extends IResource { * * This interface is used by the helper methods in `Api` */ -export interface RouteOptions { +export interface BaseRouteOptions { /** - * Specifies whether an API key is required for the route. + * The authorization scopes supported by this route. * - * Supported only for WebSocket APIs. + * @default - no authorization scope + */ + readonly authorizationScopes?: string[]; + + /** + * The Authorizer resource to be associated with this route. * - * @default false + * The authorizer identifier is generated by API Gateway when you created the authorizer. + * + * @default - no authorizer */ - readonly apiKeyRequired?: boolean; + readonly authorizer?: IAuthorizer; /** - * The authorization scopes supported by this route. + * The operation name for the route. * - * @default - no authorization scope + * @default - no operation name */ - readonly authorizationScopes?: string[]; + readonly operationName?: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Route. + * + * This interface is used by the helper methods in `Api` + */ +export interface RouteOptions extends BaseRouteOptions { + /** + * Specifies whether an API key is required for the route. + * + * @default false + */ + readonly apiKeyRequired?: boolean; /** * The authorization type for the route. * * @default 'NONE' */ - readonly authorizationType?: AuthorizationType | string; + readonly authorizationType?: HttpApiAuthorizationType | WebSocketApiAuthorizationType; /** - * The identifier of the Authorizer resource to be associated with this route. + * The model selection expression for the route. * - * The authorizer identifier is generated by API Gateway when you created the authorizer. + * @default - no selection key + */ + readonly modelSelectionExpression?: KnownModelKey | string; + + /** + * The request models for the route. * - * @default - no authorizer + * @default - no models (for example passthrough) + */ + readonly requestModels?: { [key: string]: IModel | string }; + + /** + * The request parameters for the route. + * + * @default - no parameters */ - readonly authorizerId?: IAuthorizer; + readonly requestParameters?: { [key: string]: boolean }; /** - * The model selection expression for the route. + * The route response selection expression for the route. * - * Supported only for WebSocket APIs. + * @default - no selection expression + */ + readonly routeResponseSelectionExpression?: KnownRouteResponseKey | string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 WebSocket Route. + * + * This interface is used by the helper methods in `Api` + */ +export interface WebSocketApiRouteOptions extends BaseRouteOptions { + /** + * Specifies whether an API key is required for the route. * - * @default - no selection key + * @default false */ - readonly modelSelectionExpression?: KnownModelKey | string; + readonly apiKeyRequired?: boolean; /** - * The operation name for the route. + * The authorization type for the route. * - * @default - no operation name + * @default 'NONE' */ - readonly operationName?: string; + readonly authorizationType?: WebSocketApiAuthorizationType; /** - * The request models for the route. + * The model selection expression for the route. * - * Supported only for WebSocket APIs. + * @default - no selection key + */ + readonly modelSelectionExpression?: KnownModelKey | string; + + /** + * The request models for the route. * * @default - no models (for example passthrough) */ @@ -168,8 +228,6 @@ export interface RouteOptions { /** * The request parameters for the route. * - * Supported only for WebSocket APIs. - * * @default - no parameters */ readonly requestParameters?: { [key: string]: boolean }; @@ -177,13 +235,25 @@ export interface RouteOptions { /** * The route response selection expression for the route. * - * Supported only for WebSocket APIs. - * * @default - no selection expression */ readonly routeResponseSelectionExpression?: KnownRouteResponseKey | string; } +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Api Route. + * + * This interface is used by the helper methods in `Api` + */ +export interface HttpApiRouteOptions extends BaseRouteOptions { + /** + * The authorization type for the route. + * + * @default 'NONE' + */ + readonly authorizationType?: HttpApiAuthorizationType; +} + /** * Defines the properties required for defining an Api Gateway V2 Route. */ @@ -244,11 +314,9 @@ export class Route extends Resource implements IRoute { this.api = props.api; this.key = props.key; - let authorizerId: string | undefined; - if (props.authorizerId !== undefined) { - authorizerId = props.authorizerId.authorizerId; - } + const authorizerId: string | undefined = props.authorizer?.authorizerId; let requestModels: { [key: string]: string } | undefined; + if (props.requestModels !== undefined) { requestModels = Object.assign({}, ...Object.entries(props.requestModels).map((e) => { return ({ [e[0]]: (typeof(e[1]) === 'string' ? e[1] : e[1].modelName) }); @@ -256,29 +324,20 @@ export class Route extends Resource implements IRoute { } this.resource = new CfnRoute(this, 'Resource', { - ...props, - apiKeyRequired: props.apiKeyRequired, apiId: this.api.apiId, routeKey: props.key, target: `integrations/${props.integration.integrationId}`, requestModels, authorizerId, + apiKeyRequired: props.apiKeyRequired, + authorizationScopes: props.authorizationScopes, + authorizationType: props.authorizationType, + modelSelectionExpression: props.modelSelectionExpression, + operationName: props.operationName, + requestParameters: props.requestParameters, + routeResponseSelectionExpression: props.routeResponseSelectionExpression, }); this.routeId = this.resource.ref; - - if (props.api instanceof Api) { - if (props.api.latestDeployment) { - props.api.latestDeployment.addToLogicalId({ - ...props, - id, - routeKey: this.key, - target: `integrations/${props.integration.integrationId}`, - requestModels, - authorizerId, - }); - props.api.latestDeployment.registerDependency(this.resource); - } - } } /** diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts index bf9550d4952ef..6f6942dbaa366 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts @@ -2,7 +2,7 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; import { IApi } from './api'; import { CfnStage } from './apigatewayv2.generated'; -import { Deployment, IDeployment } from './deployment'; +import { IDeployment } from './deployment'; /** * Specifies the logging level for this route. This property affects the log entries pushed to Amazon CloudWatch Logs. @@ -211,19 +211,19 @@ export class Stage extends Resource implements IStage { super(scope, id); this.resource = new CfnStage(this, 'Resource', { - ...props, apiId: props.api.apiId, deploymentId: props.deployment.deploymentId, + stageName: props.stageName, + accessLogSettings: props.accessLogSettings, + autoDeploy: props.autoDeploy, + clientCertificateId: props.clientCertificateId, + defaultRouteSettings: props.defaultRouteSettings, + description: props.description, + routeSettings: props.routeSettings, + stageVariables: props.stageVariables, + // TODO: tags: props.tags, }); - this.stageName = this.resource.ref; - if (props.deployment instanceof Deployment) { - props.deployment.addToLogicalId({ - ...props, - api: props.api.apiId, - deployment: props.deployment.deploymentId, - id, - }); - } + this.stageName = this.resource.ref; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts new file mode 100644 index 0000000000000..8f5c69a99d4cd --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts @@ -0,0 +1,380 @@ +import { IFunction } from '@aws-cdk/aws-lambda'; +import { CfnResource, Construct, IConstruct } from '@aws-cdk/core'; + +import { Api, KnownApiKeySelectionExpression, ProtocolType } from './api'; +import { Integration } from './integration'; +import { HttpIntegration, WebSocketApiHttpIntegrationOptions } from './integrations/http-integration'; +import { LambdaIntegration, WebSocketApiLambdaIntegrationOptions } from './integrations/lambda-integration'; +import { MockIntegration, WebSocketApiMockIntegrationOptions } from './integrations/mock-integration'; +import { ServiceIntegration, WebSocketApiServiceIntegrationOptions } from './integrations/service-integration'; +import { JsonSchema } from './json-schema'; +import { Model, ModelOptions } from './model'; +import { IRoute, KnownRouteKey, KnownRouteSelectionExpression, Route, WebSocketApiRouteOptions } from './route'; +import { IStage, StageOptions } from './stage'; + +/** + * Defines a default handler for the Api + * + * One of the properties need to be defined + */ +export interface WebSocketApiDefaultTarget { + /** + * Use an AWS Lambda function + * + * If this is not defined, `uri` has to be defined + * + * @default - use one of the other properties + */ + readonly handler?: IFunction; + + /** + * Use a URI for the handler. + * If a string is provided, it will test is the string + * - starts with 'http://' or 'https://': it creates an http + * - starts with 'arn:': it will create an AWS Serice integration + * - equals 'mock': it will create a Mock Serice integration + * - it will fail for any other value + * + * If this is not defined, `handler` has to be defined + * + * @default - Use inline definition + */ + readonly uri?: string; +} + +/** + * Defines the contract for an Api Gateway V2 HTTP Api. + */ +export interface IWebSocketApi extends IConstruct { + /** + * The ID of this API Gateway Api. + * @attribute + */ + readonly apiId: string; +} + +/** + * Defines the properties of a Web Socket API + */ +export interface WebSocketApiProps { + /** + * Indicates if a Deployment should be automatically created for this API, + * and recreated when the API model (route, integration) changes. + * + * Since API Gateway deployments are immutable, When this option is enabled + * (by default), an AWS::ApiGatewayV2::Deployment resource will automatically + * created with a logical ID that hashes the API model (methods, resources + * and options). This means that when the model changes, the logical ID of + * this CloudFormation resource will change, and a new deployment will be + * created. + * + * If this is set, `latestDeployment` will refer to the `Deployment` object + * and `deploymentStage` will refer to a `Stage` that points to this + * deployment. To customize the stage options, use the `deployOptions` + * property. + * + * A CloudFormation Output will also be defined with the root URL endpoint + * of this REST API. + * + * @default true + */ + readonly deploy?: boolean; + + /** + * Options for the API Gateway stage that will always point to the latest + * deployment when `deploy` is enabled. If `deploy` is disabled, + * this value cannot be set. + * + * @default - default options + */ + readonly deployOptions?: StageOptions; + + /** + * Retains old deployment resources when the API changes. This allows + * manually reverting stages to point to old deployments via the AWS + * Console. + * + * @default false + */ + readonly retainDeployments?: boolean; + + /** + * A name for the API Gateway Api resource. + * + * @default - ID of the Api construct. + */ + readonly apiName?: string; + + /** + * Expression used to select the route for this API + */ + readonly routeSelectionExpression: KnownRouteSelectionExpression | string; + + /** + * Expression used to select the Api Key to use for metering + * + * @default - No Api Key + */ + readonly apiKeySelectionExpression?: KnownApiKeySelectionExpression | string; + + /** + * A description of the purpose of this API Gateway Api resource. + * + * @default - No description. + */ + readonly description?: string; + + /** + * Indicates whether schema validation will be disabled for this Api + * + * @default false + */ + readonly disableSchemaValidation?: boolean; + + /** + * Indicates the version number for this Api + * + * @default false + */ + readonly version?: string; + + /** + * Specifies whether to rollback the API creation (`true`) or not (`false`) when a warning is encountered + * + * @default false + */ + readonly failOnWarnings?: boolean; + + /** + * If defined, creates a default proxy target for the HTTP Api. + * + * @default - no default handler or route + */ + readonly defaultTarget?: WebSocketApiDefaultTarget; +} + +export declare type WebSocketRouteName = KnownRouteKey | string; + +/** + * Represents an HTTP API in Amazon API Gateway v2. + * + * Use `addModel` and `addLambdaIntegration` to configure the API model. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + */ +export class WebSocketApi extends Construct implements IWebSocketApi { + /** + * Creates a new imported API + * + * @param scope scope of this imported resource + * @param id identifier of the resource + * @param apiId Identifier of the API + */ + public static fromApiId(scope: Construct, id: string, apiId: string): IWebSocketApi { + class Import extends Construct implements IWebSocketApi { + public readonly apiId = apiId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway Api. + */ + public readonly apiId: string; + + protected readonly resource: Api; + + constructor(scope: Construct, id: string, props: WebSocketApiProps) { + super(scope, id); + + this.resource = new Api(this, 'Default', { + ...props, + apiName: props.apiName ?? id, + protocolType: ProtocolType.WEBSOCKET, + }); + + this.apiId = this.resource.apiId; + + if (props.defaultTarget !== undefined) { + let integration; + if (props.defaultTarget.handler !== undefined) { + integration = this.addLambdaIntegration('default', { + handler: props.defaultTarget.handler, + proxy: true, + }); + } else if (props.defaultTarget.uri) { + const uri = props.defaultTarget.uri; + if (uri.startsWith('https://') || uri.startsWith('http://')) { + integration = this.addHttpIntegration('default', { + url: uri, + proxy: true, + }); + } else if (uri.startsWith('arn:')) { + integration = this.addServiceIntegration('default', { + arn: uri, + proxy: true, + }); + } else if (uri === 'MOCK') { + integration = this.addMockIntegration('default', {}); + } else { + throw new Error('Invalid string format, must be a fully qualified ARN, a URL, or "MOCK"'); + } + } else { + throw new Error('You must specify an ARN, a URL, "MOCK", or a Lambda Function'); + } + + this.addRoute(KnownRouteKey.DEFAULT, integration, {}); + } + } + + /** + * Creates a new integration for this api, using a Lambda integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addLambdaIntegration(id: string, props: WebSocketApiLambdaIntegrationOptions): LambdaIntegration { + const integration = new LambdaIntegration(this, `${id}.lambda.integration`, { + ...props, + api: this.resource, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new integration for this api, using a HTTP integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addHttpIntegration(id: string, props: WebSocketApiHttpIntegrationOptions): HttpIntegration { + const integration = new HttpIntegration(this, `${id}.http.integration`, { + ...props, + api: this.resource, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new service for this api, using a Lambda integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addServiceIntegration(id: string, props: WebSocketApiServiceIntegrationOptions): ServiceIntegration { + const integration = new ServiceIntegration(this, `${id}.service.integration`, { + ...props, + api: this.resource, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new service for this api, using a Mock integration. + * + * @param id the id of this integration + * @param props the properties for this integration + */ + public addMockIntegration(id: string, props: WebSocketApiMockIntegrationOptions): MockIntegration { + const integration = new MockIntegration(this, `${id}.service.integration`, { + ...props, + api: this.resource, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + } + return integration; + } + + /** + * Creates a new route for this api, on the specified methods. + * + * @param key the route key (predefined or not) to use + * @param integration [disable-awslint:ref-via-interface] the integration to use for this route + * @param props the properties for this route + */ + public addRoute( + key: WebSocketRouteName, + integration: Integration | LambdaIntegration | HttpIntegration | ServiceIntegration, + props?: WebSocketApiRouteOptions): Route { + const route = new Route(this, `${key}.route`, { + ...props, + api: this.resource, + integration: ((integration instanceof Integration) ? integration : integration.resource), + key, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(route.node.defaultChild as CfnResource); + } + return route; + } + + /** + * Defines a model for this Api Gateway. + * @param schema The model schema + * @param props The model integration options + */ + public addModel(schema: JsonSchema, props?: ModelOptions): Model { + const model = new Model(this, `Model.${schema.title}`, { + ...props, + modelName: schema.title, + api: this.resource, + schema, + }); + if (this.resource.deployment !== undefined) { + this.resource.deployment.registerDependency(model.node.defaultChild as CfnResource); + } + return model; + } + + /** + * Returns the ARN for a specific route and stage. + * + * @param route The route for this ARN ('*' if not defined) + * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') + */ + public executeApiArn(route?: IRoute, stage?: IStage) { + return this.resource.executeApiArn(route, stage); + } + + /** + * Returns the ARN for a specific connection. + * + * @param connectionId The identifier of this connection ('*' if not defined) + * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') + */ + public connectionsApiArn(connectionId: string = '*', stage?: IStage) { + return this.resource.webSocketConnectionsApiArn(connectionId, stage); + } + + /** + * Returns the connections URL for this Api, for a specific stage. + * + * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. + * @param stage The stage for this URL (if not defined, defaults to the deployment stage) + */ + public connectionsUrl(stage?: IStage): string { + return this.resource.webSocketConnectionsUrl(stage); + } + + /** + * Returns the client URL for this Api, for a specific stage. + * + * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. + * @param stage The stage for this URL (if not defined, defaults to the deployment stage) + */ + public clientUrl(stage?: IStage): string { + return this.resource.webSocketClientUrl(stage); + } +} \ 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 c56ab71f5337a..024cd9bd0f618 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -82,6 +82,7 @@ "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" }, @@ -102,8 +103,12 @@ }, "awslint": { "exclude": [ + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.MockIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.ServiceIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.ApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.DeploymentProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.IntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.IntegrationResponseProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.LambdaIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.RouteProps", @@ -111,6 +116,7 @@ "no-unused-type:@aws-cdk/aws-apigatewayv2.AuthorizationType", "no-unused-type:@aws-cdk/aws-apigatewayv2.ConnectionType", "no-unused-type:@aws-cdk/aws-apigatewayv2.ContentHandlingStrategy", + "no-unused-type:@aws-cdk/aws-apigatewayv2.HttpApiIntegrationMethod", "no-unused-type:@aws-cdk/aws-apigatewayv2.IntegrationMethod", "no-unused-type:@aws-cdk/aws-apigatewayv2.IntegrationType", "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownContentTypes", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts index 94f1b762a13ca..52304e01460db 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts @@ -10,18 +10,17 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); + const api = new apigw.HttpApi(stack, 'my-api'); const domainName = new apigw.DomainName(stack, 'domain-name', { domainName: 'test.example.com' }); - new apigw.ApiMapping(stack, 'mapping', { + api.addApiMapping({ stage: api.deploymentStage!, domainName, - api, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::ApiMapping', { ApiId: { Ref: 'myapi4C7BF186' }, - Stage: { Ref: 'myapiStageprod07E02E1F' }, + Stage: { Ref: 'myapiDefaultStage51F6D7C3' }, DomainName: { Ref: 'domainname1131E743' }, })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts index ac0287f8e9bf7..1d29054ef2460 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts @@ -5,20 +5,19 @@ import * as apigw from '../lib'; // tslint:disable:max-line-length -test('minimal setup', () => { +test('minimal setup (websocket)', () => { // GIVEN const stack = new Stack(); // WHEN - new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { Name: 'my-api', - ProtocolType: apigw.ProtocolType.WEBSOCKET, + ProtocolType: 'WEBSOCKET', RouteSelectionExpression: '${context.routeKey}', })); @@ -29,17 +28,42 @@ test('minimal setup', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Stage', { ApiId: { Ref: 'myapi4C7BF186' }, StageName: 'prod', - DeploymentId: { Ref: 'myapiDeployment92F2CB492D341D1B' }, + DeploymentId: { Ref: 'myapiDeployment92F2CB49' }, })); }); -test('minimal setup (no deploy)', () => { +test('minimal setup (HTTP)', () => { // GIVEN const stack = new Stack(); // WHEN - new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + new apigw.HttpApi(stack, 'my-api', { + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { + Name: 'my-api', + ProtocolType: 'HTTP', + RouteSelectionExpression: '${request.method} ${request.path}', + })); + + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Deployment', { + ApiId: { Ref: 'myapi4C7BF186' }, + })); + + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Stage', { + ApiId: { Ref: 'myapi4C7BF186' }, + StageName: 'prod', + DeploymentId: { Ref: 'myapiDeployment92F2CB49' }, + })); +}); + +test('minimal setup (WebSocket, no deploy)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); @@ -53,14 +77,31 @@ test('minimal setup (no deploy)', () => { cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Stage')); }); +test('minimal setup (HTTP, no deploy)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new apigw.HttpApi(stack, 'my-api', { + deploy: false, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { + Name: 'my-api', + })); + + cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Deployment')); + cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Stage')); +}); + test('minimal setup (no deploy, error)', () => { // GIVEN const stack = new Stack(); // WHEN expect(() => { - return new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + return new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, deployOptions: { @@ -70,12 +111,12 @@ test('minimal setup (no deploy, error)', () => { }).toThrow(); }); -test('URLs and ARNs', () => { +test('URLs and ARNs (WebSocket)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { protocolType: apigw.ProtocolType.WEBSOCKET, routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { key: 'routeKey', @@ -83,30 +124,67 @@ test('URLs and ARNs', () => { }); // THEN - expect(stack.resolve(api.clientUrl())).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiStageprod07E02E1F' } ] ] }); + expect(stack.resolve(api.clientUrl())).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiDefaultStage51F6D7C3' } ] ] }); expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); - expect(stack.resolve(api.connectionsUrl())).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiStageprod07E02E1F' }, '/@connections' ] ] }); + expect(stack.resolve(api.connectionsUrl())).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/@connections' ] ] }); expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev/@connections' ] ] }); - expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/*' ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/*' ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/*' ] ] }); expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); - expect(stack.resolve(api.connectionsApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/POST/*' ] ] }); - expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiStageprod07E02E1F' }, '/POST/my-connection' ] ] }); + expect(stack.resolve(api.connectionsApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/POST/*' ] ] }); + expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/POST/my-connection' ] ] }); expect(stack.resolve(api.connectionsApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/POST/*' ] ] }); expect(stack.resolve(api.connectionsApiArn('my-connection', importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/POST/my-connection' ] ] }); }); -test('URLs and ARNs (no deploy)', () => { +test('URLs and ARNs (HTTP)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.HttpApi(stack, 'my-api', {}); + const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); + const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { + key: 'routeKey', + routeId: 'routeId', + }); + + // THEN + expect(stack.resolve(api.clientUrl())).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiDefaultStage51F6D7C3' } ] ] }); + expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); + + expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/*' ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/*' ] ] }); + expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); +}); + +test('URLs and ARNs (HTTP, no deploy)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.HttpApi(stack, 'my-api', { + deploy: false, + }); + const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); + + // THEN + expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); + + expect(() => stack.resolve(api.clientUrl())).toThrow(); +}); + +test('URLs and ARNs (WebSocket, no deploy)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json new file mode 100644 index 0000000000000..e23189fdea64c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json @@ -0,0 +1,425 @@ +{ + "Resources": { + "MyFuncServiceRole54065130": { + "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" + ] + ] + } + ] + } + }, + "MyFunc8A243A2C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "\nimport json\ndef handler(event, context):\n return {\n 'statusCode': 200,\n 'body': json.dumps(event)\n }" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFuncServiceRole54065130", + "Arn" + ] + }, + "Runtime": "python3.7" + }, + "DependsOn": [ + "MyFuncServiceRole54065130" + ] + }, + "MyFuncApiPermissionApiagtewayV2HttpApigetbooksInteglambdaintegration4555698DC6292E59": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "HttpApiF5A9A8A7" + }, + "/", + { + "Ref": "HttpApiDefaultStage3EEB07D6" + }, + "/*" + ] + ] + } + } + }, + "RootFuncServiceRoleE4AA9E41": { + "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" + ] + ] + } + ] + } + }, + "RootFuncF39FB174": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "\nimport json, os\ndef handler(event, context):\n whoami = os.environ['WHOAMI']\n http_path = os.environ['HTTP_PATH']\n return {\n 'statusCode': 200,\n 'body': json.dumps({ 'whoami': whoami, 'http_path': http_path })\n }" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "RootFuncServiceRoleE4AA9E41", + "Arn" + ] + }, + "Runtime": "python3.7", + "Environment": { + "Variables": { + "WHOAMI": "root", + "HTTP_PATH": "/" + } + } + }, + "DependsOn": [ + "RootFuncServiceRoleE4AA9E41" + ] + }, + "RootFuncApiPermissionApiagtewayV2HttpApigetBookReviewInteglambdaintegration0ABD780C9E2600E7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RootFuncF39FB174", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "HttpApiF5A9A8A7" + }, + "/", + { + "Ref": "HttpApiDefaultStage3EEB07D6" + }, + "/*" + ] + ] + } + } + }, + "HttpApiF5A9A8A7": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpApi", + "ProtocolType": "HTTP", + "RouteSelectionExpression": "${request.method} ${request.path}" + } + }, + "HttpApiDeployment2ABEB12F": { + "Type": "AWS::ApiGatewayV2::Deployment", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "Description": "Automatically created by the Api construct" + }, + "DependsOn": [ + "HttpApidefaultroute8BBAF64E", + "HttpApidefaulthttpintegration5F125B3A", + "HttpApiGETrouteF5E07819", + "HttpApiGETbooksreviewsrouteCAD382E0", + "HttpApiGETbooksroute2D32C4F3", + "HttpApigetBookReviewInteglambdaintegrationD5870D18", + "HttpApigetbooksInteglambdaintegrationE71E55E1", + "HttpApiPOSTroute74994293", + "HttpApiRootInteghttpintegration15582736" + ] + }, + "HttpApiDefaultStage3EEB07D6": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "StageName": "prod", + "DeploymentId": { + "Ref": "HttpApiDeployment2ABEB12F" + }, + "Description": "Automatically created by the Api construct" + } + }, + "HttpApidefaulthttpintegration5F125B3A": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "HTTP_PROXY", + "IntegrationMethod": "ANY", + "IntegrationUri": "https://aws.amazon.com", + "PayloadFormatVersion": "1.0" + } + }, + "HttpApidefaultroute8BBAF64E": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApidefaulthttpintegration5F125B3A" + } + ] + ] + } + } + }, + "HttpApiRootInteghttpintegration15582736": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "HTTP_PROXY", + "IntegrationMethod": "ANY", + "IntegrationUri": "https://checkip.amazonaws.com", + "PayloadFormatVersion": "1.0" + } + }, + "HttpApiGETrouteF5E07819": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "GET /", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApiRootInteghttpintegration15582736" + } + ] + ] + } + } + }, + "HttpApiPOSTroute74994293": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "POST /", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApiRootInteghttpintegration15582736" + } + ] + ] + } + } + }, + "HttpApigetbooksInteglambdaintegrationE71E55E1": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationMethod": "POST", + "IntegrationUri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "PayloadFormatVersion": "1.0" + } + }, + "HttpApiGETbooksroute2D32C4F3": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "GET /books", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApigetbooksInteglambdaintegrationE71E55E1" + } + ] + ] + } + } + }, + "HttpApigetBookReviewInteglambdaintegrationD5870D18": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationMethod": "POST", + "IntegrationUri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RootFuncF39FB174", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "PayloadFormatVersion": "1.0" + } + }, + "HttpApiGETbooksreviewsrouteCAD382E0": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "GET /books/reviews", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApigetBookReviewInteglambdaintegrationD5870D18" + } + ] + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts new file mode 100644 index 0000000000000..a2745671df684 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts @@ -0,0 +1,51 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as apigatewayv2 from '../lib'; + +// tslint:disable:max-line-length + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'ApiagtewayV2HttpApi'); + +const getbooksHandler = new lambda.Function(stack, 'MyFunc', { + runtime: lambda.Runtime.PYTHON_3_7, + handler: 'index.handler', + code: new lambda.InlineCode(` +import json +def handler(event, context): + return { + 'statusCode': 200, + 'body': json.dumps(event) + }`), +}); + +const getbookReviewsHandler = new lambda.Function(stack, 'RootFunc', { + runtime: lambda.Runtime.PYTHON_3_7, + handler: 'index.handler', + code: new lambda.InlineCode(` +import json, os +def handler(event, context): + whoami = os.environ['WHOAMI'] + http_path = os.environ['HTTP_PATH'] + return { + 'statusCode': 200, + 'body': json.dumps({ 'whoami': whoami, 'http_path': http_path }) + }`), + environment: { + WHOAMI: 'root', + HTTP_PATH: '/', + }, +}); + +const rootUrl = 'https://checkip.amazonaws.com'; +const defaultUrl = 'https://aws.amazon.com'; + +// create a basic HTTP API with http proxy integration as the $default route +const api = new apigatewayv2.HttpApi(stack, 'HttpApi', { + defaultTarget: { uri: defaultUrl }, +}); + +api.addRoutes(['GET /', 'POST /'], api.addHttpIntegration('RootInteg', { url: rootUrl })); +api.addRoute({ method: apigatewayv2.HttpMethod.GET, path: '/books' }, api.addLambdaIntegration('getbooksInteg', { handler : getbooksHandler })); +api.addRoute('GET /books/reviews', api.addLambdaIntegration('getBookReviewInteg', { handler: getbookReviewsHandler })); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json new file mode 100644 index 0000000000000..1b829c8a9fa82 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json @@ -0,0 +1,246 @@ +{ + "Resources": { + "MyFuncServiceRole54065130": { + "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" + ] + ] + } + ] + } + }, + "MyFunc8A243A2C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "\nimport json\ndef handler(event, context):\n return {\n 'status': 'ok',\n 'message': 'success'\n }" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFuncServiceRole54065130", + "Arn" + ] + }, + "Runtime": "python3.7" + }, + "DependsOn": [ + "MyFuncServiceRole54065130" + ] + }, + "MyFuncApiPermissionApiagtewayV2HttpApiWebSocketApidefaultlambdaintegration33FE346D3DE42898": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "WebSocketApi34BCF99B" + }, + "/", + { + "Ref": "WebSocketApiDefaultStage734E750B" + }, + "/*" + ] + ] + } + } + }, + "WebSocketApi34BCF99B": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "WebSocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "${request.body.action}" + } + }, + "WebSocketApiDeployment507BF677": { + "Type": "AWS::ApiGatewayV2::Deployment", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "Description": "Automatically created by the Api construct" + }, + "DependsOn": [ + "WebSocketApiconnectrouteA20DAA0E", + "WebSocketApidefaultlambdaintegration33A4E43E", + "WebSocketApiModelstatusResponseB7B1089E" + ] + }, + "WebSocketApiDefaultStage734E750B": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "StageName": "integration", + "DeploymentId": { + "Ref": "WebSocketApiDeployment507BF677" + }, + "Description": "Automatically created by the Api construct" + } + }, + "WebSocketApiModelstatusResponseB7B1089E": { + "Type": "AWS::ApiGatewayV2::Model", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "Name": "statusResponse", + "Schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "statusResponse", + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "ContentType": "application/json" + } + }, + "WebSocketApidefaultlambdaintegration33A4E43E": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "IntegrationType": "AWS", + "Description": "WebSocket Api Connection Integration", + "IntegrationMethod": "POST", + "IntegrationUri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "connect": "{ \"action\": \"${context.routeKey}\", \"userId\": \"${context.identity.cognitoIdentityId}\", \"connectionId\": \"${context.connectionId}\", \"domainName\": \"${context.domainName}\", \"stageName\": \"${context.stage}\" }" + }, + "TemplateSelectionExpression": "connect" + } + }, + "WebSocketApidefaultlambdaintegrationResponsedefault8A571455": { + "Type": "AWS::ApiGatewayV2::IntegrationResponse", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "IntegrationId": { + "Ref": "WebSocketApidefaultlambdaintegration33A4E43E" + }, + "IntegrationResponseKey": "$default", + "ResponseTemplates": { + "default": "#set($inputRoot = $input.path('$')) { \"status\": \"${inputRoot.status}\", \"message\": \"$util.escapeJavaScript(${inputRoot.message})\" }" + }, + "TemplateSelectionExpression": "default" + } + }, + "WebSocketApiconnectrouteA20DAA0E": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "RouteKey": "$connect", + "AuthorizationType": "AWS_IAM", + "RouteResponseSelectionExpression": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "WebSocketApidefaultlambdaintegration33A4E43E" + } + ] + ] + } + } + }, + "WebSocketApiconnectrouteResponsedefaultB361BECB": { + "Type": "AWS::ApiGatewayV2::RouteResponse", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "RouteId": { + "Ref": "WebSocketApiconnectrouteA20DAA0E" + }, + "RouteResponseKey": "$default", + "ModelSelectionExpression": "default", + "ResponseModels": { + "default": "statusResponse" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts new file mode 100644 index 0000000000000..9b544c930e270 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts @@ -0,0 +1,60 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as apigatewayv2 from '../lib'; + +// tslint:disable:max-line-length + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'ApiagtewayV2HttpApi'); + +const webSocketHandler = new lambda.Function(stack, 'MyFunc', { + runtime: lambda.Runtime.PYTHON_3_7, + handler: 'index.handler', + code: new lambda.InlineCode(` +import json +def handler(event, context): + return { + 'status': 'ok', + 'message': 'success' + }`), +}); + +// create a Web Socket API +const api = new apigatewayv2.WebSocketApi(stack, 'WebSocketApi', { + routeSelectionExpression: '${request.body.action}', + deployOptions: { + stageName: 'integration', + }, +}); + +const defaultStatusIntegrationResponse: apigatewayv2.IntegrationResponseOptions = { + responseTemplates: { + default: '#set($inputRoot = $input.path(\'$\')) { "status": "${inputRoot.status}", "message": "$util.escapeJavaScript(${inputRoot.message})" }', + }, + templateSelectionExpression: 'default', +}; + +const defaultStatusRouteResponse: apigatewayv2.RouteResponseOptions = { + modelSelectionExpression: 'default', + responseModels: { + default: api.addModel({ schema: apigatewayv2.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigatewayv2.JsonSchemaType.OBJECT, properties: { status: { type: apigatewayv2.JsonSchemaType.STRING }, message: { type: apigatewayv2.JsonSchemaType.STRING } } }), + }, +}; + +const webSocketConnectIntegration = api.addLambdaIntegration('default', { + handler: webSocketHandler, + proxy: false, + passthroughBehavior: apigatewayv2.PassthroughBehavior.NEVER, + requestTemplates: { + connect: '{ "action": "${context.routeKey}", "userId": "${context.identity.cognitoIdentityId}", "connectionId": "${context.connectionId}", "domainName": "${context.domainName}", "stageName": "${context.stage}" }', + }, + templateSelectionExpression: 'connect', + description: 'WebSocket Api Connection Integration', +}); +webSocketConnectIntegration.addResponse(apigatewayv2.KnownRouteResponseKey.DEFAULT, defaultStatusIntegrationResponse); + +api.addRoute(apigatewayv2.KnownRouteKey.CONNECT, webSocketConnectIntegration, { + authorizationType: apigatewayv2.WebSocketApiAuthorizationType.IAM, + routeResponseSelectionExpression: apigatewayv2.KnownRouteResponseKey.DEFAULT, +}).addResponse(apigatewayv2.KnownRouteResponseKey.DEFAULT, defaultStatusRouteResponse); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts index edde8272a2e77..eb952de458d06 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts @@ -6,13 +6,12 @@ import * as apigw from '../lib'; // tslint:disable:max-line-length -test('Lambda integration', () => { +test('Lambda integration (WebSocket)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); @@ -28,39 +27,36 @@ test('Lambda integration', () => { })); }); -test('Lambda integration (with extra params)', () => { +test('Lambda integration (WebSocket, with extra params)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), connectionType: apigw.ConnectionType.INTERNET, - integrationMethod: apigw.IntegrationMethod.GET, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: apigw.IntegrationType.AWS, + IntegrationType: 'AWS', IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, - IntegrationMethod: apigw.IntegrationMethod.GET, - ConnectionType: apigw.ConnectionType.INTERNET, + IntegrationMethod: 'POST', + ConnectionType: 'INTERNET', })); }); -test('Lambda integration (proxy)', () => { +test('Lambda integration (WebSocket, proxy)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); @@ -72,8 +68,101 @@ test('Lambda integration (proxy)', () => { // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: apigw.IntegrationType.AWS_PROXY, + IntegrationType: 'AWS_PROXY', + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, + IntegrationMethod: 'POST', + })); +}); + +test('Lambda integration (HTTP)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.HttpApi(stack, 'my-api', { + deploy: false, + }); + api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationType: 'AWS_PROXY', IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, + IntegrationMethod: 'POST', + })); +}); + +test('Http integration (HTTP)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.HttpApi(stack, 'my-api', { + deploy: false, + }); + api.addHttpIntegration('myFunction', { + url: 'https://aws.amazon.com/', + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'https://aws.amazon.com/', + IntegrationMethod: 'ANY', + })); + + api.addHttpIntegration('myFunction2', { + url: 'https://console.aws.amazon.com/', + integrationMethod: 'POST', + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'https://aws.amazon.com/', + IntegrationMethod: 'ANY', + })); + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'https://console.aws.amazon.com/', + IntegrationMethod: 'POST', + })); +}); + +test('Service integration (HTTP)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.HttpApi(stack, 'my-api', { + deploy: false, + }); + api.addServiceIntegration('myObject', { + arn: stack.formatArn({ service: 's3', account: '', region: '', resource: 'my-bucket', resourceName: 'my-key', sep: '/'}), + }); + api.addServiceIntegration('myOtherObject', { + integrationMethod: 'GET', + arn: stack.formatArn({ service: 's3', account: '', region: '', resource: 'my-bucket', resourceName: 'my-other-key', sep: '/'}), + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationType: 'AWS_PROXY', + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket/my-key']] }, + IntegrationMethod: 'ANY', + })); + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { Ref: 'myapi4C7BF186' }, + IntegrationType: 'AWS_PROXY', + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket/my-other-key']] }, + IntegrationMethod: 'GET', })); }); @@ -82,8 +171,7 @@ test('Integration response', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); @@ -95,7 +183,7 @@ test('Integration response', () => { // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::IntegrationResponse', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationId: { Ref: 'myapimyFunction27BC3796' }, - IntegrationResponseKey: apigw.KnownIntegrationResponseKey.DEFAULT, + IntegrationId: { Ref: 'myapimyFunctionlambdaintegrationB6693307' }, + IntegrationResponseKey: '$default', })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts index 8855b6c3637ea..cb7953744154b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts @@ -11,15 +11,14 @@ test('route', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), }); - integration.addRoute(apigw.KnownRouteKey.CONNECT, { + api.addRoute(apigw.KnownRouteKey.CONNECT, integration, { modelSelectionExpression: apigw.KnownModelKey.DEFAULT, requestModels: { [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusInputModel', type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }), @@ -31,7 +30,7 @@ test('route', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { ApiId: { Ref: 'myapi4C7BF186' }, RouteKey: '$connect', - Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunction27BC3796' } ] ] }, + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, ModelSelectionExpression: '$default', RequestModels: { $default: 'statusInputModel', @@ -45,25 +44,35 @@ test('route', () => { })); }); -test('route (no model)', () => { +test('route (HTTP)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.HTTP, + const api = new apigw.HttpApi(stack, 'my-api', { deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), }); - integration.addRoute('POST /'); + api.addRoute({ method: apigw.HttpMethod.POST, path: '/' }, integration); + api.addRoutes([ { method: apigw.HttpMethod.GET, path: '/' }, 'PUT /' ] , integration); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { ApiId: { Ref: 'myapi4C7BF186' }, RouteKey: 'POST /', - Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunction27BC3796' } ] ] }, + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, + })); + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteKey: 'GET /', + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, + })); + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteKey: 'PUT /', + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, })); }); @@ -72,15 +81,14 @@ test('route response', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), }); - const route = integration.addRoute(apigw.KnownRouteKey.CONNECT, {}); + const route = api.addRoute(apigw.KnownRouteKey.CONNECT, integration, {}); route.addResponse(apigw.KnownRouteKey.CONNECT, { modelSelectionExpression: apigw.KnownModelKey.DEFAULT, responseModels: { @@ -91,7 +99,7 @@ test('route response', () => { // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::RouteResponse', { ApiId: { Ref: 'myapi4C7BF186' }, - RouteId: { Ref: 'myapimyFunctionRouteconnectA2AF3242' }, + RouteId: { Ref: 'myapiconnectrouteC62A8B0B' }, RouteResponseKey: '$connect', ModelSelectionExpression: '$default', ResponseModels: { @@ -101,7 +109,7 @@ test('route response', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { ApiId: { Ref: 'myapi4C7BF186' }, - ContentType: apigw.KnownContentTypes.JSON, + ContentType: 'application/json', Name: 'statusResponse', })); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts index 119c1f4601359..b358cbea3f3cf 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts @@ -28,6 +28,6 @@ test('minimal setup', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Stage', { ApiId: { Ref: 'myapi4C7BF186' }, StageName: 'dev', - DeploymentId: { Ref: 'deployment33381975F8795BE8' }, + DeploymentId: { Ref: 'deployment33381975' }, })); }); \ No newline at end of file From 3ec9a177500f52ce6def2a480cb4610654603ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 30 Jun 2020 00:15:48 +0200 Subject: [PATCH 5/7] feat(aws-apigatewayv2): update to HTTP Api Model Merged the model to be compatible with the new Http Api Model --- packages/@aws-cdk/aws-apigatewayv2/lib/api.ts | 521 ------------------ .../lib/common/api-mapping.ts | 12 + .../aws-apigatewayv2/lib/common/authorizer.ts | 12 + .../lib/{ => common}/domain-name.ts | 18 +- .../aws-apigatewayv2/lib/common/index.ts | 4 + .../lib/{ => common}/json-schema.ts | 0 .../@aws-cdk/aws-apigatewayv2/lib/http-api.ts | 470 ---------------- .../@aws-cdk/aws-apigatewayv2/lib/index.ts | 3 +- .../lib/integrations/http-integration.ts | 100 ---- .../lib/integrations/index.ts | 4 - .../lib/integrations/lambda-integration.ts | 109 ---- .../lib/integrations/mock-integration.ts | 68 --- .../lib/integrations/service-integration.ts | 99 ---- .../lib/{ => web-socket}/api-mapping.ts | 40 +- .../{web-socket-api.ts => web-socket/api.ts} | 231 +++++--- .../lib/{ => web-socket}/authorizer.ts | 100 +--- .../lib/{ => web-socket}/deployment.ts | 22 +- .../aws-apigatewayv2/lib/web-socket/index.ts | 11 + .../{ => web-socket}/integration-response.ts | 31 +- .../lib/{ => web-socket}/integration.ts | 209 ++----- .../lib/web-socket/integrations/http.ts | 57 ++ .../lib/web-socket/integrations/index.ts | 4 + .../lib/web-socket/integrations/lambda.ts | 71 +++ .../lib/web-socket/integrations/mock.ts | 46 ++ .../lib/web-socket/integrations/service.ts | 57 ++ .../lib/{ => web-socket}/model.ts | 37 +- .../lib/{ => web-socket}/route-response.ts | 47 +- .../lib/{ => web-socket}/route.ts | 187 +------ .../lib/{ => web-socket}/stage.ts | 67 +-- .../@aws-cdk/aws-apigatewayv2/package.json | 57 +- .../test/{ => common}/domain-name.test.ts | 2 +- .../test/integ.http-api.expected.json | 425 -------------- .../aws-apigatewayv2/test/integ.http-api.ts | 51 -- .../test/integ.web-socket-api.expected.json | 1 - .../test/integ.web-socket-api.ts | 16 +- .../aws-apigatewayv2/test/route.test.ts | 115 ---- .../test/{ => web-socket}/api-mapping.test.ts | 10 +- .../test/{ => web-socket}/api.test.ts | 108 +--- .../test/{ => web-socket}/authorizer.test.ts | 12 +- .../test/{ => web-socket}/integration.test.ts | 133 +++-- .../test/web-socket/route.test.ts | 83 +++ .../test/{ => web-socket}/stage.test.ts | 11 +- 42 files changed, 863 insertions(+), 2798 deletions(-) delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => common}/domain-name.ts (87%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => common}/json-schema.ts (100%) delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/http-api.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/api-mapping.ts (64%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{web-socket-api.ts => web-socket/api.ts} (51%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/authorizer.ts (65%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/deployment.ts (85%) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/index.ts rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/integration-response.ts (77%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/integration.ts (64%) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/http.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/mock.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/service.ts rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/model.ts (75%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/route-response.ts (64%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/route.ts (50%) rename packages/@aws-cdk/aws-apigatewayv2/lib/{ => web-socket}/stage.ts (74%) rename packages/@aws-cdk/aws-apigatewayv2/test/{ => common}/domain-name.test.ts (94%) delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts rename packages/@aws-cdk/aws-apigatewayv2/test/{ => web-socket}/api-mapping.test.ts (69%) rename packages/@aws-cdk/aws-apigatewayv2/test/{ => web-socket}/api.test.ts (50%) rename packages/@aws-cdk/aws-apigatewayv2/test/{ => web-socket}/authorizer.test.ts (76%) rename packages/@aws-cdk/aws-apigatewayv2/test/{ => web-socket}/integration.test.ts (60%) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts rename packages/@aws-cdk/aws-apigatewayv2/test/{ => web-socket}/stage.test.ts (63%) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts deleted file mode 100644 index 288ddc6ca4d42..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api.ts +++ /dev/null @@ -1,521 +0,0 @@ -import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; - -import { CfnApi } from './apigatewayv2.generated'; -import { Deployment } from './deployment'; -import { IRoute, KnownRouteSelectionExpression } from './route'; -import { IStage, Stage, StageOptions } from './stage'; - -/** - * Available protocols for ApiGateway V2 APIs (currently only 'WEBSOCKET' is supported) - */ -export enum ProtocolType { - /** - * WebSocket API - */ - WEBSOCKET = 'WEBSOCKET', - - /** - * HTTP API - */ - HTTP = 'HTTP' -} - -/** - * Specifies how to interpret the base path of the API during import - */ -export enum BasePath { - /** - * Ignores the base path - */ - IGNORE = 'ignore', - - /** - * Prepends the base path to the API path - */ - PREPEND = 'prepend', - - /** - * Splits the base path from the API path - */ - SPLIT = 'split' -} - -/** - * This expression is evaluated when the service determines the given request should proceed - * only if the client provides a valid API key. - */ -export enum KnownApiKeySelectionExpression { - /** - * Uses the `x-api-key` header to get the API Key - */ - HEADER_X_API_KEY = '$request.header.x-api-key', - - /** - * Uses the `usageIdentifier` property of the current authorizer to get the API Key - */ - AUTHORIZER_USAGE_IDENTIFIER = '$context.authorizer.usageIdentifierKey' -} - -/** - * The BodyS3Location property specifies an S3 location from which to import an OpenAPI definition. - */ -export interface BodyS3Location { - /** - * The S3 bucket that contains the OpenAPI definition to import. - */ - readonly bucket: string; - - /** - * The Etag of the S3 object. - * - * @default - no etag verification - */ - readonly etag?: string; - - /** - * The key of the S3 object. - */ - readonly key: string; - - /** - * The version of the S3 object. - * - * @default - get latest version - */ - readonly version?: string; -} - -/** - * The CorsConfiguration property specifies a CORS configuration for an API. - */ -export interface CorsConfiguration { - /** - * Specifies whether credentials are included in the CORS request. - * - * @default false - */ - readonly allowCredentials?: boolean; - - /** - * Represents a collection of allowed headers. - * - * @default - no headers allowed - */ - readonly allowHeaders?: string[]; - - /** - * Represents a collection of allowed HTTP methods. - * - * @default - no allowed methods - */ - readonly allowMethods: string[]; - - /** - * Represents a collection of allowed origins. - * - * @default - get allowed origins - */ - readonly allowOrigins?: string[]; - - /** - * Represents a collection of exposed headers. - * - * @default - get allowed origins - */ - readonly exposeHeaders?: string[]; - - /** - * The number of seconds that the browser should cache preflight request results. - * - * @default - get allowed origins - */ - readonly maxAge?: number; -} - -/** - * Defines the contract for an Api Gateway V2 Api. - */ -export interface IApi extends IResource { - /** - * The ID of this API Gateway Api. - * @attribute - */ - readonly apiId: string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Api. - */ -export interface ApiProps { - /** - * Indicates if a Deployment should be automatically created for this API, - * and recreated when the API model (route, integration) changes. - * - * Since API Gateway deployments are immutable, When this option is enabled - * (by default), an AWS::ApiGatewayV2::Deployment resource will automatically - * created with a logical ID that hashes the API model (methods, resources - * and options). This means that when the model changes, the logical ID of - * this CloudFormation resource will change, and a new deployment will be - * created. - * - * If this is set, `latestDeployment` will refer to the `Deployment` object - * and `deploymentStage` will refer to a `Stage` that points to this - * deployment. To customize the stage options, use the `deployOptions` - * property. - * - * A CloudFormation Output will also be defined with the root URL endpoint - * of this REST API. - * - * @default true - */ - readonly deploy?: boolean; - - /** - * Options for the API Gateway stage that will always point to the latest - * deployment when `deploy` is enabled. If `deploy` is disabled, - * this value cannot be set. - * - * @default - default options - */ - readonly deployOptions?: StageOptions; - - /** - * Retains old deployment resources when the API changes. This allows - * manually reverting stages to point to old deployments via the AWS - * Console. - * - * @default false - */ - readonly retainDeployments?: boolean; - - /** - * A name for the API Gateway Api resource. - * - * @default - ID of the Api construct. - */ - readonly apiName?: string; - - /** - * Specifies how to interpret the base path of the API during import. - * - * Supported only for HTTP APIs. - * @default 'ignore' - */ - readonly basePath?: BasePath; - - /** - * The OpenAPI definition. - * - * To import an HTTP API, you must specify `body` or `bodyS3Location`. - * - * Supported only for HTTP APIs. - * @default - `bodyS3Location` if defined, or no import - */ - readonly body?: string; - - /** - * The S3 location of an OpenAPI definition. - * - * To import an HTTP API, you must specify `body` or `bodyS3Location`. - * - * Supported only for HTTP APIs. - * @default - `body` if defined, or no import - */ - readonly bodyS3Location?: BodyS3Location; - - /** - * The S3 location of an OpenAPI definition. - * - * To import an HTTP API, you must specify `body` or `bodyS3Location`. - * - * Supported only for HTTP APIs. - * @default - `body` if defined, or no import - */ - readonly corsConfiguration?: CorsConfiguration; - - /** - * Available protocols for ApiGateway V2 APIs - * - * @default - required unless you specify an OpenAPI definition - */ - readonly protocolType?: ProtocolType | string; - - /** - * Expression used to select the route for this API - * - * @default - '${request.method} ${request.path}' for HTTP APIs, required for Web Socket APIs - */ - readonly routeSelectionExpression?: KnownRouteSelectionExpression | string; - - /** - * Expression used to select the Api Key to use for metering - * - * Supported only for WebSocket APIs - * - * @default - No Api Key - */ - readonly apiKeySelectionExpression?: KnownApiKeySelectionExpression | string; - - /** - * A description of the purpose of this API Gateway Api resource. - * - * @default - No description. - */ - readonly description?: string; - - /** - * Indicates whether schema validation will be disabled for this Api - * - * @default false - */ - readonly disableSchemaValidation?: boolean; - - /** - * Indicates the version number for this Api - * - * @default false - */ - readonly version?: string; - - /** - * Specifies whether to rollback the API creation (`true`) or not (`false`) when a warning is encountered - * - * @default false - */ - readonly failOnWarnings?: boolean; -} - -/** - * Represents a WebSocket API in Amazon API Gateway v2. - * - * Use `addModel` and `addLambdaIntegration` to configure the API model. - * - * By default, the API will automatically be deployed and accessible from a - * public endpoint. - */ -export class Api extends Resource implements IApi { - - /** - * Creates a new imported API - * - * @param scope scope of this imported resource - * @param id identifier of the resource - * @param apiId Identifier of the API - */ - public static fromApiId(scope: Construct, id: string, apiId: string): IApi { - class Import extends Resource implements IApi { - public readonly apiId = apiId; - } - - return new Import(scope, id); - } - - /** - * The ID of this API Gateway Api. - */ - public readonly apiId: string; - - /** - * API Gateway stage that points to the latest deployment (if defined). - */ - public deploymentStage?: Stage; - - /** - * API Gateway deployment (if defined). - */ - public deployment?: Deployment; - - protected resource: CfnApi; - - constructor(scope: Construct, id: string, props?: ApiProps) { - if (props === undefined) { - props = {}; - } - super(scope, id, { - physicalName: props.apiName || id, - }); - - if (props.protocolType === undefined && props.body === null && props.bodyS3Location === null) { - throw new Error('You must specify a protocol type, or import an Open API definition (directly or from S3)'); - } - - switch (props.protocolType) { - case ProtocolType.WEBSOCKET: { - if (props.basePath !== undefined) { - throw new Error('"basePath" is only supported with HTTP APIs'); - } - if (props.body !== undefined) { - throw new Error('"body" is only supported with HTTP APIs'); - } - if (props.bodyS3Location !== undefined) { - throw new Error('"bodyS3Location" is only supported with HTTP APIs'); - } - if (props.corsConfiguration !== undefined) { - throw new Error('"corsConfiguration" is only supported with HTTP APIs'); - } - if (props.routeSelectionExpression === undefined) { - throw new Error('"routeSelectionExpression" is required for Web Socket APIs'); - } - break; - } - case ProtocolType.HTTP: - case undefined: { - if (props.apiKeySelectionExpression !== undefined) { - throw new Error('"apiKeySelectionExpression" is only supported with Web Socket APIs'); - } - if (props.disableSchemaValidation !== undefined) { - throw new Error('"disableSchemaValidation" is only supported with Web Socket APIs'); - } - if (props.routeSelectionExpression !== undefined && props.routeSelectionExpression !== KnownRouteSelectionExpression.METHOD_PATH) { - throw new Error('"routeSelectionExpression" has a single supported value for HTTP APIs: "${request.method} ${request.path}"'); - } - break; - } - } - - this.resource = new CfnApi(this, 'Resource', { - name: this.physicalName, - apiKeySelectionExpression: props.apiKeySelectionExpression, - basePath: props.basePath, - body: props.body, - bodyS3Location: props.bodyS3Location, - corsConfiguration: props.corsConfiguration, - description: props.description, - disableSchemaValidation: props.disableSchemaValidation, - failOnWarnings: props.failOnWarnings, - protocolType: props.protocolType, - routeSelectionExpression: props.routeSelectionExpression, - // TODO: tags: props.tags, - version: props.version, - }); - this.apiId = this.resource.ref; - - const deploy = props.deploy === undefined ? true : props.deploy; - if (deploy) { - const stageName = (props.deployOptions && props.deployOptions.stageName) || 'prod'; - - this.deployment = new Deployment(this, 'Deployment', { - api: this, - description: 'Automatically created by the Api construct', - }); - - this.deploymentStage = new Stage(this, 'DefaultStage', { - deployment: this.deployment, - api: this, - stageName, - description: 'Automatically created by the Api construct', - accessLogSettings: props.deployOptions?.accessLogSettings, - autoDeploy: props.deployOptions?.autoDeploy, - clientCertificateId: props.deployOptions?.clientCertificateId, - defaultRouteSettings: props.deployOptions?.defaultRouteSettings, - routeSettings: props.deployOptions?.routeSettings, - stageVariables: props.deployOptions?.stageVariables, - }); - } else { - if (props.deployOptions) { - throw new Error('Cannot set "deployOptions" if "deploy" is disabled'); - } - } - } - - /** - * Returns the ARN for a specific route and stage. - * - * @param route The route for this ARN ('*' if not defined) - * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') - */ - public executeApiArn(route?: IRoute, stage?: IStage) { - const stack = Stack.of(this); - const apiId = this.apiId; - const routeKey = ((route === undefined) ? '*' : route.key); - const stageName = ((stage === undefined) ? - ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : - stage.stageName); - return stack.formatArn({ - service: 'execute-api', - resource: apiId, - sep: '/', - resourceName: `${stageName}/${routeKey}`, - }); - } - - /** - * Returns the ARN for a specific connection. - * - * @param connectionId The identifier of this connection ('*' if not defined) - * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') - */ - public webSocketConnectionsApiArn(connectionId: string = '*', stage?: IStage) { - const stack = Stack.of(this); - const apiId = this.apiId; - const stageName = ((stage === undefined) ? - ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : - stage.stageName); - return stack.formatArn({ - service: 'execute-api', - resource: apiId, - sep: '/', - resourceName: `${stageName}/POST/${connectionId}`, - }); - } - - /** - * Returns the client URL for this Api, for a specific stage. - * - * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. - * @param stage The stage for this URL (if not defined, defaults to the deployment stage) - */ - public httpsClientUrl(stage?: IStage): string { - const stack = Stack.of(this); - let stageName: string | undefined; - if (stage === undefined) { - if (this.deploymentStage === undefined) { - throw Error('No stage defined for this Api'); - } - stageName = this.deploymentStage.stageName; - } else { - stageName = stage.stageName; - } - return `https://${this.apiId}.execute-api.${stack.region}.amazonaws.com/${stageName}`; - } - - /** - * Returns the client URL for this Api, for a specific stage. - * - * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. - * @param stage The stage for this URL (if not defined, defaults to the deployment stage) - */ - public webSocketClientUrl(stage?: IStage): string { - const stack = Stack.of(this); - let stageName: string | undefined; - if (stage === undefined) { - if (this.deploymentStage === undefined) { - throw Error('No stage defined for this Api'); - } - stageName = this.deploymentStage.stageName; - } else { - stageName = stage.stageName; - } - return `wss://${this.apiId}.execute-api.${stack.region}.amazonaws.com/${stageName}`; - } - - /** - * Returns the connections URL for this Api, for a specific stage. - * - * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. - * @param stage The stage for this URL (if not defined, defaults to the deployment stage) - */ - public webSocketConnectionsUrl(stage?: IStage): string { - const stack = Stack.of(this); - let stageName: string | undefined; - if (stage === undefined) { - if (this.deploymentStage === undefined) { - throw Error('No stage defined for this Api'); - } - stageName = this.deploymentStage.stageName; - } else { - stageName = stage.stageName; - } - return `https://${this.apiId}.execute-api.${stack.region}.amazonaws.com/${stageName}/@connections`; - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts new file mode 100644 index 0000000000000..68314a5b42ca6 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -0,0 +1,12 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Defines the contract for an Api Gateway V2 Api Mapping. + */ +export interface IApiMapping extends IResource { + /** + * The ID of this API Gateway Api Mapping. + * @attribute + */ + readonly apiMappingId: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts new file mode 100644 index 0000000000000..a7cd1ebb0615d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts @@ -0,0 +1,12 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Defines the contract for an Api Gateway V2 Authorizer. + */ +export interface IAuthorizer extends IResource { + /** + * The ID of this API Gateway Api Mapping. + * @attribute + */ + readonly authorizerId: string; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts similarity index 87% rename from packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts index 899ccbf58abfd..23eb72282559b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/domain-name.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts @@ -1,6 +1,6 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { CfnDomainName } from './apigatewayv2.generated'; +import { CfnDomainName } from '../apigatewayv2.generated'; /** * Represents an endpoint type @@ -44,16 +44,6 @@ export interface DomainNameConfiguration { readonly endpointType?: EndpointType; } -/** - * Defines the attributes for an Api Gateway V2 Domain Name. - */ -export interface DomainNameAttributes { - /** - * The custom domain name for your API in Amazon API Gateway. - */ - readonly domainName: string; -} - /** * Defines the contract for an Api Gateway V2 Domain Name. */ @@ -97,11 +87,11 @@ export class DomainName extends Resource implements IDomainName { * * @param scope scope of this imported resource * @param id identifier of the resource - * @param attrs domain name attributes + * @param domainName name of the domain */ - public static fromDomainNameAttributes(scope: Construct, id: string, attrs: DomainNameAttributes): IDomainName { + public static fromDomainName(scope: Construct, id: string, domainName: string): IDomainName { class Import extends Resource implements IDomainName { - public readonly domainName = attrs.domainName; + public readonly domainName = domainName; } return new Import(scope, id); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index 5995c40125978..76fa2c21c1937 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -1,3 +1,7 @@ +export * from './api-mapping'; +export * from './authorizer'; +export * from './domain-name'; export * from './integration'; +export * from './json-schema'; export * from './route'; export * from './stage'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/json-schema.ts similarity index 100% rename from packages/@aws-cdk/aws-apigatewayv2/lib/json-schema.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/common/json-schema.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http-api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http-api.ts deleted file mode 100644 index 524440c67fdeb..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http-api.ts +++ /dev/null @@ -1,470 +0,0 @@ -import { IFunction } from '@aws-cdk/aws-lambda'; -import { CfnResource, Construct, IConstruct } from '@aws-cdk/core'; - -import { Api, BasePath, BodyS3Location, CorsConfiguration, ProtocolType } from './api'; -import { ApiMapping, ApiMappingOptions } from './api-mapping'; -import { HttpApiIntegrationMethod, Integration } from './integration'; -import { HttpApiHttpIntegrationOptions, HttpIntegration } from './integrations/http-integration'; -import { HttpApiLambdaIntegrationOptions, LambdaIntegration } from './integrations/lambda-integration'; -import { HttpApiServiceIntegrationOptions, ServiceIntegration } from './integrations/service-integration'; -import { HttpApiRouteOptions, IRoute, KnownRouteKey, KnownRouteSelectionExpression, Route } from './route'; -import { IStage, StageOptions } from './stage'; - -/** - * Specifies the integration's HTTP method type (only GET is supported for WebSocket) - */ -export enum HttpMethod { - /** - * All HTTP Methods are supported - */ - ANY = 'ANY', - - /** - * GET HTTP Method - */ - GET = 'GET', - - /** - * POST HTTP Method - */ - POST = 'POST', - - /** - * PUT HTTP Method - */ - PUT = 'PUT', - - /** - * DELETE HTTP Method - */ - DELETE = 'DELETE', - - /** - * OPTIONS HTTP Method - */ - OPTIONS = 'OPTIONS', - - /** - * HEAD HTTP Method - */ - HEAD = 'HEAD', - - /** - * PATCH HTTP Method - */ - PATCH = 'PATCH' -} - -/** - * Defines where an Open API Definition is stored - */ -export interface HttpApiBody { - /** - * Stored inline in this declaration. - * - * If this is not defined, `bodyS3Location` has to be defined - * - * @default - Use S3 Location - */ - readonly body?: string; - /** - * Stored in an Amazon S3 Object. - * - * If this is not defined, `body` has to be defined - * - * @default - Use inline definition - */ - readonly bodyS3Location?: BodyS3Location; -} - -/** - * Defines a default handler for the Api - * - * One of the properties need to be defined - */ -export interface HttpApiDefaultTarget { - /** - * Use an AWS Lambda function - * - * If this is not defined, `uri` has to be defined - * - * @default - use one of the other properties - */ - readonly handler?: IFunction; - - /** - * Use a URI for the handler. - * If a string is provided, it will test is the string starts with - * - 'http://' or 'https://': it creates an http - * - 'arn:': it will create an AWS Serice integration - * - it will fail for any other value - * - * If this is not defined, `handler` has to be defined - * - * @default - Use inline definition - */ - readonly uri?: string; -} - -/** - * Defines the contract for an Api Gateway V2 HTTP Api. - */ -export interface IHttpApi extends IConstruct { - /** - * The ID of this API Gateway Api. - * @attribute - */ - readonly apiId: string; -} - -/** - * Defines the properties of a Web Socket API - */ -export interface HttpApiProps { - /** - * Indicates if a Deployment should be automatically created for this API, - * and recreated when the API model (route, integration) changes. - * - * Since API Gateway deployments are immutable, When this option is enabled - * (by default), an AWS::ApiGatewayV2::Deployment resource will automatically - * created with a logical ID that hashes the API model (methods, resources - * and options). This means that when the model changes, the logical ID of - * this CloudFormation resource will change, and a new deployment will be - * created. - * - * If this is set, `latestDeployment` will refer to the `Deployment` object - * and `deploymentStage` will refer to a `Stage` that points to this - * deployment. To customize the stage options, use the `deployOptions` - * property. - * - * A CloudFormation Output will also be defined with the root URL endpoint - * of this REST API. - * - * @default true - */ - readonly deploy?: boolean; - - /** - * Options for the API Gateway stage that will always point to the latest - * deployment when `deploy` is enabled. If `deploy` is disabled, - * this value cannot be set. - * - * @default - default options - */ - readonly deployOptions?: StageOptions; - - /** - * Retains old deployment resources when the API changes. This allows - * manually reverting stages to point to old deployments via the AWS - * Console. - * - * @default false - */ - readonly retainDeployments?: boolean; - - /** - * A name for the API Gateway Api resource. - * - * @default - ID of the Api construct. - */ - readonly apiName?: string; - - /** - * Specifies how to interpret the base path of the API during import. - * - * @default 'ignore' - */ - readonly basePath?: BasePath; - - /** - * The OpenAPI definition. Used to import an HTTP Api. - * Use either a body definition or the location of an Amazon S3 Object - * - * @default - no import - */ - readonly body?: HttpApiBody; - - /** - * A CORS configuration. - * - * @default - CORS not activated - */ - readonly corsConfiguration?: CorsConfiguration; - - /** - * A description of the purpose of this API Gateway Api resource. - * - * @default - No description. - */ - readonly description?: string; - - /** - * Indicates whether schema validation will be disabled for this Api - * - * @default false - */ - readonly disableSchemaValidation?: boolean; - - /** - * Indicates the version number for this Api - * - * @default false - */ - readonly version?: string; - - /** - * Specifies whether to rollback the API creation (`true`) or not (`false`) when a warning is encountered - * - * @default false - */ - readonly failOnWarnings?: boolean; - - /** - * If defined, creates a default proxy target for the HTTP Api. - * - * @default - no default handler or route - */ - readonly defaultTarget?: HttpApiDefaultTarget; -} - -/** - * Represents an HTTP Route entry - */ -export interface HttpRouteEntry { - /** - * Method for thei API Route - * - * @default 'ANY' - */ - readonly method?: HttpMethod; - - /** - * Path of the route - */ - readonly path: string; -} - -export declare type HttpRouteName = KnownRouteKey | HttpRouteEntry | string; - -/** - * Represents an HTTP API in Amazon API Gateway v2. - * - * Use `addModel` and `addLambdaIntegration` to configure the API model. - * - * By default, the API will automatically be deployed and accessible from a - * public endpoint. - */ -export class HttpApi extends Construct implements IHttpApi { - /** - * Creates a new imported API - * - * @param scope scope of this imported resource - * @param id identifier of the resource - * @param apiId Identifier of the API - */ - public static fromApiId(scope: Construct, id: string, apiId: string): IHttpApi { - class Import extends Construct implements IHttpApi { - public readonly apiId = apiId; - } - - return new Import(scope, id); - } - - /** - * The ID of this API Gateway Api. - */ - public readonly apiId: string; - - protected readonly resource: Api; - - constructor(scope: Construct, id: string, props?: HttpApiProps) { - if (props === undefined) { - props = {}; - } - super(scope, id); - - this.resource = new Api(this, 'Default', { - ...props, - apiName: props.apiName ?? id, - body: ((props.body !== undefined) && (props.body.body !== undefined) ? props.body.body : undefined), - bodyS3Location: ((props.body !== undefined) && (props.body.bodyS3Location !== undefined) ? props.body.bodyS3Location : undefined), - protocolType: ProtocolType.HTTP, - routeSelectionExpression: KnownRouteSelectionExpression.METHOD_PATH, - }); - - this.apiId = this.resource.apiId; - - if (props.defaultTarget !== undefined) { - let integration; - if (props.defaultTarget.handler !== undefined) { - integration = this.addLambdaIntegration('default', { - handler: props.defaultTarget.handler, - }); - } else if (props.defaultTarget.uri) { - const uri = props.defaultTarget.uri; - if (uri.startsWith('https://') || uri.startsWith('http://')) { - integration = this.addHttpIntegration('default', { - url: uri, - }); - } else if (uri.startsWith('arn:')) { - integration = this.addServiceIntegration('default', { - arn: uri, - }); - } else { - throw new Error('Invalid string format, must be a fully qualified ARN or a URL'); - } - } else { - throw new Error('You must specify an ARN, a URL, or a Lambda Function'); - } - - this.addRoute(KnownRouteKey.DEFAULT, integration, {}); - } - } - - /** - * API Gateway deployment used for automated deployments. - */ - public get deployment() { - return this.resource.deployment; - } - - /** - * API Gateway stage used for automated deployments. - */ - public get deploymentStage() { - return this.resource.deploymentStage; - } - - /** - * Creates a new integration for this api, using a Lambda integration. - * - * @param id the id of this integration - * @param props the properties for this integration - */ - public addLambdaIntegration(id: string, props: HttpApiLambdaIntegrationOptions): LambdaIntegration { - const integration = new LambdaIntegration(this, `${id}.lambda.integration`, { - ...props, - payloadFormatVersion: props.payloadFormatVersion ?? '1.0', - api: this.resource, - proxy: true, - }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); - } - return integration; - } - - /** - * Creates a new integration for this api, using a HTTP integration. - * - * @param id the id of this integration - * @param props the properties for this integration - */ - public addHttpIntegration(id: string, props: HttpApiHttpIntegrationOptions): HttpIntegration { - const integration = new HttpIntegration(this, `${id}.http.integration`, { - ...props, - integrationMethod: props.integrationMethod ?? HttpApiIntegrationMethod.ANY, - payloadFormatVersion: props.payloadFormatVersion ?? '1.0', - api: this.resource, - proxy: true, - }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); - } - return integration; - } - - /** - * Creates a new service for this api, using a Lambda integration. - * - * @param id the id of this integration - * @param props the properties for this integration - */ - public addServiceIntegration(id: string, props: HttpApiServiceIntegrationOptions): ServiceIntegration { - const integration = new ServiceIntegration(this, `${id}.service.integration`, { - ...props, - integrationMethod: props.integrationMethod ?? HttpApiIntegrationMethod.ANY, - payloadFormatVersion: props.payloadFormatVersion ?? '1.0', - api: this.resource, - proxy: true, - }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); - } - return integration; - } - - /** - * Creates a new route for this api, on the specified methods. - * - * @param key the route key (predefined or not) to use - * @param integration [disable-awslint:ref-via-interface] the integration to use for this route - * @param props the properties for this route - */ - public addRoute( - key: HttpRouteName, - integration: Integration | LambdaIntegration | HttpIntegration | ServiceIntegration, - props?: HttpApiRouteOptions): Route { - const keyName = ((typeof(key) === 'object') ? `${key.method ?? 'ANY'} ${key.path}` : key); - const route = new Route(this, `${keyName}.route`, { - ...props, - api: this.resource, - integration: ((integration instanceof Integration) ? integration : integration.resource), - key: keyName, - }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(route.node.defaultChild as CfnResource); - } - return route; - } - - /** - * Creates a new route for this api, on the specified methods. - * - * @param keys the route keys (predefined or not) to use - * @param integration [disable-awslint:ref-via-interface] the integration to use for these routes - * @param props the properties for these routes - */ - public addRoutes( - keys: HttpRouteName[], - integration: Integration | LambdaIntegration | HttpIntegration | ServiceIntegration, - props?: HttpApiRouteOptions): Route[] { - return keys.map((key) => this.addRoute(key, integration, props)); - } - - /** - * Creates a new domain name mapping for this api. - * - * @param props the properties for this Api Mapping - */ - public addApiMapping(props: ApiMappingOptions): ApiMapping { - const mapping = new ApiMapping(this, `${props.domainName}.${props.stage}.mapping`, { - ...props, - api: this.resource, - }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(mapping.node.defaultChild as CfnResource); - } - return mapping; - } - - /** - * Returns the ARN for a specific route and stage. - * - * @param route The route for this ARN ('*' if not defined) - * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') - */ - public executeApiArn(route?: IRoute, stage?: IStage) { - return this.resource.executeApiArn(route, stage); - } - - /** - * Returns the client URL for this Api, for a specific stage. - * - * Fails if `stage` is not defined, and `deploymentStage` is not set either by `deploy` or explicitly. - * @param stage The stage for this URL (if not defined, defaults to the deployment stage) - */ - public clientUrl(stage?: IStage): string { - return this.resource.httpsClientUrl(stage); - } -} \ 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 31ea86b4a91c2..e07d9f3389332 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 './web-socket'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts deleted file mode 100644 index fc6876954633d..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/http-integration.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Construct } from '@aws-cdk/core'; - -import { IApi } from '../api'; -import { HttpApiIntegrationMethod, HttpApiIntegrationOptions, Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; -import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface BaseHttpIntegrationOptions { - /** - * The HTTP URL for this integration - */ - readonly url: string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface HttpIntegrationOptions extends IntegrationOptions, BaseHttpIntegrationOptions { -} - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Integration. - */ -export interface HttpIntegrationProps extends HttpIntegrationOptions { - /** - * Defines if this integration is a proxy integration or not. - * - * @default false - */ - readonly proxy?: boolean; - - /** - * Defines the api for this integration. - */ - readonly api: IApi; -} - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface HttpApiHttpIntegrationOptions extends HttpApiIntegrationOptions, BaseHttpIntegrationOptions { - /** - * Specifies the integration's HTTP method type. - * - * @default - 'ANY' - */ - readonly integrationMethod?: HttpApiIntegrationMethod | string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface WebSocketApiHttpIntegrationOptions extends WebSocketApiIntegrationOptions, BaseHttpIntegrationOptions { - /** - * Defines if this integration is a proxy integration or not. - * - * @default false - */ - readonly proxy?: boolean; -} - -/** - * An AWS Lambda integration for an API in Amazon API Gateway v2. - */ -export class HttpIntegration extends Construct { - /** - * L1 Integration construct - */ - public readonly resource: Integration; - - constructor(scope: Construct, id: string, props: HttpIntegrationProps) { - super(scope, id); - - this.resource = new Integration(this, 'Default', { - ...props, - type: props.proxy ? IntegrationType.HTTP_PROXY : IntegrationType.HTTP, - uri: props.url, - }); - } - - /** - * Creates a new response for this integration. - * - * @param key the key (predefined or not) that will select this response - * @param props the properties for this response - */ - public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { - return this.resource.addResponse(key, props); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts deleted file mode 100644 index 9338f63c873a8..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './http-integration'; -export * from './lambda-integration'; -export * from './mock-integration'; -export * from './service-integration'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts deleted file mode 100644 index b4643d7b6c1f8..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/lambda-integration.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ServicePrincipal } from '@aws-cdk/aws-iam'; -import { IFunction } from '@aws-cdk/aws-lambda'; -import { Construct, Stack } from '@aws-cdk/core'; - -import { Api, IApi } from '../api'; -import { HttpApiIntegrationOptions, Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; -import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface BaseLambdaIntegrationOptions { - /** - * The Lambda function handler for this integration - */ - readonly handler: IFunction; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface LambdaIntegrationOptions extends IntegrationOptions, BaseLambdaIntegrationOptions { -} - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - */ -export interface LambdaIntegrationProps extends LambdaIntegrationOptions { - /** - * Defines if this integration is a proxy integration or not. - * - * @default false - */ - readonly proxy?: boolean; - - /** - * Defines the api for this integration. - */ - readonly api: IApi; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Http API Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface HttpApiLambdaIntegrationOptions extends HttpApiIntegrationOptions, BaseLambdaIntegrationOptions { -} - -/** - * Defines the properties required for defining an Api Gateway V2 WebSocket API Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface WebSocketApiLambdaIntegrationOptions extends WebSocketApiIntegrationOptions, BaseLambdaIntegrationOptions { - /** - * Defines if this integration is a proxy integration or not. - * - * @default false - */ - readonly proxy?: boolean; -} - -/** - * An AWS Lambda integration for an API in Amazon API Gateway v2. - */ -export class LambdaIntegration extends Construct { - /** - * L1 Integration construct - */ - public readonly resource: Integration; - - constructor(scope: Construct, id: string, props: LambdaIntegrationProps) { - super(scope, id); - - const stack = Stack.of(scope); - - // This is not a standard ARN as it does not have the account-id part in it - const uri = `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`; - this.resource = new Integration(this, 'Default', { - ...props, - type: props.proxy ? IntegrationType.AWS_PROXY : IntegrationType.AWS, - integrationMethod: 'POST', - uri, - }); - - if (props.api instanceof Api) { - const sourceArn = props.api.executeApiArn(); - props.handler.addPermission(`ApiPermission.${this.node.uniqueId}`, { - principal: new ServicePrincipal('apigateway.amazonaws.com'), - sourceArn, - }); - } - } - - /** - * Creates a new response for this integration. - * - * @param key the key (predefined or not) that will select this response - * @param props the properties for this response - */ - public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { - return this.resource.addResponse(key, props); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts deleted file mode 100644 index 0babb1995c562..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/mock-integration.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Construct } from '@aws-cdk/core'; - -import { IApi } from '../api'; -import { Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; -import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; - -/** - * Defines the properties required for defining an Api Gateway V2 Mock Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface BaseMockIntegrationOptions { -} - -/** - * Defines the properties required for defining an Api Gateway V2 Mock Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface MockIntegrationOptions extends IntegrationOptions { -} - -/** - * Defines the properties required for defining an Api Gateway V2 Mock Integration. - */ -export interface MockIntegrationProps extends MockIntegrationOptions { - /** - * Defines the api for this integration. - */ - readonly api: IApi; -} - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface WebSocketApiMockIntegrationOptions extends WebSocketApiIntegrationOptions, BaseMockIntegrationOptions { -} - -/** - * An AWS Lambda integration for an API in Amazon API Gateway v2. - */ -export class MockIntegration extends Construct { - /** - * L1 Integration construct - */ - public readonly resource: Integration; - - constructor(scope: Construct, id: string, props: MockIntegrationProps) { - super(scope, id); - this.resource = new Integration(this, 'Default', { - ...props, - type: IntegrationType.MOCK, - uri: '', - }); - } - - /** - * Creates a new response for this integration. - * - * @param key the key (predefined or not) that will select this response - * @param props the properties for this response - */ - public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { - return this.resource.addResponse(key, props); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts deleted file mode 100644 index 965ddb64b9f79..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integrations/service-integration.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Construct } from '@aws-cdk/core'; - -import { IApi } from '../api'; -import { HttpApiIntegrationMethod, HttpApiIntegrationOptions, Integration, IntegrationOptions, IntegrationType, WebSocketApiIntegrationOptions } from '../integration'; -import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from '../integration-response'; - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface BaseServiceIntegrationOptions { - /** - * The ARN of the target service for this integration - */ - readonly arn: string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface ServiceIntegrationOptions extends IntegrationOptions, BaseServiceIntegrationOptions { -} - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - */ -export interface ServiceIntegrationProps extends ServiceIntegrationOptions { - /** - * Defines if this integration is a proxy integration or not. - * - * @default false - */ - readonly proxy?: boolean; - - /** - * Defines the api for this integration. - */ - readonly api: IApi; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface HttpApiServiceIntegrationOptions extends HttpApiIntegrationOptions, BaseServiceIntegrationOptions { - /** - * Specifies the integration's HTTP method type. - * - * @default - 'ANY' - */ - readonly integrationMethod?: HttpApiIntegrationMethod | string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Lambda Integration. - * - * This interface is used by the helper methods in `Integration` - */ -export interface WebSocketApiServiceIntegrationOptions extends WebSocketApiIntegrationOptions, BaseServiceIntegrationOptions { - /** - * Defines if this integration is a proxy integration or not. - * - * @default false - */ - readonly proxy?: boolean; -} - -/** - * An AWS Lambda integration for an API in Amazon API Gateway v2. - */ -export class ServiceIntegration extends Construct { - /** - * L1 Integration construct - */ - public readonly resource: Integration; - - constructor(scope: Construct, id: string, props: ServiceIntegrationProps) { - super(scope, id); - this.resource = new Integration(this, 'Default', { - ...props, - type: props.proxy ? IntegrationType.AWS_PROXY : IntegrationType.AWS, - uri: props.arn, - }); - } - - /** - * Creates a new response for this integration. - * - * @param key the key (predefined or not) that will select this response - * @param props the properties for this response - */ - public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { - return this.resource.addResponse(key, props); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api-mapping.ts similarity index 64% rename from packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api-mapping.ts index 62c3eadbcd298..71036619ad227 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api-mapping.ts @@ -1,27 +1,17 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, Resource } from '@aws-cdk/core'; -import { IApi } from './api'; -import { CfnApiMapping } from './apigatewayv2.generated'; -import { IDomainName } from './domain-name'; -import { IStage } from './stage'; - -/** - * Defines the contract for an Api Gateway V2 Api Mapping. - */ -export interface IApiMapping extends IResource { - /** - * The ID of this API Gateway Api Mapping. - * @attribute - */ - readonly apiMappingId: string; -} +import { CfnApiMapping } from '../apigatewayv2.generated'; +import { IApiMapping } from '../common/api-mapping'; +import { IDomainName } from '../common/domain-name'; +import { IStage } from '../common/stage'; +import { IWebSocketApi } from './api'; /** * Defines the properties required for defining an Api Gateway V2 Api Mapping. * * This interface is used by the helper methods in `Api` and the sub-classes */ -export interface ApiMappingOptions { +export interface WebSocketApiMappingOptions { /** * The API mapping key. * @@ -32,7 +22,7 @@ export interface ApiMappingOptions { /** * The associated domain name */ - readonly domainName: IDomainName | string; + readonly domainName: IDomainName; /** * The API stage. @@ -43,11 +33,11 @@ export interface ApiMappingOptions { /** * Defines the properties required for defining an Api Gateway V2 Api Mapping. */ -export interface ApiMappingProps extends ApiMappingOptions { +export interface WebSocketApiMappingProps extends WebSocketApiMappingOptions { /** * Defines the api for this deployment. */ - readonly api: IApi; + readonly api: IWebSocketApi; } /** @@ -56,8 +46,10 @@ export interface ApiMappingProps extends ApiMappingOptions { * A custom domain name can have multiple API mappings, but the paths can't overlap. * * A custom domain can map only to APIs of the same protocol type. + * + * @resource AWS::ApiGatewayV2::ApiMapping */ -export class ApiMapping extends Resource implements IApiMapping { +export class WebSocketApiMapping extends Resource implements IApiMapping { /** * Creates a new imported API @@ -81,12 +73,12 @@ export class ApiMapping extends Resource implements IApiMapping { protected resource: CfnApiMapping; - constructor(scope: Construct, id: string, props: ApiMappingProps) { + constructor(scope: Construct, id: string, props: WebSocketApiMappingProps) { super(scope, id); this.resource = new CfnApiMapping(this, 'Resource', { - apiId: props.api.apiId, - domainName: ((typeof(props.domainName) === 'string') ? props.domainName : props.domainName.domainName), + apiId: props.api.webSocketApiId, + domainName: props.domainName.domainName, stage: props.stage.stageName, apiMappingKey: props.apiMappingKey, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts similarity index 51% rename from packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts index 8f5c69a99d4cd..f75c291e3ea84 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket-api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts @@ -1,16 +1,18 @@ import { IFunction } from '@aws-cdk/aws-lambda'; -import { CfnResource, Construct, IConstruct } from '@aws-cdk/core'; - -import { Api, KnownApiKeySelectionExpression, ProtocolType } from './api'; -import { Integration } from './integration'; -import { HttpIntegration, WebSocketApiHttpIntegrationOptions } from './integrations/http-integration'; -import { LambdaIntegration, WebSocketApiLambdaIntegrationOptions } from './integrations/lambda-integration'; -import { MockIntegration, WebSocketApiMockIntegrationOptions } from './integrations/mock-integration'; -import { ServiceIntegration, WebSocketApiServiceIntegrationOptions } from './integrations/service-integration'; -import { JsonSchema } from './json-schema'; -import { Model, ModelOptions } from './model'; -import { IRoute, KnownRouteKey, KnownRouteSelectionExpression, Route, WebSocketApiRouteOptions } from './route'; -import { IStage, StageOptions } from './stage'; +import { CfnResource, Construct, IResource, Resource, Stack } from '@aws-cdk/core'; +import { CfnApi } from '../apigatewayv2.generated'; +import { JsonSchema } from '../common/json-schema'; +import { IStage } from '../common/stage'; + +import { WebSocketDeployment } from './deployment'; +import { WebSocketIntegration } from './integration'; +import { WebSocketHttpIntegration, WebSocketHttpIntegrationOptions } from './integrations/http'; +import { WebSocketLambdaIntegration, WebSocketLambdaIntegrationOptions } from './integrations/lambda'; +import { WebSocketMockIntegration, WebSocketMockIntegrationOptions } from './integrations/mock'; +import { WebSocketServiceIntegration, WebSocketServiceIntegrationOptions } from './integrations/service'; +import { WebSocketModel, WebSocketModelOptions } from './model'; +import { WebSocketKnownRouteKey, WebSocketRoute, WebSocketRouteOptions } from './route'; +import { WebSocketStage, WebSocketStageOptions } from './stage'; /** * Defines a default handler for the Api @@ -45,12 +47,12 @@ export interface WebSocketApiDefaultTarget { /** * Defines the contract for an Api Gateway V2 HTTP Api. */ -export interface IWebSocketApi extends IConstruct { +export interface IWebSocketApi extends IResource { /** * The ID of this API Gateway Api. * @attribute */ - readonly apiId: string; + readonly webSocketApiId: string; } /** @@ -87,7 +89,7 @@ export interface WebSocketApiProps { * * @default - default options */ - readonly deployOptions?: StageOptions; + readonly deployOptions?: WebSocketStageOptions; /** * Retains old deployment resources when the API changes. This allows @@ -108,14 +110,14 @@ export interface WebSocketApiProps { /** * Expression used to select the route for this API */ - readonly routeSelectionExpression: KnownRouteSelectionExpression | string; + readonly routeSelectionExpression: string; /** * Expression used to select the Api Key to use for metering * * @default - No Api Key */ - readonly apiKeySelectionExpression?: KnownApiKeySelectionExpression | string; + readonly apiKeySelectionExpression?: string; /** * A description of the purpose of this API Gateway Api resource. @@ -153,27 +155,27 @@ export interface WebSocketApiProps { readonly defaultTarget?: WebSocketApiDefaultTarget; } -export declare type WebSocketRouteName = KnownRouteKey | string; - /** - * Represents an HTTP API in Amazon API Gateway v2. + * Represents a Web Socket API in Amazon API Gateway v2. * * Use `addModel` and `addLambdaIntegration` to configure the API model. * * By default, the API will automatically be deployed and accessible from a * public endpoint. + * + * @resource AWS::ApiGatewayV2::Api */ -export class WebSocketApi extends Construct implements IWebSocketApi { +export class WebSocketApi extends Resource implements IWebSocketApi { /** * Creates a new imported API * * @param scope scope of this imported resource * @param id identifier of the resource - * @param apiId Identifier of the API + * @param webSocketApiId Identifier of the API */ - public static fromApiId(scope: Construct, id: string, apiId: string): IWebSocketApi { - class Import extends Construct implements IWebSocketApi { - public readonly apiId = apiId; + public static fromApiId(scope: Construct, id: string, webSocketApiId: string): IWebSocketApi { + class Import extends Resource implements IWebSocketApi { + public readonly webSocketApiId = webSocketApiId; } return new Import(scope, id); @@ -182,20 +184,65 @@ export class WebSocketApi extends Construct implements IWebSocketApi { /** * The ID of this API Gateway Api. */ - public readonly apiId: string; + public readonly webSocketApiId: string; - protected readonly resource: Api; + /** + * API Gateway stage that points to the latest deployment (if defined). + */ + public deploymentStage?: WebSocketStage; + + /** + * API Gateway deployment (if defined). + */ + public deployment?: WebSocketDeployment; + + protected resource: CfnApi; constructor(scope: Construct, id: string, props: WebSocketApiProps) { - super(scope, id); + super(scope, id, { + physicalName: props.apiName || id, + }); - this.resource = new Api(this, 'Default', { - ...props, - apiName: props.apiName ?? id, - protocolType: ProtocolType.WEBSOCKET, + this.resource = new CfnApi(this, 'Resource', { + name: this.physicalName, + apiKeySelectionExpression: props.apiKeySelectionExpression, + description: props.description, + disableSchemaValidation: props.disableSchemaValidation, + failOnWarnings: props.failOnWarnings, + protocolType: 'WEBSOCKET', + routeSelectionExpression: props.routeSelectionExpression, + // TODO: tags: props.tags, + version: props.version, }); - this.apiId = this.resource.apiId; + this.webSocketApiId = this.resource.ref; + + const deploy = props.deploy === undefined ? true : props.deploy; + if (deploy) { + const stageName = (props.deployOptions && props.deployOptions.stageName) || 'prod'; + + this.deployment = new WebSocketDeployment(this, 'Deployment', { + api: this, + description: 'Automatically created by the Api construct', + }); + + this.deploymentStage = new WebSocketStage(this, 'DefaultStage', { + deployment: this.deployment, + api: this, + stageName, + description: 'Automatically created by the Api construct', + accessLogSettings: props.deployOptions?.accessLogSettings, + autoDeploy: props.deployOptions?.autoDeploy, + clientCertificateId: props.deployOptions?.clientCertificateId, + defaultRouteSettings: props.deployOptions?.defaultRouteSettings, + routeSettings: props.deployOptions?.routeSettings, + stageVariables: props.deployOptions?.stageVariables, + }); + } else { + if (props.deployOptions) { + throw new Error('Cannot set "deployOptions" if "deploy" is disabled'); + } + } if (props.defaultTarget !== undefined) { let integration; @@ -225,7 +272,7 @@ export class WebSocketApi extends Construct implements IWebSocketApi { throw new Error('You must specify an ARN, a URL, "MOCK", or a Lambda Function'); } - this.addRoute(KnownRouteKey.DEFAULT, integration, {}); + this.addRoute(WebSocketKnownRouteKey.DEFAULT, integration, {}); } } @@ -235,13 +282,13 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param id the id of this integration * @param props the properties for this integration */ - public addLambdaIntegration(id: string, props: WebSocketApiLambdaIntegrationOptions): LambdaIntegration { - const integration = new LambdaIntegration(this, `${id}.lambda.integration`, { + public addLambdaIntegration(id: string, props: WebSocketLambdaIntegrationOptions): WebSocketLambdaIntegration { + const integration = new WebSocketLambdaIntegration(this, `${id}.lambda.integration`, { ...props, - api: this.resource, + api: this, }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + if (this.deployment !== undefined) { + this.deployment.registerDependency(integration.node.defaultChild as CfnResource); } return integration; } @@ -252,13 +299,13 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param id the id of this integration * @param props the properties for this integration */ - public addHttpIntegration(id: string, props: WebSocketApiHttpIntegrationOptions): HttpIntegration { - const integration = new HttpIntegration(this, `${id}.http.integration`, { + public addHttpIntegration(id: string, props: WebSocketHttpIntegrationOptions): WebSocketHttpIntegration { + const integration = new WebSocketHttpIntegration(this, `${id}.http.integration`, { ...props, - api: this.resource, + api: this, }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + if (this.deployment !== undefined) { + this.deployment.registerDependency(integration.node.defaultChild as CfnResource); } return integration; } @@ -269,13 +316,13 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param id the id of this integration * @param props the properties for this integration */ - public addServiceIntegration(id: string, props: WebSocketApiServiceIntegrationOptions): ServiceIntegration { - const integration = new ServiceIntegration(this, `${id}.service.integration`, { + public addServiceIntegration(id: string, props: WebSocketServiceIntegrationOptions): WebSocketServiceIntegration { + const integration = new WebSocketServiceIntegration(this, `${id}.service.integration`, { ...props, - api: this.resource, + api: this, }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + if (this.deployment !== undefined) { + this.deployment.registerDependency(integration.node.defaultChild as CfnResource); } return integration; } @@ -286,13 +333,13 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param id the id of this integration * @param props the properties for this integration */ - public addMockIntegration(id: string, props: WebSocketApiMockIntegrationOptions): MockIntegration { - const integration = new MockIntegration(this, `${id}.service.integration`, { + public addMockIntegration(id: string, props: WebSocketMockIntegrationOptions): WebSocketMockIntegration { + const integration = new WebSocketMockIntegration(this, `${id}.service.integration`, { ...props, - api: this.resource, + api: this, }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(integration.resource.node.defaultChild as CfnResource); + if (this.deployment !== undefined) { + this.deployment.registerDependency(integration.node.defaultChild as CfnResource); } return integration; } @@ -304,18 +351,15 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param integration [disable-awslint:ref-via-interface] the integration to use for this route * @param props the properties for this route */ - public addRoute( - key: WebSocketRouteName, - integration: Integration | LambdaIntegration | HttpIntegration | ServiceIntegration, - props?: WebSocketApiRouteOptions): Route { - const route = new Route(this, `${key}.route`, { + public addRoute(key: string, integration: WebSocketIntegration, props?: WebSocketRouteOptions): WebSocketRoute { + const route = new WebSocketRoute(this, `${key}.route`, { ...props, - api: this.resource, - integration: ((integration instanceof Integration) ? integration : integration.resource), + api: this, + integration, key, }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(route.node.defaultChild as CfnResource); + if (this.deployment !== undefined) { + this.deployment.registerDependency(route.node.defaultChild as CfnResource); } return route; } @@ -325,15 +369,15 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param schema The model schema * @param props The model integration options */ - public addModel(schema: JsonSchema, props?: ModelOptions): Model { - const model = new Model(this, `Model.${schema.title}`, { + public addModel(schema: JsonSchema, props?: WebSocketModelOptions): WebSocketModel { + const model = new WebSocketModel(this, `Model.${schema.title}`, { ...props, modelName: schema.title, - api: this.resource, + api: this, schema, }); - if (this.resource.deployment !== undefined) { - this.resource.deployment.registerDependency(model.node.defaultChild as CfnResource); + if (this.deployment !== undefined) { + this.deployment.registerDependency(model.node.defaultChild as CfnResource); } return model; } @@ -344,8 +388,19 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param route The route for this ARN ('*' if not defined) * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') */ - public executeApiArn(route?: IRoute, stage?: IStage) { - return this.resource.executeApiArn(route, stage); + public executeApiArn(route?: string, stage?: IStage) { + const stack = Stack.of(this); + const apiId = this.webSocketApiId; + const routeKey = ((route === undefined) ? '*' : route); + const stageName = ((stage === undefined) ? + ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : + stage.stageName); + return stack.formatArn({ + service: 'execute-api', + resource: apiId, + sep: '/', + resourceName: `${stageName}/${routeKey}`, + }); } /** @@ -355,7 +410,17 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') */ public connectionsApiArn(connectionId: string = '*', stage?: IStage) { - return this.resource.webSocketConnectionsApiArn(connectionId, stage); + const stack = Stack.of(this); + const apiId = this.webSocketApiId; + const stageName = ((stage === undefined) ? + ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : + stage.stageName); + return stack.formatArn({ + service: 'execute-api', + resource: apiId, + sep: '/', + resourceName: `${stageName}/POST/${connectionId}`, + }); } /** @@ -365,7 +430,17 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param stage The stage for this URL (if not defined, defaults to the deployment stage) */ public connectionsUrl(stage?: IStage): string { - return this.resource.webSocketConnectionsUrl(stage); + const stack = Stack.of(this); + let stageName: string | undefined; + if (stage === undefined) { + if (this.deploymentStage === undefined) { + throw Error('No stage defined for this Api'); + } + stageName = this.deploymentStage.stageName; + } else { + stageName = stage.stageName; + } + return `https://${this.webSocketApiId}.execute-api.${stack.region}.amazonaws.com/${stageName}/@connections`; } /** @@ -375,6 +450,16 @@ export class WebSocketApi extends Construct implements IWebSocketApi { * @param stage The stage for this URL (if not defined, defaults to the deployment stage) */ public clientUrl(stage?: IStage): string { - return this.resource.webSocketClientUrl(stage); + const stack = Stack.of(this); + let stageName: string | undefined; + if (stage === undefined) { + if (this.deploymentStage === undefined) { + throw Error('No stage defined for this Api'); + } + stageName = this.deploymentStage.stageName; + } else { + stageName = stage.stageName; + } + return `wss://${this.webSocketApiId}.execute-api.${stack.region}.amazonaws.com/${stageName}`; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/authorizer.ts similarity index 65% rename from packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/authorizer.ts index a64a13a0c9932..fa684074c2a0f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/authorizer.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/authorizer.ts @@ -1,75 +1,16 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, Resource } from '@aws-cdk/core'; -import { IApi } from './api'; -import { CfnAuthorizer } from './apigatewayv2.generated'; +import { CfnAuthorizer } from '../apigatewayv2.generated'; +import { IAuthorizer } from '../common/authorizer'; -/** - * The authorizer type - */ -export enum AuthorizerType { - /** - * For WebSocket APIs, specify REQUEST for a Lambda function using incoming request parameters - */ - REQUEST = 'REQUEST', - - /** - * For HTTP APIs, specify JWT to use JSON Web Tokens - */ - JWT = 'JWT' -} - -/** - * Specifies the configuration of a `JWT` authorizer. - * - * Required for the `JWT` authorizer type. - * - * Supported only for HTTP APIs. - */ -export interface JwtConfiguration { - /** - * A list of the intended recipients of the `JWT`. - * - * A valid `JWT` must provide an `aud` that matches at least one entry in this list. - * - * See RFC 7519. - * - * Supported only for HTTP APIs - * - * @default - no specified audience - */ - readonly audience?: string[]; - - /** - * The base domain of the identity provider that issues JSON Web Tokens. - * - * For example, an Amazon Cognito user pool has the following format: `https://cognito-idp.{region}.amazonaws.com/{userPoolId}`. - * - * Required for the `JWT` authorizer type. - * - * Supported only for HTTP APIs. - * - * @default - no issuer - */ - readonly issuer?: string; -} - -/** - * Defines the contract for an Api Gateway V2 Authorizer. - */ -export interface IAuthorizer extends IResource { - /** - * The ID of this API Gateway Api Mapping. - * @attribute - */ - readonly authorizerId: string; -} +import { IWebSocketApi } from './api'; /** * Defines the properties required for defining an Api Gateway V2 Authorizer. * * This interface is used by the helper methods in `Api` and the sub-classes */ -export interface AuthorizerOptions { +export interface WebSocketAuthorizerOptions { /** * Specifies the required credentials as an IAM role for API Gateway to invoke the authorizer. * @@ -81,11 +22,6 @@ export interface AuthorizerOptions { */ readonly authorizerCredentialsArn?: string; - /** - * The authorizer type. - */ - readonly authorizerType: AuthorizerType; - /** * The authorizer's Uniform Resource Identifier (URI). * @@ -115,17 +51,6 @@ export interface AuthorizerOptions { */ readonly identitySource?: string[]; - /** - * The JWTConfiguration property specifies the configuration of a JWT authorizer. - * - * Required for the JWT authorizer type. - * - * Supported only for HTTP APIs. - * - * @default - only required for HTTP APIs - */ - readonly jwtConfiguration?: JwtConfiguration; - /** * The name of the authorizer. */ @@ -135,11 +60,11 @@ export interface AuthorizerOptions { /** * Defines the properties required for defining an Api Gateway V2 Authorizer. */ -export interface AuthorizerProps extends AuthorizerOptions { +export interface WebSocketAuthorizerProps extends WebSocketAuthorizerOptions { /** * Defines the api for this deployment. */ - readonly api: IApi; + readonly api: IWebSocketApi; } /** @@ -148,8 +73,10 @@ export interface AuthorizerProps extends AuthorizerOptions { * A custom domain name can have multiple API mappings, but the paths can't overlap. * * A custom domain can map only to APIs of the same protocol type. + * + * @resource AWS::ApiGatewayV2::Authorizer */ -export class Authorizer extends Resource implements IAuthorizer { +export class WebSocketAuthorizer extends Resource implements IAuthorizer { /** * Creates a new imported API @@ -173,19 +100,18 @@ export class Authorizer extends Resource implements IAuthorizer { protected resource: CfnAuthorizer; - constructor(scope: Construct, id: string, props: AuthorizerProps) { + constructor(scope: Construct, id: string, props: WebSocketAuthorizerProps) { super(scope, id); this.resource = new CfnAuthorizer(this, 'Resource', { identitySource: (props.identitySource ? props.identitySource : []), - apiId: props.api.apiId, + apiId: props.api.webSocketApiId, name: props.authorizerName, - authorizerType: props.authorizerType, + authorizerType: 'REQUEST', authorizerCredentialsArn: props.authorizerCredentialsArn, // TODO: authorizerResultTtlInSeconds: props.authorizerResultTtl.toSeconds(), authorizerUri: props.authorizerUri, // TODO: identityValidationExpression: props.identityValidationExpression - jwtConfiguration: props.jwtConfiguration, }); this.authorizerId = this.resource.ref; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/deployment.ts similarity index 85% rename from packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/deployment.ts index 567c5ed987255..0452255842bae 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/deployment.ts @@ -1,12 +1,12 @@ import { CfnResource, Construct, IResource, Lazy, Resource } from '@aws-cdk/core'; +import { CfnDeployment } from '../apigatewayv2.generated'; -import { IApi } from './api'; -import { CfnDeployment } from './apigatewayv2.generated'; +import { IWebSocketApi } from './api'; /** * Defines the contract for an Api Gateway V2 Deployment. */ -export interface IDeployment extends IResource { +export interface IWebSocketDeployment extends IResource { /** * The ID of this API Gateway Deployment. * @attribute @@ -17,11 +17,11 @@ export interface IDeployment extends IResource { /** * Defines the properties required for defining an Api Gateway V2 Deployment. */ -export interface DeploymentProps { +export interface WebSocketDeploymentProps { /** * Defines the api for this deployment. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * A description for this Deployment. @@ -67,8 +67,10 @@ export interface DeploymentProps { * resources are created, which means that it will represent a "half-baked" * model. Use the `registerDependency(dep)` method to circumvent that. This is done * automatically for the `api.latestDeployment` deployment. + * + * @resource AWS::ApiGatewayV2::Deployment */ -export class Deployment extends Resource implements IDeployment { +export class WebSocketDeployment extends Resource implements IWebSocketDeployment { /** * Creates a new imported API Deployment * @@ -76,8 +78,8 @@ export class Deployment extends Resource implements IDeployment { * @param id identifier of the resource * @param deploymentId Identifier of the Deployment */ - public static fromDeploymentId(scope: Construct, id: string, deploymentId: string): IDeployment { - class Import extends Resource implements IDeployment { + public static fromDeploymentId(scope: Construct, id: string, deploymentId: string): IWebSocketDeployment { + class Import extends Resource implements IWebSocketDeployment { public readonly deploymentId = deploymentId; } @@ -91,11 +93,11 @@ export class Deployment extends Resource implements IDeployment { protected resource: CfnDeployment; - constructor(scope: Construct, id: string, props: DeploymentProps) { + constructor(scope: Construct, id: string, props: WebSocketDeploymentProps) { super(scope, id); this.resource = new CfnDeployment(this, 'Resource', { - apiId: props.api.apiId, + apiId: props.api.webSocketApiId, description: props.description, stageName: props.stageName, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/index.ts new file mode 100644 index 0000000000000..a145983c300bd --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/index.ts @@ -0,0 +1,11 @@ +export * from './api'; +export * from './api-mapping'; +export * from './authorizer'; +export * from './deployment'; +export * from './integration'; +export * from './integration-response'; +export * from './integrations'; +export * from './model'; +export * from './route'; +export * from './route-response'; +export * from './stage'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts similarity index 77% rename from packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts index e4db256df349c..fcb19668c3f8b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integration-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts @@ -1,13 +1,14 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { IApi } from './api'; -import { CfnIntegrationResponse } from './apigatewayv2.generated'; -import { ContentHandlingStrategy, IIntegration, KnownTemplateKey } from './integration'; +import { CfnIntegrationResponse } from '../apigatewayv2.generated'; +import { IIntegration } from '../common/integration'; + +import { IWebSocketApi } from './api'; /** * Defines a set of common response patterns known to the system */ -export enum KnownIntegrationResponseKey { +export enum WebSocketKnownIntegrationResponseKey { /** * Default response, when no other pattern matches */ @@ -27,7 +28,7 @@ export enum KnownIntegrationResponseKey { /** * Defines the contract for an Api Gateway V2 Deployment. */ -export interface IIntegrationResponse extends IResource { +export interface IWebSocketIntegrationResponse extends IResource { } /** @@ -35,7 +36,7 @@ export interface IIntegrationResponse extends IResource { * * This interface is used by the helper methods in `Integration` */ -export interface IntegrationResponseOptions { +export interface WebSocketIntegrationResponseOptions { /** * Specifies how to handle response payload content type conversions. * @@ -43,7 +44,7 @@ export interface IntegrationResponseOptions { * * @default - Pass through unmodified */ - readonly contentHandlingStrategy?: ContentHandlingStrategy | string; + readonly contentHandlingStrategy?: string; /** * A key-value map specifying response parameters that are passed to the method response from the backend. @@ -76,17 +77,17 @@ export interface IntegrationResponseOptions { * * @default - no template selected */ - readonly templateSelectionExpression?: KnownTemplateKey | string; + readonly templateSelectionExpression?: string; } /** * Defines the properties required for defining an Api Gateway V2 Integration. */ -export interface IntegrationResponseProps extends IntegrationResponseOptions { +export interface WebSocketIntegrationResponseProps extends WebSocketIntegrationResponseOptions { /** * Defines the api for this response. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * Defines the parent integration for this response. @@ -96,19 +97,21 @@ export interface IntegrationResponseProps extends IntegrationResponseOptions { /** * The integration response key. */ - readonly key: KnownIntegrationResponseKey | string; + readonly key: string; } /** * A response for an integration for an API in Amazon API Gateway v2. + * + * @resource AWS::ApiGatewayV2::IntegrationResponse */ -export class IntegrationResponse extends Resource implements IIntegrationResponse { +export class WebSocketIntegrationResponse extends Resource implements IWebSocketIntegrationResponse { protected resource: CfnIntegrationResponse; - constructor(scope: Construct, id: string, props: IntegrationResponseProps) { + constructor(scope: Construct, id: string, props: WebSocketIntegrationResponseProps) { super(scope, id); this.resource = new CfnIntegrationResponse(this, 'Resource', { - apiId: props.api.apiId, + apiId: props.api.webSocketApiId, integrationId: props.integration.integrationId, integrationResponseKey: props.key, contentHandlingStrategy: props.contentHandlingStrategy, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts similarity index 64% rename from packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts index 9a71d219bacdf..1297b9e1553a8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts @@ -1,13 +1,15 @@ -import { Construct, Duration, IResource, Resource } from '@aws-cdk/core'; +import { Construct, Duration, Resource } from '@aws-cdk/core'; -import { IApi } from './api'; -import { CfnIntegration } from './apigatewayv2.generated'; -import { IntegrationResponse, IntegrationResponseOptions, KnownIntegrationResponseKey } from './integration-response'; +import { CfnIntegration } from '../apigatewayv2.generated'; +import { IIntegration } from '../common/integration'; + +import { IWebSocketApi } from './api'; +import { WebSocketIntegrationResponse, WebSocketIntegrationResponseOptions } from './integration-response'; /** * The type of the network connection to the integration endpoint. */ -export enum ConnectionType { +export enum WebSocketConnectionType { /** * Internet connectivity through the public routable internet */ @@ -22,7 +24,7 @@ export enum ConnectionType { /** * The integration type of an integration. */ -export enum IntegrationType { +export enum WebSocketIntegrationType { /** * Integration of the route or method request with an AWS service action, including the Lambda function-invoking action. * With the Lambda function-invoking action, this is referred to as the Lambda custom integration. @@ -60,7 +62,7 @@ export enum IntegrationType { * If this property is not defined, the response payload will be passed through from the integration response * to the route response or method response without modification. */ -export enum ContentHandlingStrategy { +export enum WebSocketContentHandlingStrategy { /** * Converts a response payload from a Base64-encoded string to the corresponding binary blob */ @@ -77,7 +79,7 @@ export enum ContentHandlingStrategy { * Content-Type header in the request, and the available mapping templates * specified as the requestTemplates property on the Integration resource. */ -export enum PassthroughBehavior { +export enum WebSocketPassthroughBehavior { /** * Passes the request body for unmapped content types through to the * integration backend without transformation @@ -92,69 +94,39 @@ export enum PassthroughBehavior { /** * Rejects unmapped content types with an HTTP 415 Unsupported Media Type response */ - NEVER = 'NEVER' + NEVER = 'NEVER', } /** * Defines a set of common template patterns known to the system */ -export enum KnownTemplateKey { +export enum WebSocketKnownTemplateKey { /** * Default template, when no other pattern matches */ - DEFAULT = '$default' + DEFAULT = '$default', } /** - * Specifies the integration's HTTP method type (only GET is supported for WebSocket) + * Payload format version for lambda proxy integration + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html */ -export enum HttpApiIntegrationMethod { - /** - * All HTTP Methods are supported - */ - ANY = 'ANY', - - /** - * GET HTTP Method - */ - GET = 'GET', - - /** - * POST HTTP Method - */ - POST = 'POST', - - /** - * PUT HTTP Method - */ - PUT = 'PUT', - - /** - * DELETE HTTP Method - */ - DELETE = 'DELETE', - +export enum WebSocketPayloadFormatVersion { /** - * OPTIONS HTTP Method + * Version 1.0 */ - OPTIONS = 'OPTIONS', - + VERSION_1_0 = '1.0', /** - * HEAD HTTP Method + * Version 2.0 */ - HEAD = 'HEAD', - - /** - * PATCH HTTP Method - */ - PATCH = 'PATCH' + VERSION_2_0 = '2.0', } /** * The TLS configuration for a private integration. If you specify a TLS configuration, * private integration traffic uses the HTTPS protocol. */ -export interface TlsConfig { +export interface WebSocketTlsConfig { /** * If you specify a server name, API Gateway uses it to verify the hostname on * the integration's certificate. @@ -165,29 +137,18 @@ export interface TlsConfig { readonly serverNameToVerify: string; } -/** - * Defines the contract for an Api Gateway V2 Deployment. - */ -export interface IIntegration extends IResource { - /** - * The ID of this API Gateway Integration. - * @attribute - */ - readonly integrationId: string; -} - /** * Defines the properties required for defining an Api Gateway V2 Integration. * * This interface is used by the helper methods in `Api` and the sub-classes */ -export interface BaseIntegrationOptions { +export interface WebSocketIntegrationOptions { /** * The type of the network connection to the integration endpoint. * * @default 'INTERNET' */ - readonly connectionType?: ConnectionType; + readonly connectionType?: WebSocketConnectionType; /** * Specifies the credentials required for the integration, if any. @@ -214,20 +175,13 @@ export interface BaseIntegrationOptions { * @default - timeout is 29 seconds for WebSocket APIs and 30 seconds for HTTP APIs. */ readonly timeout?: Duration; -} -/** - * Defines the properties required for defining an Api Gateway V2 Integration. - * - * This interface is used by the helper methods in `Api` and the sub-classes - */ -export interface IntegrationOptions extends BaseIntegrationOptions { /** * Specifies how to handle response payload content type conversions. * * @default - Pass through unmodified */ - readonly contentHandlingStrategy?: string; + readonly contentHandlingStrategy?: WebSocketContentHandlingStrategy; /** * Specifies the pass-through behavior for incoming requests based on the `Content-Type` header in the request, @@ -235,7 +189,7 @@ export interface IntegrationOptions extends BaseIntegrationOptions { * * @default - the response payload will be passed through from the integration response to the route response or method response unmodified */ - readonly passthroughBehavior?: string; + readonly passthroughBehavior?: WebSocketPassthroughBehavior; /** * A key-value map specifying request parameters that are passed from the method request to the backend. @@ -273,11 +227,11 @@ export interface IntegrationOptions extends BaseIntegrationOptions { // TODO: readonly connectionId?: string; /** - * Specifies the format of the payload sent to an integration.. + * Specifies the format of the payload sent to an integration. * * @default '1.0' */ - readonly payloadFormatVersion?: string; + readonly payloadFormatVersion?: WebSocketPayloadFormatVersion; /** * The TlsConfig property specifies the TLS configuration for a private integration. @@ -285,108 +239,22 @@ export interface IntegrationOptions extends BaseIntegrationOptions { * * @default - no private TLS configuration */ - readonly tlsConfig?: TlsConfig; - - /** - * Specifies the integration's HTTP method type. - * - * @default - 'ANY' - */ - readonly integrationMethod?: string; + readonly tlsConfig?: WebSocketTlsConfig; } /** * Defines the properties required for defining an Api Gateway V2 Integration. - * - * This interface is used by the helper methods in `Api` and the sub-classes */ -export interface WebSocketApiIntegrationOptions extends BaseIntegrationOptions { - /** - * Specifies how to handle response payload content type conversions. - * - * @default - Pass through unmodified - */ - readonly contentHandlingStrategy?: ContentHandlingStrategy | string; - - /** - * Specifies the pass-through behavior for incoming requests based on the `Content-Type` header in the request, - * and the available mapping templates specified as the `requestTemplates` property on the `Integration` resource. - * - * @default - the response payload will be passed through from the integration response to the route response or method response unmodified - */ - readonly passthroughBehavior?: PassthroughBehavior | string; - - /** - * A key-value map specifying request parameters that are passed from the method request to the backend. - * The key is an integration request parameter name and the associated value is a method request parameter value or static value - * that must be enclosed within single quotes and pre-encoded as required by the backend. - * - * The method request parameter value must match the pattern of `method.request.{location}.{name}`, where `{location}` is - * `querystring`, `path`, or `header`; and `{name}` must be a valid and unique method request parameter name. - * - * @default - no parameter used - */ - readonly requestParameters?: { [key: string]: string }; - - /** - * Represents a map of Velocity templates that are applied on the request payload based on the value of - * the `Content-Type` header sent by the client. The content type value is the key in this map, and the - * template is the value. - * - * @default - no templates used - */ - readonly requestTemplates?: { [key: string]: string }; - - /** - * The template selection expression for the integration. - * - * @default - no template selected - */ - readonly templateSelectionExpression?: KnownTemplateKey | string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Integration. - * - * This interface is used by the helper methods in `Api` and the sub-classes - */ -export interface HttpApiIntegrationOptions extends BaseIntegrationOptions { - /** - * The ID of the VPC link for a private integration. - * - * @default - don't use a VPC link - */ - // TODO: readonly connectionId?: string; - - /** - * Specifies the format of the payload sent to an integration.. - * - * @default '1.0' - */ - readonly payloadFormatVersion?: string; - - /** - * The TlsConfig property specifies the TLS configuration for a private integration. - * If you specify a TLS configuration, private integration traffic uses the HTTPS protocol. - * - * @default - no private TLS configuration - */ - readonly tlsConfig?: TlsConfig; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Integration. - */ -export interface IntegrationProps extends IntegrationOptions { +export interface WebSocketIntegrationProps extends WebSocketIntegrationOptions { /** * Defines the api for this integration. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * The integration type of an integration. */ - readonly type: IntegrationType | string; + readonly type: WebSocketIntegrationType; /** * For a Lambda integration, specify the URI of a Lambda function. @@ -408,8 +276,10 @@ export interface IntegrationProps extends IntegrationOptions { * An integration for an API in Amazon API Gateway v2. * * Use `addResponse` and `addRoute` to configure integration. + * + * @resource AWS::ApiGatewayV2::Integration */ -export class Integration extends Resource implements IIntegration { +export class WebSocketIntegration extends Resource implements IIntegration { /** * Creates a new imported API Integration * @@ -430,10 +300,10 @@ export class Integration extends Resource implements IIntegration { */ public readonly integrationId: string; - protected api: IApi; + protected api: IWebSocketApi; protected resource: CfnIntegration; - constructor(scope: Construct, id: string, props: IntegrationProps) { + constructor(scope: Construct, id: string, props: WebSocketIntegrationProps) { super(scope, id); this.api = props.api; this.resource = new CfnIntegration(this, 'Resource', { @@ -444,7 +314,6 @@ export class Integration extends Resource implements IIntegration { contentHandlingStrategy: props.contentHandlingStrategy, credentialsArn: props.credentialsArn, description: props.description, - integrationMethod: props.integrationMethod, passthroughBehavior: props.passthroughBehavior, payloadFormatVersion: props.payloadFormatVersion, requestParameters: props.requestParameters, @@ -452,7 +321,7 @@ export class Integration extends Resource implements IIntegration { templateSelectionExpression: props.templateSelectionExpression, tlsConfig: props.tlsConfig, timeoutInMillis: (props.timeout ? props.timeout.toMilliseconds() : undefined), - apiId: props.api.apiId, + apiId: props.api.webSocketApiId, }); this.integrationId = this.resource.ref; @@ -464,8 +333,8 @@ export class Integration extends Resource implements IIntegration { * @param key the key (predefined or not) that will select this response * @param props the properties for this response */ - public addResponse(key: KnownIntegrationResponseKey | string, props?: IntegrationResponseOptions): IntegrationResponse { - return new IntegrationResponse(this, `Response.${key}`, { + public addResponse(key: string, props?: WebSocketIntegrationResponseOptions): WebSocketIntegrationResponse { + return new WebSocketIntegrationResponse(this, `Response.${key}`, { ...props, api: this.api, integration: this, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/http.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/http.ts new file mode 100644 index 0000000000000..adad4cbb931a5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/http.ts @@ -0,0 +1,57 @@ +import { Construct } from '@aws-cdk/core'; + +import { IWebSocketApi } from '../api'; +import { WebSocketIntegration, WebSocketIntegrationOptions, WebSocketIntegrationType } from '../integration'; + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketHttpIntegrationOptions extends WebSocketIntegrationOptions { + /** + * The HTTP URL for this integration + */ + readonly url: string; + + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; +} + +/** + * Defines the properties required for defining an Api Gateway V2 HTTP Integration. + */ +export interface WebSocketHttpIntegrationProps extends WebSocketHttpIntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IWebSocketApi; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class WebSocketHttpIntegration extends WebSocketIntegration { + constructor(scope: Construct, id: string, props: WebSocketHttpIntegrationProps) { + super(scope, id, { + api: props.api, + connectionType: props.connectionType, + contentHandlingStrategy: props.contentHandlingStrategy, + credentialsArn: props.credentialsArn, + description: props.description, + passthroughBehavior: props.passthroughBehavior, + payloadFormatVersion: props.payloadFormatVersion, + requestParameters: props.requestParameters, + requestTemplates: props.requestTemplates, + templateSelectionExpression: props.templateSelectionExpression, + timeout: props.timeout, + tlsConfig: props.tlsConfig, + type: props.proxy ? WebSocketIntegrationType.HTTP_PROXY : WebSocketIntegrationType.HTTP, + uri: props.url, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/index.ts new file mode 100644 index 0000000000000..0960cced9e7a7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/index.ts @@ -0,0 +1,4 @@ +export * from './http'; +export * from './lambda'; +export * from './mock'; +export * from './service'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts new file mode 100644 index 0000000000000..cf7e741d996c6 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts @@ -0,0 +1,71 @@ +import { ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { Construct, Stack } from '@aws-cdk/core'; + +import { IWebSocketApi, WebSocketApi } from '../api'; +import { WebSocketIntegration, WebSocketIntegrationOptions, WebSocketIntegrationType } from '../integration'; + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketLambdaIntegrationOptions extends WebSocketIntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; + + /** + * The Lambda function handler for this integration + */ + readonly handler: IFunction; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + */ +export interface WebSocketLambdaIntegrationProps extends WebSocketLambdaIntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IWebSocketApi; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class WebSocketLambdaIntegration extends WebSocketIntegration { + constructor(scope: Construct, id: string, props: WebSocketLambdaIntegrationProps) { + const stack = Stack.of(scope); + + // This is not a standard ARN as it does not have the account-id part in it + const uri = `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`; + super(scope, id, { + api: props.api, + connectionType: props.connectionType, + contentHandlingStrategy: props.contentHandlingStrategy, + credentialsArn: props.credentialsArn, + description: props.description, + passthroughBehavior: props.passthroughBehavior, + payloadFormatVersion: props.payloadFormatVersion, + requestParameters: props.requestParameters, + requestTemplates: props.requestTemplates, + templateSelectionExpression: props.templateSelectionExpression, + timeout: props.timeout, + tlsConfig: props.tlsConfig, + type: props.proxy ? WebSocketIntegrationType.AWS_PROXY : WebSocketIntegrationType.AWS, + uri, + }); + + if (props.api instanceof WebSocketApi) { + const sourceArn = props.api.executeApiArn(); + props.handler.addPermission(`ApiPermission.${this.node.uniqueId}`, { + principal: new ServicePrincipal('apigateway.amazonaws.com'), + sourceArn, + }); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/mock.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/mock.ts new file mode 100644 index 0000000000000..720211a9d7ea0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/mock.ts @@ -0,0 +1,46 @@ +import { Construct } from '@aws-cdk/core'; + +import { IWebSocketApi } from '../api'; +import { WebSocketIntegration, WebSocketIntegrationOptions, WebSocketIntegrationType } from '../integration'; + +/** + * Defines the properties required for defining an Api Gateway V2 Mock Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketMockIntegrationOptions extends WebSocketIntegrationOptions { +} + +/** + * Defines the properties required for defining an Api Gateway V2 Mock Integration. + */ +export interface WebSocketMockIntegrationProps extends WebSocketMockIntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IWebSocketApi; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class WebSocketMockIntegration extends WebSocketIntegration { + constructor(scope: Construct, id: string, props: WebSocketMockIntegrationProps) { + super(scope, id, { + api: props.api, + connectionType: props.connectionType, + contentHandlingStrategy: props.contentHandlingStrategy, + credentialsArn: props.credentialsArn, + description: props.description, + passthroughBehavior: props.passthroughBehavior, + payloadFormatVersion: props.payloadFormatVersion, + requestParameters: props.requestParameters, + requestTemplates: props.requestTemplates, + templateSelectionExpression: props.templateSelectionExpression, + timeout: props.timeout, + tlsConfig: props.tlsConfig, + type: WebSocketIntegrationType.MOCK, + uri: '', + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/service.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/service.ts new file mode 100644 index 0000000000000..e0f6e6c26d21e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/service.ts @@ -0,0 +1,57 @@ +import { Construct } from '@aws-cdk/core'; + +import { IWebSocketApi } from '../api'; +import { WebSocketIntegration, WebSocketIntegrationOptions, WebSocketIntegrationType } from '../integration'; + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + * + * This interface is used by the helper methods in `Integration` + */ +export interface WebSocketServiceIntegrationOptions extends WebSocketIntegrationOptions { + /** + * Defines if this integration is a proxy integration or not. + * + * @default false + */ + readonly proxy?: boolean; + + /** + * The ARN of the target service for this integration + */ + readonly arn: string; +} + +/** + * Defines the properties required for defining an Api Gateway V2 Lambda Integration. + */ +export interface WebSocketServiceIntegrationProps extends WebSocketServiceIntegrationOptions { + /** + * Defines the api for this integration. + */ + readonly api: IWebSocketApi; +} + +/** + * An AWS Lambda integration for an API in Amazon API Gateway v2. + */ +export class WebSocketServiceIntegration extends WebSocketIntegration { + constructor(scope: Construct, id: string, props: WebSocketServiceIntegrationProps) { + super(scope, id, { + api: props.api, + connectionType: props.connectionType, + contentHandlingStrategy: props.contentHandlingStrategy, + credentialsArn: props.credentialsArn, + description: props.description, + passthroughBehavior: props.passthroughBehavior, + payloadFormatVersion: props.payloadFormatVersion, + requestParameters: props.requestParameters, + requestTemplates: props.requestTemplates, + templateSelectionExpression: props.templateSelectionExpression, + timeout: props.timeout, + tlsConfig: props.tlsConfig, + type: props.proxy ? WebSocketIntegrationType.AWS_PROXY : WebSocketIntegrationType.AWS, + uri: props.arn, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts similarity index 75% rename from packages/@aws-cdk/aws-apigatewayv2/lib/model.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts index 615032523c4c7..0201b8be725ac 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/model.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts @@ -1,13 +1,12 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; - -import { IApi } from './api'; -import { CfnModel } from './apigatewayv2.generated'; -import { JsonSchema, JsonSchemaMapper } from './json-schema'; +import { CfnModel } from '../apigatewayv2.generated'; +import { JsonSchema, JsonSchemaMapper } from '../common/json-schema'; +import { IWebSocketApi } from './api'; /** * Defines a set of common model patterns known to the system */ -export enum KnownModelKey { +export enum WebSocketKnownModelKey { /** * Default model, when no other pattern matches */ @@ -22,7 +21,7 @@ export enum KnownModelKey { /** * Defines a set of common content types for APIs */ -export enum KnownContentTypes { +export enum WebSocketKnownContentTypes { /** * JSON request or response (default) */ @@ -48,7 +47,7 @@ export enum KnownContentTypes { /** * Defines the attributes for an Api Gateway V2 Model. */ -export interface ModelAttributes { +export interface WebSocketModelAttributes { /** * The ID of this API Gateway Model. */ @@ -63,7 +62,7 @@ export interface ModelAttributes { /** * Defines the contract for an Api Gateway V2 Model. */ -export interface IModel extends IResource { +export interface IWebSocketModel extends IResource { /** * The ID of this API Gateway Model. * @attribute @@ -82,13 +81,13 @@ export interface IModel extends IResource { * * This interface is used by the helper methods in `Api` */ -export interface ModelOptions { +export interface WebSocketModelOptions { /** * The content-type for the model, for example, `application/json`. * * @default "application/json" */ - readonly contentType?: KnownContentTypes | string; + readonly contentType?: string; /** * The name of the model. @@ -108,11 +107,11 @@ export interface ModelOptions { /** * Defines the properties required for defining an Api Gateway V2 Model. */ -export interface ModelProps extends ModelOptions { +export interface WebSocketModelProps extends WebSocketModelOptions { /** * Defines the api for this response. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * The schema for the model. For `application/json` models, this should be JSON schema draft 4 model. @@ -122,8 +121,10 @@ export interface ModelProps extends ModelOptions { /** * A model for an API in Amazon API Gateway v2. + * + * @resource AWS::ApiGatewayV2::Model */ -export class Model extends Resource implements IModel { +export class WebSocketModel extends Resource implements IWebSocketModel { /** * Creates a new imported Model * @@ -131,8 +132,8 @@ export class Model extends Resource implements IModel { * @param id identifier of the resource * @param attrs attributes of the API Model */ - public static fromModelAttributes(scope: Construct, id: string, attrs: ModelAttributes): IModel { - class Import extends Resource implements IModel { + public static fromModelAttributes(scope: Construct, id: string, attrs: WebSocketModelAttributes): IWebSocketModel { + class Import extends Resource implements IWebSocketModel { public readonly modelId = attrs.modelId; public readonly modelName = attrs.modelName; } @@ -144,15 +145,15 @@ export class Model extends Resource implements IModel { public readonly modelName: string; protected resource: CfnModel; - constructor(scope: Construct, id: string, props: ModelProps) { + constructor(scope: Construct, id: string, props: WebSocketModelProps) { super(scope, id, { physicalName: props.modelName || id, }); this.modelName = this.physicalName; this.resource = new CfnModel(this, 'Resource', { - contentType: props.contentType || KnownContentTypes.JSON, - apiId: props.api.apiId, + contentType: props.contentType || WebSocketKnownContentTypes.JSON, + apiId: props.api.webSocketApiId, name: this.modelName, schema: JsonSchemaMapper.toCfnJsonSchema(props.schema), }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts similarity index 64% rename from packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts index 5eb7155938127..a645165db60f3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts @@ -1,14 +1,14 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnRouteResponse } from '../apigatewayv2.generated'; +import { IRoute } from '../common/route'; -import { IApi } from './api'; -import { CfnRouteResponse } from './apigatewayv2.generated'; -import { IModel, KnownModelKey } from './model'; -import { IRoute } from './route'; +import { IWebSocketApi } from './api'; +import { IWebSocketModel } from './model'; /** * Defines a set of common response patterns known to the system */ -export enum KnownRouteResponseKey { +export enum WebSocketKnownRouteResponseKey { /** * Default response, when no other pattern matches */ @@ -28,7 +28,12 @@ export enum KnownRouteResponseKey { /** * Defines the contract for an Api Gateway V2 Route Response. */ -export interface IRouteResponse extends IResource { +export interface IWebSocketRouteResponse extends IResource { + /** + * The Route Response resource ID. + * @attribute + */ + readonly routeResponseId: string; } /** @@ -36,7 +41,7 @@ export interface IRouteResponse extends IResource { * * This interface is used by the helper methods in `Route` */ -export interface RouteResponseOptions { +export interface WebSocketRouteResponseOptions { /** * The route response parameters. * @@ -51,7 +56,7 @@ export interface RouteResponseOptions { * * @default - no models */ - readonly responseModels?: { [key: string]: IModel | string }; + readonly responseModels?: { [key: string]: IWebSocketModel }; /** * The model selection expression for the route response. @@ -60,13 +65,13 @@ export interface RouteResponseOptions { * * @default - no selection expression */ - readonly modelSelectionExpression?: KnownModelKey | string; + readonly modelSelectionExpression?: string; } /** * Defines the properties required for defining an Api Gateway V2 Route Response. */ -export interface RouteResponseProps extends RouteResponseOptions { +export interface WebSocketRouteResponseProps extends WebSocketRouteResponseOptions { /** * Defines the route for this response. */ @@ -75,21 +80,29 @@ export interface RouteResponseProps extends RouteResponseOptions { /** * Defines the api for this response. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * The route response key. */ - readonly key: KnownRouteResponseKey | string; + readonly key: string; } /** * A response for a route for an API in Amazon API Gateway v2. + * + * @resource AWS::ApiGatewayV2::RouteResponse */ -export class RouteResponse extends Resource implements IRouteResponse { +export class WebSocketRouteResponse extends Resource implements IWebSocketRouteResponse { + /** + * The Route Response resource ID. + * @attribute + */ + public readonly routeResponseId: string; + protected resource: CfnRouteResponse; - constructor(scope: Construct, id: string, props: RouteResponseProps) { + constructor(scope: Construct, id: string, props: WebSocketRouteResponseProps) { super(scope, id, { physicalName: props.key || id, }); @@ -102,12 +115,14 @@ export class RouteResponse extends Resource implements IRouteResponse { } this.resource = new CfnRouteResponse(this, 'Resource', { - apiId: props.api.apiId, + apiId: props.api.webSocketApiId, routeId: props.route.routeId, routeResponseKey: props.key, responseModels, modelSelectionExpression: props.modelSelectionExpression, responseParameters: props.responseParameters, }); + + this.routeResponseId = this.resource.ref; } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts similarity index 50% rename from packages/@aws-cdk/aws-apigatewayv2/lib/route.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts index 69b4129382e25..12241b4af8f38 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts @@ -1,34 +1,17 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, Resource } from '@aws-cdk/core'; -import { IApi } from './api'; -import { CfnRoute } from './apigatewayv2.generated'; -import { IAuthorizer } from './authorizer'; -import { IIntegration } from './integration'; -import { IModel, KnownModelKey } from './model'; -import { KnownRouteResponseKey, RouteResponse, RouteResponseOptions } from './route-response'; - -/** - * Available authorization providers for ApiGateway V2 HTTP APIs - */ -export enum HttpApiAuthorizationType { - /** - * Open access (Web Socket, HTTP APIs). - */ - NONE = 'NONE', - /** - * Use JSON Web Tokens (HTTP APIs). - */ - JWT = 'JWT' -} +import { CfnRoute } from '../apigatewayv2.generated'; +import { IAuthorizer } from '../common/authorizer'; +import { IIntegration } from '../common/integration'; +import { IRoute } from '../common/route'; +import { IWebSocketApi } from './api'; +import { IWebSocketModel } from './model'; +import { WebSocketRouteResponse, WebSocketRouteResponseOptions } from './route-response'; /** * Available authorization providers for ApiGateway V2 APIs */ -export enum WebSocketApiAuthorizationType { - /** - * Open access (Web Socket, HTTP APIs). - */ - NONE = 'NONE', +export enum WebSocketAuthorizationType { /** * Use AWS IAM permissions (Web Socket APIs). */ @@ -46,7 +29,7 @@ export enum WebSocketApiAuthorizationType { /** * Defines a set of common route keys known to the system */ -export enum KnownRouteKey { +export enum WebSocketKnownRouteKey { /** * Default route, when no other pattern matches */ @@ -64,52 +47,11 @@ export enum KnownRouteKey { /** * Known expressions for selecting a route in an API */ -export enum KnownRouteSelectionExpression { +export enum WebSocketKnownRouteSelectionExpression { /** * Selects the route key from the request context - * - * Supported only for WebSocket APIs. */ CONTEXT_ROUTE_KEY = '${context.routeKey}', - - /** - * A string starting with the method ans containing the request path - * - * Only supported value for HTTP APIs, if not provided, will be the default - */ - METHOD_PATH = '${request.method} ${request.path}' -} - -/** - * Defines the attributes for an Api Gateway V2 Route. - */ -export interface RouteAttributes { - /** - * The ID of this API Gateway Route. - */ - readonly routeId: string; - - /** - * The key of this API Gateway Route. - */ - readonly key: string; -} - -/** - * Defines the contract for an Api Gateway V2 Route. - */ -export interface IRoute extends IResource { - /** - * The ID of this API Gateway Route. - * @attribute - */ - readonly routeId: string; - - /** - * The key of this API Gateway Route. - * @attribute - */ - readonly key: KnownRouteKey | string; } /** @@ -117,7 +59,7 @@ export interface IRoute extends IResource { * * This interface is used by the helper methods in `Api` */ -export interface BaseRouteOptions { +export interface WebSocketRouteOptions { /** * The authorization scopes supported by this route. * @@ -140,63 +82,7 @@ export interface BaseRouteOptions { * @default - no operation name */ readonly operationName?: string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 Route. - * - * This interface is used by the helper methods in `Api` - */ -export interface RouteOptions extends BaseRouteOptions { - /** - * Specifies whether an API key is required for the route. - * - * @default false - */ - readonly apiKeyRequired?: boolean; - - /** - * The authorization type for the route. - * - * @default 'NONE' - */ - readonly authorizationType?: HttpApiAuthorizationType | WebSocketApiAuthorizationType; - - /** - * The model selection expression for the route. - * - * @default - no selection key - */ - readonly modelSelectionExpression?: KnownModelKey | string; - - /** - * The request models for the route. - * - * @default - no models (for example passthrough) - */ - readonly requestModels?: { [key: string]: IModel | string }; - - /** - * The request parameters for the route. - * - * @default - no parameters - */ - readonly requestParameters?: { [key: string]: boolean }; - /** - * The route response selection expression for the route. - * - * @default - no selection expression - */ - readonly routeResponseSelectionExpression?: KnownRouteResponseKey | string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 WebSocket Route. - * - * This interface is used by the helper methods in `Api` - */ -export interface WebSocketApiRouteOptions extends BaseRouteOptions { /** * Specifies whether an API key is required for the route. * @@ -209,21 +95,21 @@ export interface WebSocketApiRouteOptions extends BaseRouteOptions { * * @default 'NONE' */ - readonly authorizationType?: WebSocketApiAuthorizationType; + readonly authorizationType?: WebSocketAuthorizationType; /** * The model selection expression for the route. * * @default - no selection key */ - readonly modelSelectionExpression?: KnownModelKey | string; + readonly modelSelectionExpression?: string; /** * The request models for the route. * * @default - no models (for example passthrough) */ - readonly requestModels?: { [key: string]: IModel | string }; + readonly requestModels?: { [key: string]: IWebSocketModel }; /** * The request parameters for the route. @@ -237,36 +123,22 @@ export interface WebSocketApiRouteOptions extends BaseRouteOptions { * * @default - no selection expression */ - readonly routeResponseSelectionExpression?: KnownRouteResponseKey | string; -} - -/** - * Defines the properties required for defining an Api Gateway V2 HTTP Api Route. - * - * This interface is used by the helper methods in `Api` - */ -export interface HttpApiRouteOptions extends BaseRouteOptions { - /** - * The authorization type for the route. - * - * @default 'NONE' - */ - readonly authorizationType?: HttpApiAuthorizationType; + readonly routeResponseSelectionExpression?: string; } /** * Defines the properties required for defining an Api Gateway V2 Route. */ -export interface RouteProps extends RouteOptions { +export interface WebSocketRouteProps extends WebSocketRouteOptions { /** * The route key for the route. */ - readonly key: KnownRouteKey | string; + readonly key: string; /** * Defines the api for this route. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * Defines the integration for this route. @@ -278,19 +150,20 @@ export interface RouteProps extends RouteOptions { * An route for an API in Amazon API Gateway v2. * * Use `addResponse` to configure routes. + * + * @resource AWS::ApiGatewayV2::Route */ -export class Route extends Resource implements IRoute { +export class WebSocketRoute extends Resource implements IRoute { /** * Creates a new imported API Deployment * * @param scope scope of this imported resource * @param id identifier of the resource - * @param attrs Attributes of the Route + * @param routeId identifier of the CloudFormation route resource */ - public static fromRouteAttributes(scope: Construct, id: string, attrs: RouteAttributes): IRoute { + public static fromRouteId(scope: Construct, id: string, routeId: string): IRoute { class Import extends Resource implements IRoute { - public readonly routeId = attrs.routeId; - public readonly key = attrs.key; + public readonly routeId = routeId; } return new Import(scope, id); @@ -306,10 +179,10 @@ export class Route extends Resource implements IRoute { */ public readonly key: string; - protected api: IApi; + protected api: IWebSocketApi; protected resource: CfnRoute; - constructor(scope: Construct, id: string, props: RouteProps) { + constructor(scope: Construct, id: string, props: WebSocketRouteProps) { super(scope, id); this.api = props.api; this.key = props.key; @@ -324,7 +197,7 @@ export class Route extends Resource implements IRoute { } this.resource = new CfnRoute(this, 'Resource', { - apiId: this.api.apiId, + apiId: this.api.webSocketApiId, routeKey: props.key, target: `integrations/${props.integration.integrationId}`, requestModels, @@ -346,8 +219,8 @@ export class Route extends Resource implements IRoute { * @param key the key (predefined or not) that will select this response * @param props the properties for this response */ - public addResponse(key: KnownRouteResponseKey | string, props?: RouteResponseOptions): RouteResponse { - return new RouteResponse(this, `Response.${key}`, { + public addResponse(key: string, props?: WebSocketRouteResponseOptions): WebSocketRouteResponse { + return new WebSocketRouteResponse(this, `Response.${key}`, { ...props, route: this, api: this.api, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/stage.ts similarity index 74% rename from packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts rename to packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/stage.ts index 6f6942dbaa366..e395d9cac8cda 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/stage.ts @@ -1,13 +1,13 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; - -import { IApi } from './api'; -import { CfnStage } from './apigatewayv2.generated'; -import { IDeployment } from './deployment'; +import { Construct, Resource } from '@aws-cdk/core'; +import { CfnStage } from '../apigatewayv2.generated'; +import { CommonStageOptions, IStage } from '../common/stage'; +import { IWebSocketApi } from './api'; +import { IWebSocketDeployment } from './deployment'; /** * Specifies the logging level for this route. This property affects the log entries pushed to Amazon CloudWatch Logs. */ -export enum LoggingLevel { +export enum WebSocketRouteLoggingLevel { /** * Displays all log information */ @@ -27,7 +27,7 @@ export enum LoggingLevel { /** * Route settings for the stage. */ -export interface RouteSettings { +export interface WebSocketRouteSettings { /** * Specifies whether (true) or not (false) data trace logging is enabled for this route. * @@ -53,7 +53,7 @@ export interface RouteSettings { * * @default - default logging level */ - readonly loggingLevel?: LoggingLevel | string; + readonly loggingLevel?: WebSocketRouteLoggingLevel; /** * Specifies the throttling burst limit. @@ -73,7 +73,7 @@ export interface RouteSettings { /** * Settings for logging access in a stage. */ -export interface AccessLogSettings { +export interface WebSocketAccessLogSettings { /** * The ARN of the CloudWatch Logs log group to receive access logs. * @@ -90,39 +90,16 @@ export interface AccessLogSettings { readonly format?: string; } -/** - * Defines the contract for an Api Gateway V2 Stage. - */ -export interface IStage extends IResource { - /** - * The name of this API Gateway Stage. - * @attribute - */ - readonly stageName: string; -} - /** * Defines the properties required for defining an Api Gateway V2 Stage. */ -export interface StageOptions { - /** - * The stage name. Stage names can only contain alphanumeric characters, hyphens, and underscores. Maximum length is 128 characters. - */ - readonly stageName: string; - - /** - * Specifies whether updates to an API automatically trigger a new deployment. - * - * @default false - */ - readonly autoDeploy?: boolean; - +export interface WebSocketStageOptions extends CommonStageOptions { /** * Settings for logging access in this stage * - * @default - default nog settings + * @default - default log settings */ - readonly accessLogSettings?: AccessLogSettings; + readonly accessLogSettings?: WebSocketAccessLogSettings; /** * The identifier of a client certificate for a Stage. @@ -138,14 +115,14 @@ export interface StageOptions { * * @default - default values */ - readonly defaultRouteSettings?: RouteSettings; + readonly defaultRouteSettings?: WebSocketRouteSettings; /** * Route settings for the stage. * * @default - default route settings */ - readonly routeSettings?: { [key: string]: RouteSettings }; + readonly routeSettings?: { [key: string]: WebSocketRouteSettings }; /** * The description for the API stage. @@ -169,22 +146,24 @@ export interface StageOptions { /** * Defines the properties required for defining an Api Gateway V2 Stage. */ -export interface StageProps extends StageOptions { +export interface WebSocketStageProps extends WebSocketStageOptions { /** * Defines the api for this stage. */ - readonly api: IApi; + readonly api: IWebSocketApi; /** * The deployment for the API stage. Can't be updated if autoDeploy is enabled. */ - readonly deployment: IDeployment; + readonly deployment: IWebSocketDeployment; } /** * A stage for a route for an API in Amazon API Gateway v2. + * + * @resource AWS::ApiGatewayV2::Stage */ -export class Stage extends Resource implements IStage { +export class WebSocketStage extends Resource implements IStage { /** * Creates a new imported Stage * @@ -207,13 +186,13 @@ export class Stage extends Resource implements IStage { protected resource: CfnStage; - constructor(scope: Construct, id: string, props: StageProps) { + constructor(scope: Construct, id: string, props: WebSocketStageProps) { super(scope, id); this.resource = new CfnStage(this, 'Resource', { - apiId: props.api.apiId, + apiId: props.api.webSocketApiId, deploymentId: props.deployment.deploymentId, - stageName: props.stageName, + stageName: props.stageName || '$default', accessLogSettings: props.accessLogSettings, autoDeploy: props.autoDeploy, clientCertificateId: props.clientCertificateId, diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 5da15e738fa57..fa44781fbf84e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -91,15 +91,15 @@ }, "awslint": { "exclude": [ + "from-method:@aws-cdk/aws-apigatewayv2.DomainName", "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketDeployment", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketModel", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketRouteResponse", "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" - ] - }, - "awslint": { - "exclude": [ + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.MockIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.ServiceIntegrationProps", @@ -110,26 +110,35 @@ "props-physical-name:@aws-cdk/aws-apigatewayv2.LambdaIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.RouteProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.RouteResponseProps", - "no-unused-type:@aws-cdk/aws-apigatewayv2.AuthorizationType", - "no-unused-type:@aws-cdk/aws-apigatewayv2.ConnectionType", - "no-unused-type:@aws-cdk/aws-apigatewayv2.ContentHandlingStrategy", - "no-unused-type:@aws-cdk/aws-apigatewayv2.HttpApiIntegrationMethod", - "no-unused-type:@aws-cdk/aws-apigatewayv2.IntegrationMethod", - "no-unused-type:@aws-cdk/aws-apigatewayv2.IntegrationType", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownContentTypes", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownIntegrationResponseKey", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownModelKey", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownRouteKey", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownRouteResponseKey", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownTemplateKey", - "no-unused-type:@aws-cdk/aws-apigatewayv2.LoggingLevel", - "no-unused-type:@aws-cdk/aws-apigatewayv2.PassthroughBehavior", - "no-unused-type:@aws-cdk/aws-apigatewayv2.ProtocolType", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketApiMappingProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketDeploymentProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketHttpIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationResponseProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketLambdaIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketMockIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketRouteProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketRouteResponseProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketServiceIntegrationProps", "no-unused-type:@aws-cdk/aws-apigatewayv2.EndpointType", - "no-unused-type:@aws-cdk/aws-apigatewayv2.AuthorizerType", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownApiKeySelectionExpression", - "no-unused-type:@aws-cdk/aws-apigatewayv2.KnownRouteSelectionExpression", - "resource-interface:@aws-cdk/aws-apigatewayv2.LambdaIntegration" + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationType", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownContentTypes", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownIntegrationResponseKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownModelKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownRouteKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownRouteResponseKey", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownRouteSelectionExpression", + "no-unused-type:@aws-cdk/aws-apigatewayv2.WebSocketKnownTemplateKey", + "resource-interface:@aws-cdk/aws-apigatewayv2.HttpStage", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketApiMapping", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketAuthorizer", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketHttpIntegration", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketIntegration", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketLambdaIntegration", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketMockIntegration", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketRoute", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketServiceIntegration", + "resource-interface:@aws-cdk/aws-apigatewayv2.WebSocketStage" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/domain-name.test.ts similarity index 94% rename from packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/common/domain-name.test.ts index e16ca867ac18f..0f59faba10b80 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/domain-name.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/domain-name.test.ts @@ -1,7 +1,7 @@ import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; +import * as apigw from '../../lib'; // tslint:disable:max-line-length diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json deleted file mode 100644 index e23189fdea64c..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.expected.json +++ /dev/null @@ -1,425 +0,0 @@ -{ - "Resources": { - "MyFuncServiceRole54065130": { - "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" - ] - ] - } - ] - } - }, - "MyFunc8A243A2C": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "\nimport json\ndef handler(event, context):\n return {\n 'statusCode': 200,\n 'body': json.dumps(event)\n }" - }, - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "MyFuncServiceRole54065130", - "Arn" - ] - }, - "Runtime": "python3.7" - }, - "DependsOn": [ - "MyFuncServiceRole54065130" - ] - }, - "MyFuncApiPermissionApiagtewayV2HttpApigetbooksInteglambdaintegration4555698DC6292E59": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "MyFunc8A243A2C", - "Arn" - ] - }, - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":execute-api:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":", - { - "Ref": "HttpApiF5A9A8A7" - }, - "/", - { - "Ref": "HttpApiDefaultStage3EEB07D6" - }, - "/*" - ] - ] - } - } - }, - "RootFuncServiceRoleE4AA9E41": { - "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" - ] - ] - } - ] - } - }, - "RootFuncF39FB174": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "\nimport json, os\ndef handler(event, context):\n whoami = os.environ['WHOAMI']\n http_path = os.environ['HTTP_PATH']\n return {\n 'statusCode': 200,\n 'body': json.dumps({ 'whoami': whoami, 'http_path': http_path })\n }" - }, - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "RootFuncServiceRoleE4AA9E41", - "Arn" - ] - }, - "Runtime": "python3.7", - "Environment": { - "Variables": { - "WHOAMI": "root", - "HTTP_PATH": "/" - } - } - }, - "DependsOn": [ - "RootFuncServiceRoleE4AA9E41" - ] - }, - "RootFuncApiPermissionApiagtewayV2HttpApigetBookReviewInteglambdaintegration0ABD780C9E2600E7": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "RootFuncF39FB174", - "Arn" - ] - }, - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":execute-api:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":", - { - "Ref": "HttpApiF5A9A8A7" - }, - "/", - { - "Ref": "HttpApiDefaultStage3EEB07D6" - }, - "/*" - ] - ] - } - } - }, - "HttpApiF5A9A8A7": { - "Type": "AWS::ApiGatewayV2::Api", - "Properties": { - "Name": "HttpApi", - "ProtocolType": "HTTP", - "RouteSelectionExpression": "${request.method} ${request.path}" - } - }, - "HttpApiDeployment2ABEB12F": { - "Type": "AWS::ApiGatewayV2::Deployment", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "Description": "Automatically created by the Api construct" - }, - "DependsOn": [ - "HttpApidefaultroute8BBAF64E", - "HttpApidefaulthttpintegration5F125B3A", - "HttpApiGETrouteF5E07819", - "HttpApiGETbooksreviewsrouteCAD382E0", - "HttpApiGETbooksroute2D32C4F3", - "HttpApigetBookReviewInteglambdaintegrationD5870D18", - "HttpApigetbooksInteglambdaintegrationE71E55E1", - "HttpApiPOSTroute74994293", - "HttpApiRootInteghttpintegration15582736" - ] - }, - "HttpApiDefaultStage3EEB07D6": { - "Type": "AWS::ApiGatewayV2::Stage", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "StageName": "prod", - "DeploymentId": { - "Ref": "HttpApiDeployment2ABEB12F" - }, - "Description": "Automatically created by the Api construct" - } - }, - "HttpApidefaulthttpintegration5F125B3A": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "IntegrationType": "HTTP_PROXY", - "IntegrationMethod": "ANY", - "IntegrationUri": "https://aws.amazon.com", - "PayloadFormatVersion": "1.0" - } - }, - "HttpApidefaultroute8BBAF64E": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "RouteKey": "$default", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpApidefaulthttpintegration5F125B3A" - } - ] - ] - } - } - }, - "HttpApiRootInteghttpintegration15582736": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "IntegrationType": "HTTP_PROXY", - "IntegrationMethod": "ANY", - "IntegrationUri": "https://checkip.amazonaws.com", - "PayloadFormatVersion": "1.0" - } - }, - "HttpApiGETrouteF5E07819": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "RouteKey": "GET /", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpApiRootInteghttpintegration15582736" - } - ] - ] - } - } - }, - "HttpApiPOSTroute74994293": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "RouteKey": "POST /", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpApiRootInteghttpintegration15582736" - } - ] - ] - } - } - }, - "HttpApigetbooksInteglambdaintegrationE71E55E1": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationMethod": "POST", - "IntegrationUri": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":apigateway:", - { - "Ref": "AWS::Region" - }, - ":lambda:path/2015-03-31/functions/", - { - "Fn::GetAtt": [ - "MyFunc8A243A2C", - "Arn" - ] - }, - "/invocations" - ] - ] - }, - "PayloadFormatVersion": "1.0" - } - }, - "HttpApiGETbooksroute2D32C4F3": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "RouteKey": "GET /books", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpApigetbooksInteglambdaintegrationE71E55E1" - } - ] - ] - } - } - }, - "HttpApigetBookReviewInteglambdaintegrationD5870D18": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationMethod": "POST", - "IntegrationUri": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":apigateway:", - { - "Ref": "AWS::Region" - }, - ":lambda:path/2015-03-31/functions/", - { - "Fn::GetAtt": [ - "RootFuncF39FB174", - "Arn" - ] - }, - "/invocations" - ] - ] - }, - "PayloadFormatVersion": "1.0" - } - }, - "HttpApiGETbooksreviewsrouteCAD382E0": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpApiF5A9A8A7" - }, - "RouteKey": "GET /books/reviews", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpApigetBookReviewInteglambdaintegrationD5870D18" - } - ] - ] - } - } - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts deleted file mode 100644 index a2745671df684..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integ.http-api.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as lambda from '@aws-cdk/aws-lambda'; -import * as cdk from '@aws-cdk/core'; -import * as apigatewayv2 from '../lib'; - -// tslint:disable:max-line-length - -const app = new cdk.App(); - -const stack = new cdk.Stack(app, 'ApiagtewayV2HttpApi'); - -const getbooksHandler = new lambda.Function(stack, 'MyFunc', { - runtime: lambda.Runtime.PYTHON_3_7, - handler: 'index.handler', - code: new lambda.InlineCode(` -import json -def handler(event, context): - return { - 'statusCode': 200, - 'body': json.dumps(event) - }`), -}); - -const getbookReviewsHandler = new lambda.Function(stack, 'RootFunc', { - runtime: lambda.Runtime.PYTHON_3_7, - handler: 'index.handler', - code: new lambda.InlineCode(` -import json, os -def handler(event, context): - whoami = os.environ['WHOAMI'] - http_path = os.environ['HTTP_PATH'] - return { - 'statusCode': 200, - 'body': json.dumps({ 'whoami': whoami, 'http_path': http_path }) - }`), - environment: { - WHOAMI: 'root', - HTTP_PATH: '/', - }, -}); - -const rootUrl = 'https://checkip.amazonaws.com'; -const defaultUrl = 'https://aws.amazon.com'; - -// create a basic HTTP API with http proxy integration as the $default route -const api = new apigatewayv2.HttpApi(stack, 'HttpApi', { - defaultTarget: { uri: defaultUrl }, -}); - -api.addRoutes(['GET /', 'POST /'], api.addHttpIntegration('RootInteg', { url: rootUrl })); -api.addRoute({ method: apigatewayv2.HttpMethod.GET, path: '/books' }, api.addLambdaIntegration('getbooksInteg', { handler : getbooksHandler })); -api.addRoute('GET /books/reviews', api.addLambdaIntegration('getBookReviewInteg', { handler: getbookReviewsHandler })); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json index 1b829c8a9fa82..2cb8528627126 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json @@ -157,7 +157,6 @@ }, "IntegrationType": "AWS", "Description": "WebSocket Api Connection Integration", - "IntegrationMethod": "POST", "IntegrationUri": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts index 9b544c930e270..6ce502a6ec703 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts @@ -28,14 +28,14 @@ const api = new apigatewayv2.WebSocketApi(stack, 'WebSocketApi', { }, }); -const defaultStatusIntegrationResponse: apigatewayv2.IntegrationResponseOptions = { +const defaultStatusIntegrationResponse: apigatewayv2.WebSocketIntegrationResponseOptions = { responseTemplates: { default: '#set($inputRoot = $input.path(\'$\')) { "status": "${inputRoot.status}", "message": "$util.escapeJavaScript(${inputRoot.message})" }', }, templateSelectionExpression: 'default', }; -const defaultStatusRouteResponse: apigatewayv2.RouteResponseOptions = { +const defaultStatusRouteResponse: apigatewayv2.WebSocketRouteResponseOptions = { modelSelectionExpression: 'default', responseModels: { default: api.addModel({ schema: apigatewayv2.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigatewayv2.JsonSchemaType.OBJECT, properties: { status: { type: apigatewayv2.JsonSchemaType.STRING }, message: { type: apigatewayv2.JsonSchemaType.STRING } } }), @@ -45,16 +45,16 @@ const defaultStatusRouteResponse: apigatewayv2.RouteResponseOptions = { const webSocketConnectIntegration = api.addLambdaIntegration('default', { handler: webSocketHandler, proxy: false, - passthroughBehavior: apigatewayv2.PassthroughBehavior.NEVER, + passthroughBehavior: apigatewayv2.WebSocketPassthroughBehavior.NEVER, requestTemplates: { connect: '{ "action": "${context.routeKey}", "userId": "${context.identity.cognitoIdentityId}", "connectionId": "${context.connectionId}", "domainName": "${context.domainName}", "stageName": "${context.stage}" }', }, templateSelectionExpression: 'connect', description: 'WebSocket Api Connection Integration', }); -webSocketConnectIntegration.addResponse(apigatewayv2.KnownRouteResponseKey.DEFAULT, defaultStatusIntegrationResponse); +webSocketConnectIntegration.addResponse(apigatewayv2.WebSocketKnownRouteResponseKey.DEFAULT, defaultStatusIntegrationResponse); -api.addRoute(apigatewayv2.KnownRouteKey.CONNECT, webSocketConnectIntegration, { - authorizationType: apigatewayv2.WebSocketApiAuthorizationType.IAM, - routeResponseSelectionExpression: apigatewayv2.KnownRouteResponseKey.DEFAULT, -}).addResponse(apigatewayv2.KnownRouteResponseKey.DEFAULT, defaultStatusRouteResponse); +api.addRoute(apigatewayv2.WebSocketKnownRouteKey.CONNECT, webSocketConnectIntegration, { + authorizationType: apigatewayv2.WebSocketAuthorizationType.IAM, + routeResponseSelectionExpression: apigatewayv2.WebSocketKnownRouteResponseKey.DEFAULT, +}).addResponse(apigatewayv2.WebSocketKnownRouteResponseKey.DEFAULT, defaultStatusRouteResponse); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts deleted file mode 100644 index cb7953744154b..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/test/route.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; -import '@aws-cdk/assert/jest'; -import * as lambda from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; - -// tslint:disable:max-line-length - -test('route', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false, - }); - const integration = api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), - }); - api.addRoute(apigw.KnownRouteKey.CONNECT, integration, { - modelSelectionExpression: apigw.KnownModelKey.DEFAULT, - requestModels: { - [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusInputModel', type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }), - }, - routeResponseSelectionExpression: apigw.KnownRouteResponseKey.DEFAULT, - }); - - // THEN - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { - ApiId: { Ref: 'myapi4C7BF186' }, - RouteKey: '$connect', - Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, - ModelSelectionExpression: '$default', - RequestModels: { - $default: 'statusInputModel', - }, - })); - - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { - ApiId: { Ref: 'myapi4C7BF186' }, - ContentType: apigw.KnownContentTypes.JSON, - Name: 'statusInputModel', - })); -}); - -test('route (HTTP)', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const api = new apigw.HttpApi(stack, 'my-api', { - deploy: false, - }); - const integration = api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), - }); - api.addRoute({ method: apigw.HttpMethod.POST, path: '/' }, integration); - api.addRoutes([ { method: apigw.HttpMethod.GET, path: '/' }, 'PUT /' ] , integration); - - // THEN - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { - ApiId: { Ref: 'myapi4C7BF186' }, - RouteKey: 'POST /', - Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, - })); - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { - ApiId: { Ref: 'myapi4C7BF186' }, - RouteKey: 'GET /', - Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, - })); - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { - ApiId: { Ref: 'myapi4C7BF186' }, - RouteKey: 'PUT /', - Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, - })); -}); - -test('route response', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false, - }); - const integration = api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), - }); - const route = api.addRoute(apigw.KnownRouteKey.CONNECT, integration, {}); - route.addResponse(apigw.KnownRouteKey.CONNECT, { - modelSelectionExpression: apigw.KnownModelKey.DEFAULT, - responseModels: { - [apigw.KnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }), - }, - }); - - // THEN - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::RouteResponse', { - ApiId: { Ref: 'myapi4C7BF186' }, - RouteId: { Ref: 'myapiconnectrouteC62A8B0B' }, - RouteResponseKey: '$connect', - ModelSelectionExpression: '$default', - ResponseModels: { - $default: 'statusResponse', - }, - })); - - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { - ApiId: { Ref: 'myapi4C7BF186' }, - ContentType: 'application/json', - Name: 'statusResponse', - })); -}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts similarity index 69% rename from packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts index 52304e01460db..772bd83a4e251 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts @@ -1,7 +1,7 @@ import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; +import * as apigw from '../../lib'; // tslint:disable:max-line-length @@ -10,10 +10,14 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - const api = new apigw.HttpApi(stack, 'my-api'); + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + deploy: true, + }); const domainName = new apigw.DomainName(stack, 'domain-name', { domainName: 'test.example.com' }); - api.addApiMapping({ + new apigw.WebSocketApiMapping(stack, 'domain-name-mapping', { stage: api.deploymentStage!, + api, domainName, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts similarity index 50% rename from packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts index 1d29054ef2460..0c58f331159ca 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts @@ -1,17 +1,17 @@ import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; +import * as apigw from '../../lib'; // tslint:disable:max-line-length -test('minimal setup (websocket)', () => { +test('minimal setup (WebSocket)', () => { // GIVEN const stack = new Stack(); // WHEN new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, }); // THEN @@ -32,57 +32,13 @@ test('minimal setup (websocket)', () => { })); }); -test('minimal setup (HTTP)', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - new apigw.HttpApi(stack, 'my-api', { - }); - - // THEN - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { - Name: 'my-api', - ProtocolType: 'HTTP', - RouteSelectionExpression: '${request.method} ${request.path}', - })); - - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Deployment', { - ApiId: { Ref: 'myapi4C7BF186' }, - })); - - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Stage', { - ApiId: { Ref: 'myapi4C7BF186' }, - StageName: 'prod', - DeploymentId: { Ref: 'myapiDeployment92F2CB49' }, - })); -}); - test('minimal setup (WebSocket, no deploy)', () => { // GIVEN const stack = new Stack(); // WHEN new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, - deploy: false, - }); - - // THEN - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Api', { - Name: 'my-api', - })); - - cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Deployment')); - cdkExpect(stack).notTo(haveResource('AWS::ApiGatewayV2::Stage')); -}); - -test('minimal setup (HTTP, no deploy)', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - new apigw.HttpApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); @@ -102,7 +58,7 @@ test('minimal setup (no deploy, error)', () => { // WHEN expect(() => { return new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, deployOptions: { stageName: 'testStage', @@ -116,12 +72,8 @@ test('URLs and ARNs (WebSocket)', () => { const stack = new Stack(); // WHEN - const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); - const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); - const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { - key: 'routeKey', - routeId: 'routeId', - }); + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); + const importedStage = apigw.WebSocketStage.fromStageName(stack, 'devStage', 'dev'); // THEN expect(stack.resolve(api.clientUrl())).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiDefaultStage51F6D7C3' } ] ] }); @@ -131,9 +83,9 @@ test('URLs and ARNs (WebSocket)', () => { expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev/@connections' ] ] }); expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/*' ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn('routeKey'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/*' ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn('routeKey', importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); expect(stack.resolve(api.connectionsApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/POST/*' ] ] }); expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/POST/my-connection' ] ] }); @@ -141,54 +93,16 @@ test('URLs and ARNs (WebSocket)', () => { expect(stack.resolve(api.connectionsApiArn('my-connection', importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/POST/my-connection' ] ] }); }); -test('URLs and ARNs (HTTP)', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const api = new apigw.HttpApi(stack, 'my-api', {}); - const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); - const importedRoute = apigw.Route.fromRouteAttributes(stack, 'devRoute', { - key: 'routeKey', - routeId: 'routeId', - }); - - // THEN - expect(stack.resolve(api.clientUrl())).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/', { Ref: 'myapiDefaultStage51F6D7C3' } ] ] }); - expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); - - expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/*' ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); - expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/*' ] ] }); - expect(stack.resolve(api.executeApiArn(importedRoute, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); -}); - -test('URLs and ARNs (HTTP, no deploy)', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const api = new apigw.HttpApi(stack, 'my-api', { - deploy: false, - }); - const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); - - // THEN - expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); - - expect(() => stack.resolve(api.clientUrl())).toThrow(); -}); - test('URLs and ARNs (WebSocket, no deploy)', () => { // GIVEN const stack = new Stack(); // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); - const importedStage = apigw.Stage.fromStageName(stack, 'devStage', 'dev'); + const importedStage = apigw.WebSocketStage.fromStageName(stack, 'devStage', 'dev'); // THEN expect(stack.resolve(api.clientUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'wss://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev' ] ] }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts similarity index 76% rename from packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts index 94cc82f3634c0..6968f741f972c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/authorizer.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts @@ -1,7 +1,7 @@ import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; +import * as apigw from '../../lib'; // tslint:disable:max-line-length @@ -10,15 +10,13 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const functionArn = stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'}); - new apigw.Authorizer(stack, 'authorizer', { + new apigw.WebSocketAuthorizer(stack, 'authorizer', { authorizerName: 'my-authorizer', - authorizerType: apigw.AuthorizerType.JWT, authorizerUri: `arn:${stack.partition}:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${functionArn}/invocations`, api, }); @@ -27,7 +25,7 @@ test('minimal setup', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Authorizer', { ApiId: { Ref: 'myapi4C7BF186' }, Name: 'my-authorizer', - AuthorizerType: 'JWT', + AuthorizerType: 'REQUEST', AuthorizerUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, IdentitySource: [], })); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts similarity index 60% rename from packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts index eb952de458d06..5ddbf9da5bd64 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts @@ -2,167 +2,178 @@ import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; +import * as apigw from '../../lib'; // tslint:disable:max-line-length -test('Lambda integration (WebSocket)', () => { +test('Http integration (WebSocket)', () => { // GIVEN const stack = new Stack(); // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); - api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), + api.addHttpIntegration('myFunction', { + url: 'https://test.example.com/', }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: apigw.IntegrationType.AWS, - IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, + IntegrationType: apigw.WebSocketIntegrationType.HTTP, + IntegrationUri: 'https://test.example.com/', })); }); -test('Lambda integration (WebSocket, with extra params)', () => { +test('Http integration (WebSocket, proxy)', () => { // GIVEN const stack = new Stack(); // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); - api.addLambdaIntegration('myFunction', { - handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), - connectionType: apigw.ConnectionType.INTERNET, + api.addHttpIntegration('myFunction', { + url: 'https://test.example.com/', + proxy: true, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'AWS', - IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, - IntegrationMethod: 'POST', - ConnectionType: 'INTERNET', + IntegrationType: apigw.WebSocketIntegrationType.HTTP_PROXY, + IntegrationUri: 'https://test.example.com/', })); }); -test('Lambda integration (WebSocket, proxy)', () => { +test('Lambda integration (WebSocket)', () => { // GIVEN const stack = new Stack(); // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), - proxy: true, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'AWS_PROXY', + IntegrationType: apigw.WebSocketIntegrationType.AWS, IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, - IntegrationMethod: 'POST', })); }); -test('Lambda integration (HTTP)', () => { +test('Lambda integration (WebSocket, proxy)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.HttpApi(stack, 'my-api', { + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), + proxy: true, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'AWS_PROXY', + IntegrationType: apigw.WebSocketIntegrationType.AWS_PROXY, IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, - IntegrationMethod: 'POST', })); }); -test('Http integration (HTTP)', () => { +test('Mock integration (WebSocket)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.HttpApi(stack, 'my-api', { + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); - api.addHttpIntegration('myFunction', { - url: 'https://aws.amazon.com/', - }); + api.addMockIntegration('myMock', { }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'HTTP_PROXY', - IntegrationUri: 'https://aws.amazon.com/', - IntegrationMethod: 'ANY', + IntegrationType: apigw.WebSocketIntegrationType.MOCK, + IntegrationUri: '', })); +}); + +test('Service integration (WebSocket)', () => { + // GIVEN + const stack = new Stack(); - api.addHttpIntegration('myFunction2', { - url: 'https://console.aws.amazon.com/', - integrationMethod: 'POST', + // WHEN + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + deploy: false, + }); + api.addServiceIntegration('myService', { + arn: stack.formatArn({ service: 'dynamodb', resource: 'table', resourceName: 'my-table', sep: '/'}), }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'HTTP_PROXY', - IntegrationUri: 'https://aws.amazon.com/', - IntegrationMethod: 'ANY', - })); - cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { - ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'HTTP_PROXY', - IntegrationUri: 'https://console.aws.amazon.com/', - IntegrationMethod: 'POST', + IntegrationType: apigw.WebSocketIntegrationType.AWS, + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':dynamodb:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':table/my-table']] }, })); }); -test('Service integration (HTTP)', () => { +test('Service integration (WebSocket, proxy)', () => { // GIVEN const stack = new Stack(); // WHEN - const api = new apigw.HttpApi(stack, 'my-api', { + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); - api.addServiceIntegration('myObject', { - arn: stack.formatArn({ service: 's3', account: '', region: '', resource: 'my-bucket', resourceName: 'my-key', sep: '/'}), - }); - api.addServiceIntegration('myOtherObject', { - integrationMethod: 'GET', - arn: stack.formatArn({ service: 's3', account: '', region: '', resource: 'my-bucket', resourceName: 'my-other-key', sep: '/'}), + api.addServiceIntegration('myService', { + arn: stack.formatArn({ service: 'dynamodb', resource: 'table', resourceName: 'my-table', sep: '/'}), + proxy: true, }); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'AWS_PROXY', - IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket/my-key']] }, - IntegrationMethod: 'ANY', + IntegrationType: apigw.WebSocketIntegrationType.AWS_PROXY, + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':dynamodb:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':table/my-table']] }, })); +}); + +test('Lambda integration (WebSocket, with extra params)', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + deploy: false, + }); + api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), + connectionType: apigw.WebSocketConnectionType.INTERNET, + }); + + // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Integration', { ApiId: { Ref: 'myapi4C7BF186' }, - IntegrationType: 'AWS_PROXY', - IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket/my-other-key']] }, - IntegrationMethod: 'GET', + IntegrationType: 'AWS', + IntegrationUri: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/arn:', { Ref: 'AWS::Partition' }, ':lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:my-function/invocations']] }, + ConnectionType: 'INTERNET', })); }); @@ -172,13 +183,13 @@ test('Integration response', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), }); - integration.addResponse(apigw.KnownIntegrationResponseKey.DEFAULT); + integration.addResponse(apigw.WebSocketKnownIntegrationResponseKey.DEFAULT); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::IntegrationResponse', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts new file mode 100644 index 0000000000000..fc2e0bb9931d1 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts @@ -0,0 +1,83 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import * as apigw from '../../lib'; + +// tslint:disable:max-line-length + +test('route', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + deploy: false, + }); + const integration = api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), + }); + api.addRoute(apigw.WebSocketKnownRouteKey.CONNECT, integration, { + modelSelectionExpression: apigw.WebSocketKnownModelKey.DEFAULT, + requestModels: { + [apigw.WebSocketKnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusInputModel', type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }), + }, + routeResponseSelectionExpression: apigw.WebSocketKnownRouteResponseKey.DEFAULT, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Route', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteKey: '$connect', + Target: { 'Fn::Join': ['', [ 'integrations/', { Ref: 'myapimyFunctionlambdaintegrationB6693307' } ] ] }, + ModelSelectionExpression: '$default', + RequestModels: { + $default: 'statusInputModel', + }, + })); + + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { + ApiId: { Ref: 'myapi4C7BF186' }, + ContentType: apigw.WebSocketKnownContentTypes.JSON, + Name: 'statusInputModel', + })); +}); + +test('route response', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + deploy: false, + }); + const integration = api.addLambdaIntegration('myFunction', { + handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), + }); + const route = api.addRoute(apigw.WebSocketKnownRouteKey.CONNECT, integration, {}); + route.addResponse(apigw.WebSocketKnownRouteKey.CONNECT, { + modelSelectionExpression: apigw.WebSocketKnownModelKey.DEFAULT, + responseModels: { + [apigw.WebSocketKnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }), + }, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::RouteResponse', { + ApiId: { Ref: 'myapi4C7BF186' }, + RouteId: { Ref: 'myapiconnectrouteC62A8B0B' }, + RouteResponseKey: '$connect', + ModelSelectionExpression: '$default', + ResponseModels: { + $default: 'statusResponse', + }, + })); + + cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { + ApiId: { Ref: 'myapi4C7BF186' }, + ContentType: 'application/json', + Name: 'statusResponse', + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts similarity index 63% rename from packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts index b358cbea3f3cf..f033d28187674 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts @@ -1,7 +1,7 @@ import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import * as apigw from '../lib'; +import * as apigw from '../../lib'; // tslint:disable:max-line-length @@ -10,15 +10,14 @@ test('minimal setup', () => { const stack = new Stack(); // WHEN - const api = new apigw.Api(stack, 'my-api', { - protocolType: apigw.ProtocolType.WEBSOCKET, - routeSelectionExpression: apigw.KnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + const api = new apigw.WebSocketApi(stack, 'my-api', { + routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); - const deployment = new apigw.Deployment(stack, 'deployment', { + const deployment = new apigw.WebSocketDeployment(stack, 'deployment', { api, }); - new apigw.Stage(stack, 'stage', { + new apigw.WebSocketStage(stack, 'stage', { api, deployment, stageName: 'dev', From 35f48b1069caa8b25ecc7460fe2c0212c9c7cd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 30 Jun 2020 18:16:58 +0200 Subject: [PATCH 6/7] feat(aws-apigatewayv2): change open-ended enum parameters (known values) to classes Update of the classes of API Gateway V2 to use classes instead of enums for open-ended value lists (known content type, routes, models, ...) --- .../aws-apigatewayv2/lib/web-socket/api.ts | 63 +++++++-- .../lib/web-socket/integration-response.ts | 79 ++++++++++-- .../lib/web-socket/integration.ts | 37 +++++- .../aws-apigatewayv2/lib/web-socket/model.ts | 60 +++++++-- .../lib/web-socket/route-response.ts | 76 +++++++++-- .../aws-apigatewayv2/lib/web-socket/route.ts | 120 +++++++++++++++--- .../test/integ.web-socket-api.ts | 16 +-- .../test/web-socket/api-mapping.test.ts | 2 +- .../test/web-socket/api.test.ts | 14 +- .../test/web-socket/authorizer.test.ts | 2 +- .../test/web-socket/integration.test.ts | 20 +-- .../test/web-socket/route.test.ts | 24 ++-- .../test/web-socket/stage.test.ts | 2 +- 13 files changed, 411 insertions(+), 104 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts index f75c291e3ea84..84e3fc698db5e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/api.ts @@ -11,9 +11,56 @@ import { WebSocketLambdaIntegration, WebSocketLambdaIntegrationOptions } from '. import { WebSocketMockIntegration, WebSocketMockIntegrationOptions } from './integrations/mock'; import { WebSocketServiceIntegration, WebSocketServiceIntegrationOptions } from './integrations/service'; import { WebSocketModel, WebSocketModelOptions } from './model'; -import { WebSocketKnownRouteKey, WebSocketRoute, WebSocketRouteOptions } from './route'; +import { WebSocketRoute, WebSocketRouteKey, WebSocketRouteOptions } from './route'; import { WebSocketStage, WebSocketStageOptions } from './stage'; +/** + * Available Api Key Selectors for ApiGateway V2 APIs + */ +export enum WebSocketApiKeySelectionExpression { + /** + * Use the request header x-api-key. + */ + HEADER_X_API_KEY = '$request.header.x-api-key', + /** + * Use the authorizer's usage identifier key. + */ + AUTHORIZER_USAGE_IDENTIFIER_KEY = '$context.authorizer.usageIdentifierKey', +} + +/** + * Known expressions for selecting a route in an API + */ +export class WebSocketRouteSelectionExpression { + /** + * Default route, when no other pattern matches + */ + public static readonly CONTEXT_ROUTE_KEY = new WebSocketRouteSelectionExpression('${context.routeKey}'); + + /** + * Creates a custom route key + * @param value the name of the route key + */ + public static custom(value: string): WebSocketRouteSelectionExpression { + return new WebSocketRouteSelectionExpression(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } +} + /** * Defines a default handler for the Api * @@ -110,14 +157,14 @@ export interface WebSocketApiProps { /** * Expression used to select the route for this API */ - readonly routeSelectionExpression: string; + readonly routeSelectionExpression: WebSocketRouteSelectionExpression; /** * Expression used to select the Api Key to use for metering * * @default - No Api Key */ - readonly apiKeySelectionExpression?: string; + readonly apiKeySelectionExpression?: WebSocketApiKeySelectionExpression; /** * A description of the purpose of this API Gateway Api resource. @@ -210,7 +257,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { disableSchemaValidation: props.disableSchemaValidation, failOnWarnings: props.failOnWarnings, protocolType: 'WEBSOCKET', - routeSelectionExpression: props.routeSelectionExpression, + routeSelectionExpression: props.routeSelectionExpression.toString(), // TODO: tags: props.tags, version: props.version, }); @@ -272,7 +319,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { throw new Error('You must specify an ARN, a URL, "MOCK", or a Lambda Function'); } - this.addRoute(WebSocketKnownRouteKey.DEFAULT, integration, {}); + this.addRoute(WebSocketRouteKey.DEFAULT, integration, {}); } } @@ -351,7 +398,7 @@ export class WebSocketApi extends Resource implements IWebSocketApi { * @param integration [disable-awslint:ref-via-interface] the integration to use for this route * @param props the properties for this route */ - public addRoute(key: string, integration: WebSocketIntegration, props?: WebSocketRouteOptions): WebSocketRoute { + public addRoute(key: WebSocketRouteKey, integration: WebSocketIntegration, props?: WebSocketRouteOptions): WebSocketRoute { const route = new WebSocketRoute(this, `${key}.route`, { ...props, api: this, @@ -388,10 +435,10 @@ export class WebSocketApi extends Resource implements IWebSocketApi { * @param route The route for this ARN ('*' if not defined) * @param stage The stage for this ARN (if not defined, defaults to the deployment stage if defined, or to '*') */ - public executeApiArn(route?: string, stage?: IStage) { + public executeApiArn(route?: WebSocketRouteKey, stage?: IStage) { const stack = Stack.of(this); const apiId = this.webSocketApiId; - const routeKey = ((route === undefined) ? '*' : route); + const routeKey = ((route === undefined) ? '*' : route.toString()); const stageName = ((stage === undefined) ? ((this.deploymentStage === undefined) ? '*' : this.deploymentStage.stageName) : stage.stageName); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts index fcb19668c3f8b..b6c0d3b3fe8d8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration-response.ts @@ -4,25 +4,82 @@ import { CfnIntegrationResponse } from '../apigatewayv2.generated'; import { IIntegration } from '../common/integration'; import { IWebSocketApi } from './api'; +import { WebSocketContentHandlingStrategy } from './integration'; /** - * Defines a set of common response patterns known to the system + * Defines a set of common template patterns known to the system */ -export enum WebSocketKnownIntegrationResponseKey { +export class WebSocketIntegrationResponseTemplateSelectionExpressionKey { /** - * Default response, when no other pattern matches + * Default template, when no other pattern matches */ - DEFAULT = '$default', + public static readonly DEFAULT = new WebSocketIntegrationResponseTemplateSelectionExpressionKey('$default'); /** * Empty response */ - EMPTY = 'empty', + public static readonly EMPTY = new WebSocketIntegrationResponseTemplateSelectionExpressionKey('empty'); /** * Error response */ - ERROR = 'error' + public static readonly ERROR = new WebSocketIntegrationResponseTemplateSelectionExpressionKey('error'); + + /** + * Creates a custom selection expression + * @param value the name of the response template key + */ + public static custom(value: string): WebSocketIntegrationResponseTemplateSelectionExpressionKey { + return new WebSocketIntegrationResponseTemplateSelectionExpressionKey(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } +} + +/** + * Defines a set of common model patterns known to the system + */ +export class WebSocketIntegrationResponseKey { + /** + * Default model, when no other pattern matches + */ + public static readonly DEFAULT = new WebSocketIntegrationResponseKey('$default'); + + /** + * Creates a custom selection expression + * @param value the name of the model key + */ + public static custom(value: string): WebSocketIntegrationResponseKey { + return new WebSocketIntegrationResponseKey(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** @@ -44,7 +101,7 @@ export interface WebSocketIntegrationResponseOptions { * * @default - Pass through unmodified */ - readonly contentHandlingStrategy?: string; + readonly contentHandlingStrategy?: WebSocketContentHandlingStrategy; /** * A key-value map specifying response parameters that are passed to the method response from the backend. @@ -77,7 +134,7 @@ export interface WebSocketIntegrationResponseOptions { * * @default - no template selected */ - readonly templateSelectionExpression?: string; + readonly templateSelectionExpression?: WebSocketIntegrationResponseTemplateSelectionExpressionKey; } /** @@ -97,7 +154,7 @@ export interface WebSocketIntegrationResponseProps extends WebSocketIntegrationR /** * The integration response key. */ - readonly key: string; + readonly key: WebSocketIntegrationResponseKey; } /** @@ -113,11 +170,11 @@ export class WebSocketIntegrationResponse extends Resource implements IWebSocket this.resource = new CfnIntegrationResponse(this, 'Resource', { apiId: props.api.webSocketApiId, integrationId: props.integration.integrationId, - integrationResponseKey: props.key, + integrationResponseKey: props.key.toString(), contentHandlingStrategy: props.contentHandlingStrategy, responseParameters: props.responseParameters, responseTemplates: props.responseTemplates, - templateSelectionExpression: props.templateSelectionExpression, + templateSelectionExpression: props.templateSelectionExpression?.toString(), }); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts index 1297b9e1553a8..78d5e99ce8b4d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts @@ -4,7 +4,7 @@ import { CfnIntegration } from '../apigatewayv2.generated'; import { IIntegration } from '../common/integration'; import { IWebSocketApi } from './api'; -import { WebSocketIntegrationResponse, WebSocketIntegrationResponseOptions } from './integration-response'; +import { WebSocketIntegrationResponse, WebSocketIntegrationResponseKey, WebSocketIntegrationResponseOptions } from './integration-response'; /** * The type of the network connection to the integration endpoint. @@ -100,11 +100,34 @@ export enum WebSocketPassthroughBehavior { /** * Defines a set of common template patterns known to the system */ -export enum WebSocketKnownTemplateKey { +export class WebSocketIntegrationTemplateSelectionExpression { /** * Default template, when no other pattern matches */ - DEFAULT = '$default', + public static readonly DEFAULT = new WebSocketIntegrationTemplateSelectionExpression('$default'); + + /** + * Creates a custom selection expression + * @param value the name of the route key + */ + public static custom(value: string): WebSocketIntegrationTemplateSelectionExpression { + return new WebSocketIntegrationTemplateSelectionExpression(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** @@ -217,7 +240,7 @@ export interface WebSocketIntegrationOptions { * * @default - no template selected */ - readonly templateSelectionExpression?: string; + readonly templateSelectionExpression?: WebSocketIntegrationTemplateSelectionExpression; /** * The ID of the VPC link for a private integration. @@ -318,9 +341,9 @@ export class WebSocketIntegration extends Resource implements IIntegration { payloadFormatVersion: props.payloadFormatVersion, requestParameters: props.requestParameters, requestTemplates: props.requestTemplates, - templateSelectionExpression: props.templateSelectionExpression, + templateSelectionExpression: props.templateSelectionExpression?.toString(), tlsConfig: props.tlsConfig, - timeoutInMillis: (props.timeout ? props.timeout.toMilliseconds() : undefined), + timeoutInMillis: props.timeout?.toMilliseconds(), apiId: props.api.webSocketApiId, }); @@ -333,7 +356,7 @@ export class WebSocketIntegration extends Resource implements IIntegration { * @param key the key (predefined or not) that will select this response * @param props the properties for this response */ - public addResponse(key: string, props?: WebSocketIntegrationResponseOptions): WebSocketIntegrationResponse { + public addResponse(key: WebSocketIntegrationResponseKey, props?: WebSocketIntegrationResponseOptions): WebSocketIntegrationResponse { return new WebSocketIntegrationResponse(this, `Response.${key}`, { ...props, api: this.api, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts index 0201b8be725ac..30595f74f500a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/model.ts @@ -6,42 +6,80 @@ import { IWebSocketApi } from './api'; /** * Defines a set of common model patterns known to the system */ -export enum WebSocketKnownModelKey { +export class WebSocketModelKey { /** * Default model, when no other pattern matches */ - DEFAULT = '$default', + public static readonly DEFAULT = new WebSocketModelKey('$default'); /** * Default model, when no other pattern matches */ - EMPTY = '' + public static readonly EMPTY = new WebSocketModelKey(''); + + /** + * Creates a custom selection expression + * @param value the name of the model key + */ + public static custom(value: string): WebSocketModelKey { + return new WebSocketModelKey(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** * Defines a set of common content types for APIs */ -export enum WebSocketKnownContentTypes { +export class WebSocketContentTypes { /** * JSON request or response (default) */ - JSON = 'application/json', + public static readonly JSON = new WebSocketContentTypes('application/json'); /** * XML request or response */ - XML = 'application/xml', + public static readonly XML = new WebSocketContentTypes('application/xml'); /** * Pnain text request or response */ - TEXT = 'text/plain', + public static readonly TEXT = new WebSocketContentTypes('text/plain'); /** * URL encoded web form */ - FORM_URL_ENCODED = 'application/x-www-form-urlencoded', + public static readonly FORM_URL_ENCODED = new WebSocketContentTypes('application/x-www-form-urlencoded'); /** * Data from a web form */ - FORM_DATA = 'multipart/form-data' + public static readonly FORM_DATA = new WebSocketContentTypes('multipart/form-data'); + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** @@ -87,7 +125,7 @@ export interface WebSocketModelOptions { * * @default "application/json" */ - readonly contentType?: string; + readonly contentType?: WebSocketContentTypes; /** * The name of the model. @@ -152,7 +190,7 @@ export class WebSocketModel extends Resource implements IWebSocketModel { this.modelName = this.physicalName; this.resource = new CfnModel(this, 'Resource', { - contentType: props.contentType || WebSocketKnownContentTypes.JSON, + contentType: (props.contentType || WebSocketContentTypes.JSON).toString(), apiId: props.api.webSocketApiId, name: this.modelName, schema: JsonSchemaMapper.toCfnJsonSchema(props.schema), diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts index a645165db60f3..419821c0e76ef 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts @@ -8,21 +8,77 @@ import { IWebSocketModel } from './model'; /** * Defines a set of common response patterns known to the system */ -export enum WebSocketKnownRouteResponseKey { +export class WebSocketRouteResponseKey { /** - * Default response, when no other pattern matches + * Default route response, when no other pattern matches */ - DEFAULT = '$default', + public static readonly DEFAULT = new WebSocketRouteResponseKey('$default'); /** * Empty response */ - EMPTY = 'empty', + public static readonly EMPTY = new WebSocketRouteResponseKey('empty'); /** * Error response */ - ERROR = 'error' + public static readonly ERROR = new WebSocketRouteResponseKey('error'); + + /** + * Creates a custom route key + * @param value the name of the route key + */ + public static custom(value: string): WebSocketRouteResponseKey { + return new WebSocketRouteResponseKey(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } +} + +/** + * Known expressions for selecting a route in an API + */ +export class WebSocketRouteResponseModelSelectionExpression { + /** + * Default route, when no other pattern matches + */ + public static readonly DEFAULT = new WebSocketRouteResponseModelSelectionExpression('$default'); + + /** + * Creates a custom route key + * @param value the name of the route key + */ + public static custom(value: string): WebSocketRouteResponseModelSelectionExpression { + return new WebSocketRouteResponseModelSelectionExpression(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** @@ -65,7 +121,7 @@ export interface WebSocketRouteResponseOptions { * * @default - no selection expression */ - readonly modelSelectionExpression?: string; + readonly modelSelectionExpression?: WebSocketRouteResponseModelSelectionExpression; } /** @@ -85,7 +141,7 @@ export interface WebSocketRouteResponseProps extends WebSocketRouteResponseOptio /** * The route response key. */ - readonly key: string; + readonly key: WebSocketRouteResponseKey; } /** @@ -104,7 +160,7 @@ export class WebSocketRouteResponse extends Resource implements IWebSocketRouteR constructor(scope: Construct, id: string, props: WebSocketRouteResponseProps) { super(scope, id, { - physicalName: props.key || id, + physicalName: props.key?.toString() || id, }); let responseModels: { [key: string]: string } | undefined; @@ -117,9 +173,9 @@ export class WebSocketRouteResponse extends Resource implements IWebSocketRouteR this.resource = new CfnRouteResponse(this, 'Resource', { apiId: props.api.webSocketApiId, routeId: props.route.routeId, - routeResponseKey: props.key, + routeResponseKey: props.key.toString(), responseModels, - modelSelectionExpression: props.modelSelectionExpression, + modelSelectionExpression: props.modelSelectionExpression?.toString(), responseParameters: props.responseParameters, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts index 12241b4af8f38..e61292b069b01 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts @@ -6,7 +6,7 @@ import { IIntegration } from '../common/integration'; import { IRoute } from '../common/route'; import { IWebSocketApi } from './api'; import { IWebSocketModel } from './model'; -import { WebSocketRouteResponse, WebSocketRouteResponseOptions } from './route-response'; +import { WebSocketRouteResponse, WebSocketRouteResponseKey, WebSocketRouteResponseOptions } from './route-response'; /** * Available authorization providers for ApiGateway V2 APIs @@ -27,31 +27,117 @@ export enum WebSocketAuthorizationType { } /** - * Defines a set of common route keys known to the system + * Defines a set of common template patterns known to the system */ -export enum WebSocketKnownRouteKey { +export class WebSocketRouteKey { /** * Default route, when no other pattern matches */ - DEFAULT = '$default', + public static readonly DEFAULT = new WebSocketRouteKey('$default'); + /** * This route is a reserved route, used when a client establishes a connection to the WebSocket API */ - CONNECT = '$connect', + public static readonly CONNECT = new WebSocketRouteKey('$connect'); + /** * This route is a reserved route, used when a client disconnects from the WebSocket API */ - DISCONNECT = '$disconnect' + public static readonly DISCONNECT = new WebSocketRouteKey('$disconnect'); + + /** + * Creates a custom route key + * @param value the name of the route key + */ + public static custom(value: string): WebSocketRouteKey { + return new WebSocketRouteKey(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } +} + +/** + * Known expressions for selecting a route in an API + */ +export class WebSocketRouteResponseSelectionExpression { + /** + * Default route, when no other pattern matches + */ + public static readonly DEFAULT = new WebSocketRouteResponseSelectionExpression('$default'); + + /** + * Use the context route key + */ + public static readonly CONTEXT_ROUTE_KEY = new WebSocketRouteResponseSelectionExpression('${context.routeKey}'); + + /** + * Creates a custom route key + * @param value the name of the route key + */ + public static custom(value: string): WebSocketRouteResponseSelectionExpression { + return new WebSocketRouteResponseSelectionExpression(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** * Known expressions for selecting a route in an API */ -export enum WebSocketKnownRouteSelectionExpression { +export class WebSocketRouteModelSelectionExpression { /** - * Selects the route key from the request context + * Default route, when no other pattern matches */ - CONTEXT_ROUTE_KEY = '${context.routeKey}', + public static readonly DEFAULT = new WebSocketRouteModelSelectionExpression('$default'); + + /** + * Creates a custom route key + * @param value the name of the route key + */ + public static custom(value: string): WebSocketRouteModelSelectionExpression { + return new WebSocketRouteModelSelectionExpression(value); + } + + /** + * Contains the template key + */ + private readonly value: string; + private constructor(value: string) { + this.value = value; + } + + /** + * Returns the current value of the template key + */ + public toString(): string { + return this.value; + } } /** @@ -102,7 +188,7 @@ export interface WebSocketRouteOptions { * * @default - no selection key */ - readonly modelSelectionExpression?: string; + readonly modelSelectionExpression?: WebSocketRouteModelSelectionExpression; /** * The request models for the route. @@ -123,7 +209,7 @@ export interface WebSocketRouteOptions { * * @default - no selection expression */ - readonly routeResponseSelectionExpression?: string; + readonly routeResponseSelectionExpression?: WebSocketRouteResponseSelectionExpression; } /** @@ -133,7 +219,7 @@ export interface WebSocketRouteProps extends WebSocketRouteOptions { /** * The route key for the route. */ - readonly key: string; + readonly key: WebSocketRouteKey; /** * Defines the api for this route. @@ -177,7 +263,7 @@ export class WebSocketRoute extends Resource implements IRoute { /** * The key of this API Gateway Route. */ - public readonly key: string; + public readonly key: WebSocketRouteKey; protected api: IWebSocketApi; protected resource: CfnRoute; @@ -198,17 +284,17 @@ export class WebSocketRoute extends Resource implements IRoute { this.resource = new CfnRoute(this, 'Resource', { apiId: this.api.webSocketApiId, - routeKey: props.key, + routeKey: props.key.toString(), target: `integrations/${props.integration.integrationId}`, requestModels, authorizerId, apiKeyRequired: props.apiKeyRequired, authorizationScopes: props.authorizationScopes, authorizationType: props.authorizationType, - modelSelectionExpression: props.modelSelectionExpression, + modelSelectionExpression: props.modelSelectionExpression?.toString(), operationName: props.operationName, requestParameters: props.requestParameters, - routeResponseSelectionExpression: props.routeResponseSelectionExpression, + routeResponseSelectionExpression: props.routeResponseSelectionExpression?.toString(), }); this.routeId = this.resource.ref; } @@ -219,7 +305,7 @@ export class WebSocketRoute extends Resource implements IRoute { * @param key the key (predefined or not) that will select this response * @param props the properties for this response */ - public addResponse(key: string, props?: WebSocketRouteResponseOptions): WebSocketRouteResponse { + public addResponse(key: WebSocketRouteResponseKey, props?: WebSocketRouteResponseOptions): WebSocketRouteResponse { return new WebSocketRouteResponse(this, `Response.${key}`, { ...props, route: this, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts index 6ce502a6ec703..a44d5c8e4a959 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.ts @@ -22,7 +22,7 @@ def handler(event, context): // create a Web Socket API const api = new apigatewayv2.WebSocketApi(stack, 'WebSocketApi', { - routeSelectionExpression: '${request.body.action}', + routeSelectionExpression: apigatewayv2.WebSocketRouteSelectionExpression.custom('${request.body.action}'), deployOptions: { stageName: 'integration', }, @@ -32,11 +32,11 @@ const defaultStatusIntegrationResponse: apigatewayv2.WebSocketIntegrationRespons responseTemplates: { default: '#set($inputRoot = $input.path(\'$\')) { "status": "${inputRoot.status}", "message": "$util.escapeJavaScript(${inputRoot.message})" }', }, - templateSelectionExpression: 'default', + templateSelectionExpression: apigatewayv2.WebSocketIntegrationResponseTemplateSelectionExpressionKey.custom('default'), }; const defaultStatusRouteResponse: apigatewayv2.WebSocketRouteResponseOptions = { - modelSelectionExpression: 'default', + modelSelectionExpression: apigatewayv2.WebSocketRouteResponseModelSelectionExpression.custom('default'), responseModels: { default: api.addModel({ schema: apigatewayv2.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigatewayv2.JsonSchemaType.OBJECT, properties: { status: { type: apigatewayv2.JsonSchemaType.STRING }, message: { type: apigatewayv2.JsonSchemaType.STRING } } }), }, @@ -49,12 +49,12 @@ const webSocketConnectIntegration = api.addLambdaIntegration('default', { requestTemplates: { connect: '{ "action": "${context.routeKey}", "userId": "${context.identity.cognitoIdentityId}", "connectionId": "${context.connectionId}", "domainName": "${context.domainName}", "stageName": "${context.stage}" }', }, - templateSelectionExpression: 'connect', + templateSelectionExpression: apigatewayv2.WebSocketIntegrationTemplateSelectionExpression.custom('connect'), description: 'WebSocket Api Connection Integration', }); -webSocketConnectIntegration.addResponse(apigatewayv2.WebSocketKnownRouteResponseKey.DEFAULT, defaultStatusIntegrationResponse); +webSocketConnectIntegration.addResponse(apigatewayv2.WebSocketIntegrationResponseKey.DEFAULT, defaultStatusIntegrationResponse); -api.addRoute(apigatewayv2.WebSocketKnownRouteKey.CONNECT, webSocketConnectIntegration, { +api.addRoute(apigatewayv2.WebSocketRouteKey.CONNECT, webSocketConnectIntegration, { authorizationType: apigatewayv2.WebSocketAuthorizationType.IAM, - routeResponseSelectionExpression: apigatewayv2.WebSocketKnownRouteResponseKey.DEFAULT, -}).addResponse(apigatewayv2.WebSocketKnownRouteResponseKey.DEFAULT, defaultStatusRouteResponse); + routeResponseSelectionExpression: apigatewayv2.WebSocketRouteResponseSelectionExpression.DEFAULT, +}).addResponse(apigatewayv2.WebSocketRouteResponseKey.DEFAULT, defaultStatusRouteResponse); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts index 772bd83a4e251..ce559aafb0249 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api-mapping.test.ts @@ -11,7 +11,7 @@ test('minimal setup', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: true, }); const domainName = new apigw.DomainName(stack, 'domain-name', { domainName: 'test.example.com' }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts index 0c58f331159ca..5361ae4aeea4c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/api.test.ts @@ -11,7 +11,7 @@ test('minimal setup (WebSocket)', () => { // WHEN new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, }); // THEN @@ -38,7 +38,7 @@ test('minimal setup (WebSocket, no deploy)', () => { // WHEN new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); @@ -58,7 +58,7 @@ test('minimal setup (no deploy, error)', () => { // WHEN expect(() => { return new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, deployOptions: { stageName: 'testStage', @@ -72,7 +72,7 @@ test('URLs and ARNs (WebSocket)', () => { const stack = new Stack(); // WHEN - const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY }); + const api = new apigw.WebSocketApi(stack, 'my-api', { routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY }); const importedStage = apigw.WebSocketStage.fromStageName(stack, 'devStage', 'dev'); // THEN @@ -83,9 +83,9 @@ test('URLs and ARNs (WebSocket)', () => { expect(stack.resolve(api.connectionsUrl(importedStage))).toEqual({ 'Fn::Join': [ '', [ 'https://', { Ref: 'myapi4C7BF186' }, '.execute-api.', { Ref: 'AWS::Region' }, '.amazonaws.com/dev/@connections' ] ] }); expect(stack.resolve(api.executeApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/*' ] ] }); - expect(stack.resolve(api.executeApiArn('routeKey'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn(apigw.WebSocketRouteKey.custom('routeKey')))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/routeKey' ] ] }); expect(stack.resolve(api.executeApiArn(undefined, importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/*' ] ] }); - expect(stack.resolve(api.executeApiArn('routeKey', importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); + expect(stack.resolve(api.executeApiArn(apigw.WebSocketRouteKey.custom('routeKey'), importedStage))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/dev/routeKey' ] ] }); expect(stack.resolve(api.connectionsApiArn())).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/POST/*' ] ] }); expect(stack.resolve(api.connectionsApiArn('my-connection'))).toEqual({ 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':execute-api:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'myapi4C7BF186' }, '/', { Ref: 'myapiDefaultStage51F6D7C3' }, '/POST/my-connection' ] ] }); @@ -99,7 +99,7 @@ test('URLs and ARNs (WebSocket, no deploy)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const importedStage = apigw.WebSocketStage.fromStageName(stack, 'devStage', 'dev'); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts index 6968f741f972c..39b99519d9057 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/authorizer.test.ts @@ -11,7 +11,7 @@ test('minimal setup', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const functionArn = stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts index 5ddbf9da5bd64..42d5a963a8bad 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/integration.test.ts @@ -12,7 +12,7 @@ test('Http integration (WebSocket)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addHttpIntegration('myFunction', { @@ -33,7 +33,7 @@ test('Http integration (WebSocket, proxy)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addHttpIntegration('myFunction', { @@ -55,7 +55,7 @@ test('Lambda integration (WebSocket)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addLambdaIntegration('myFunction', { @@ -76,7 +76,7 @@ test('Lambda integration (WebSocket, proxy)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addLambdaIntegration('myFunction', { @@ -98,7 +98,7 @@ test('Mock integration (WebSocket)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addMockIntegration('myMock', { }); @@ -117,7 +117,7 @@ test('Service integration (WebSocket)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addServiceIntegration('myService', { @@ -138,7 +138,7 @@ test('Service integration (WebSocket, proxy)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addServiceIntegration('myService', { @@ -160,7 +160,7 @@ test('Lambda integration (WebSocket, with extra params)', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); api.addLambdaIntegration('myFunction', { @@ -183,13 +183,13 @@ test('Integration response', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', stack.formatArn({ service: 'lambda', resource: 'function', resourceName: 'my-function', sep: ':'})), }); - integration.addResponse(apigw.WebSocketKnownIntegrationResponseKey.DEFAULT); + integration.addResponse(apigw.WebSocketIntegrationResponseKey.DEFAULT); // THEN cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::IntegrationResponse', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts index fc2e0bb9931d1..db460f15f0a2a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/route.test.ts @@ -12,18 +12,18 @@ test('route', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), }); - api.addRoute(apigw.WebSocketKnownRouteKey.CONNECT, integration, { - modelSelectionExpression: apigw.WebSocketKnownModelKey.DEFAULT, + api.addRoute(apigw.WebSocketRouteKey.CONNECT, integration, { + modelSelectionExpression: apigw.WebSocketRouteModelSelectionExpression.DEFAULT, requestModels: { - [apigw.WebSocketKnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusInputModel', type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }), + [apigw.WebSocketModelKey.DEFAULT.toString()]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusInputModel', type: apigw.JsonSchemaType.OBJECT, properties: { action: { type: apigw.JsonSchemaType.STRING } } }), }, - routeResponseSelectionExpression: apigw.WebSocketKnownRouteResponseKey.DEFAULT, + routeResponseSelectionExpression: apigw.WebSocketRouteResponseSelectionExpression.DEFAULT, }); // THEN @@ -39,7 +39,7 @@ test('route', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::Model', { ApiId: { Ref: 'myapi4C7BF186' }, - ContentType: apigw.WebSocketKnownContentTypes.JSON, + ContentType: 'application/json', Name: 'statusInputModel', })); }); @@ -50,17 +50,17 @@ test('route response', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const integration = api.addLambdaIntegration('myFunction', { handler: lambda.Function.fromFunctionArn(stack, 'handler', `arn:aws:lambda:${stack.region}:${stack.account}:function:my-function`), }); - const route = api.addRoute(apigw.WebSocketKnownRouteKey.CONNECT, integration, {}); - route.addResponse(apigw.WebSocketKnownRouteKey.CONNECT, { - modelSelectionExpression: apigw.WebSocketKnownModelKey.DEFAULT, + const route = api.addRoute(apigw.WebSocketRouteKey.CONNECT, integration, {}); + route.addResponse(apigw.WebSocketRouteResponseKey.DEFAULT, { + modelSelectionExpression: apigw.WebSocketRouteResponseModelSelectionExpression.DEFAULT, responseModels: { - [apigw.WebSocketKnownModelKey.DEFAULT]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }), + [apigw.WebSocketModelKey.DEFAULT.toString()]: api.addModel({ schema: apigw.JsonSchemaVersion.DRAFT4, title: 'statusResponse', type: apigw.JsonSchemaType.NUMBER, properties: { status: { type: apigw.JsonSchemaType.STRING }, message: { type: apigw.JsonSchemaType.STRING } } }), }, }); @@ -68,7 +68,7 @@ test('route response', () => { cdkExpect(stack).to(haveResource('AWS::ApiGatewayV2::RouteResponse', { ApiId: { Ref: 'myapi4C7BF186' }, RouteId: { Ref: 'myapiconnectrouteC62A8B0B' }, - RouteResponseKey: '$connect', + RouteResponseKey: '$default', ModelSelectionExpression: '$default', ResponseModels: { $default: 'statusResponse', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts index f033d28187674..41925cc63b3c4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/web-socket/stage.test.ts @@ -11,7 +11,7 @@ test('minimal setup', () => { // WHEN const api = new apigw.WebSocketApi(stack, 'my-api', { - routeSelectionExpression: apigw.WebSocketKnownRouteSelectionExpression.CONTEXT_ROUTE_KEY, + routeSelectionExpression: apigw.WebSocketRouteSelectionExpression.CONTEXT_ROUTE_KEY, deploy: false, }); const deployment = new apigw.WebSocketDeployment(stack, 'deployment', { From 8b6f2bdf6f8545e71aa56becf6e95785b627fe64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Wed, 1 Jul 2020 11:31:58 +0200 Subject: [PATCH 7/7] fix(aws-apigatewayv2): make rights granular per route in Lambda integrations Bind the IAM policies for Lambda functions ro the integration/route pair, instead of just to the API/Integration pair. --- .../lib/web-socket/integration.ts | 8 ++++++++ .../lib/web-socket/integrations/lambda.ts | 18 +++++++++++++++--- .../lib/web-socket/route-response.ts | 12 ++++++++++-- .../aws-apigatewayv2/lib/web-socket/route.ts | 16 ++++++++++++++-- .../test/integ.web-socket-api.expected.json | 7 +++++-- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts index 78d5e99ce8b4d..70fa0325a3e15 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integration.ts @@ -2,6 +2,7 @@ import { Construct, Duration, Resource } from '@aws-cdk/core'; import { CfnIntegration } from '../apigatewayv2.generated'; import { IIntegration } from '../common/integration'; +import { IRoute } from '../common/route'; import { IWebSocketApi } from './api'; import { WebSocketIntegrationResponse, WebSocketIntegrationResponseKey, WebSocketIntegrationResponseOptions } from './integration-response'; @@ -350,6 +351,13 @@ export class WebSocketIntegration extends Resource implements IIntegration { this.integrationId = this.resource.ref; } + /** + * Bind this integration to the route. + */ + public bind(_: IRoute) { + // Pass + } + /** * Creates a new response for this integration. * diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts index cf7e741d996c6..a426f3ad9ba1a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/integrations/lambda.ts @@ -2,8 +2,11 @@ import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; import { Construct, Stack } from '@aws-cdk/core'; +import { IRoute } from '../../common/route'; + import { IWebSocketApi, WebSocketApi } from '../api'; import { WebSocketIntegration, WebSocketIntegrationOptions, WebSocketIntegrationType } from '../integration'; +import { WebSocketRoute } from '../route'; /** * Defines the properties required for defining an Api Gateway V2 Lambda Integration. @@ -38,6 +41,8 @@ export interface WebSocketLambdaIntegrationProps extends WebSocketLambdaIntegrat * An AWS Lambda integration for an API in Amazon API Gateway v2. */ export class WebSocketLambdaIntegration extends WebSocketIntegration { + protected handler: IFunction; + constructor(scope: Construct, id: string, props: WebSocketLambdaIntegrationProps) { const stack = Stack.of(scope); @@ -60,9 +65,16 @@ export class WebSocketLambdaIntegration extends WebSocketIntegration { uri, }); - if (props.api instanceof WebSocketApi) { - const sourceArn = props.api.executeApiArn(); - props.handler.addPermission(`ApiPermission.${this.node.uniqueId}`, { + this.handler = props.handler; + } + + /** + * Bind this integration to the route. + */ + public bind(route: IRoute) { + if ((this.api instanceof WebSocketApi) && (route instanceof WebSocketRoute)) { + const sourceArn = this.api.executeApiArn(route.key); + this.handler.addPermission(`ApiPermission.${this.node.uniqueId}`, { principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts index 419821c0e76ef..6381b6371e8a7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route-response.ts @@ -1,9 +1,9 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnResource, Construct, IResource, Resource } from '@aws-cdk/core'; import { CfnRouteResponse } from '../apigatewayv2.generated'; import { IRoute } from '../common/route'; import { IWebSocketApi } from './api'; -import { IWebSocketModel } from './model'; +import { IWebSocketModel, WebSocketModel } from './model'; /** * Defines a set of common response patterns known to the system @@ -179,6 +179,14 @@ export class WebSocketRouteResponse extends Resource implements IWebSocketRouteR responseParameters: props.responseParameters, }); + if (props.responseModels !== undefined) { + for (const model of Object.values(props.responseModels)) { + if ((model instanceof WebSocketModel) && (model.node.defaultChild instanceof CfnResource)) { + this.resource.addDependsOn(model.node.defaultChild); + } + } + } + this.routeResponseId = this.resource.ref; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts index e61292b069b01..449dc29ae6bd5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/web-socket/route.ts @@ -1,11 +1,12 @@ -import { Construct, Resource } from '@aws-cdk/core'; +import { CfnResource, Construct, Resource } from '@aws-cdk/core'; import { CfnRoute } from '../apigatewayv2.generated'; import { IAuthorizer } from '../common/authorizer'; import { IIntegration } from '../common/integration'; import { IRoute } from '../common/route'; import { IWebSocketApi } from './api'; -import { IWebSocketModel } from './model'; +import { WebSocketIntegration } from './integration'; +import { IWebSocketModel, WebSocketModel } from './model'; import { WebSocketRouteResponse, WebSocketRouteResponseKey, WebSocketRouteResponseOptions } from './route-response'; /** @@ -282,6 +283,9 @@ export class WebSocketRoute extends Resource implements IRoute { })); } + if (props.integration instanceof WebSocketIntegration) { + props.integration.bind(this); + } this.resource = new CfnRoute(this, 'Resource', { apiId: this.api.webSocketApiId, routeKey: props.key.toString(), @@ -296,6 +300,14 @@ export class WebSocketRoute extends Resource implements IRoute { requestParameters: props.requestParameters, routeResponseSelectionExpression: props.routeResponseSelectionExpression?.toString(), }); + + if (props.requestModels !== undefined) { + for (const model of Object.values(props.requestModels)) { + if ((model instanceof WebSocketModel) && (model.node.defaultChild instanceof CfnResource)) { + this.resource.addDependsOn(model.node.defaultChild); + } + } + } this.routeId = this.resource.ref; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json index 2cb8528627126..398f8eaad34e3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2/test/integ.web-socket-api.expected.json @@ -85,7 +85,7 @@ { "Ref": "WebSocketApiDefaultStage734E750B" }, - "/*" + "/$connect" ] ] } @@ -239,7 +239,10 @@ "ResponseModels": { "default": "statusResponse" } - } + }, + "DependsOn": [ + "WebSocketApiModelstatusResponseB7B1089E" + ] } } } \ No newline at end of file