From 0452aed2f00198e05bd65b1d20246f7de0b24e20 Mon Sep 17 00:00:00 2001 From: Daniel Lindberg Date: Wed, 13 Oct 2021 17:01:00 +0200 Subject: [PATCH] feat(apigatewayv2-integrations): http api - support for request parameter mapping (#15630) ---- This is an initial PR as discussed with @nija-at in an attempt to describe the user experience for supporting parameter mapping. This PR will only support parameter mappings for HTTP APIs _without_ an integration subtypes, but it will provide interfaces that can (and probably should) be reused when adding support for integration subtypes as well. Since it also provides the possibility to provide custom key/value pairs for maximum flexibility, it can support and integration subtype although it requires a bit more work on the user side. closes #15293 *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigatewayv2-integrations/README.md | 35 +++++ .../lib/http/alb.ts | 1 + .../lib/http/base-types.ts | 9 +- .../lib/http/http-proxy.ts | 9 ++ .../lib/http/lambda.ts | 9 ++ .../lib/http/nlb.ts | 1 + .../lib/http/service-discovery.ts | 1 + .../test/http/alb.test.ts | 32 +++- .../test/http/http-proxy.test.ts | 24 ++- .../test/http/lambda.test.ts | 24 ++- .../test/http/nlb.test.ts | 32 +++- .../test/http/service-discovery.test.ts | 36 ++++- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 1 + .../aws-apigatewayv2/lib/http/integration.ts | 16 ++ .../@aws-cdk/aws-apigatewayv2/lib/index.ts | 1 + .../aws-apigatewayv2/lib/parameter-mapping.ts | 145 ++++++++++++++++++ .../aws-apigatewayv2/test/http/route.test.ts | 51 +++++- 17 files changed, 419 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index cce77fd6398e6..14eb72a56e7a4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -21,6 +21,7 @@ - [Lambda Integration](#lambda) - [HTTP Proxy Integration](#http-proxy) - [Private Integration](#private-integration) + - [Request Parameters](#request-parameters) - [WebSocket APIs](#websocket-apis) - [Lambda WebSocket Integration](#lambda-websocket-integration) @@ -149,6 +150,40 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { }); ``` +### Request Parameters + +Request parameter mapping allows API requests from clients to be modified before they reach backend integrations. +Parameter mapping can be used to specify modifications to request parameters. See [Transforming API requests and +responses](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html). + +The following example creates a new header - `header2` - as a copy of `header1` and removes `header1`. + +```ts +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpAlbIntegration({ + // ... + requestParameters: new ParameterMapping() + .appendHeader('header2', MappingValue.header('header1')) + .removeHeader('header1'); + }), + }), +}); +``` + +To add mapping keys and values not yet supported by the CDK, use the `custom()` method: + +```ts +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpAlbIntegration({ + listener, + requestParameters: new ParameterMapping() + .custom('myKey', 'myValue'), + }), + }), +}); +``` + + ## WebSocket APIs WebSocket integrations connect a route to backend resources. The following integrations are supported in the CDK. diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts index 656e0a550408f..e5e6d5c448663 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts @@ -44,6 +44,7 @@ export class HttpAlbIntegration extends HttpPrivateIntegration { connectionId: vpcLink.vpcLinkId, uri: this.props.listener.listenerArn, secureServerName: this.props.secureServerName, + parameterMapping: this.props.parameterMapping, }; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts index db14e50f7fc54..1627b9b0c4deb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts @@ -1,4 +1,4 @@ -import { HttpMethod, IVpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpMethod, IVpcLink, ParameterMapping } from '@aws-cdk/aws-apigatewayv2'; /** * Base options for private integration @@ -24,4 +24,11 @@ export interface HttpPrivateIntegrationOptions { */ readonly secureServerName?: string; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts index a7ef2d1b4d7b9..70873c9582fc8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts @@ -4,6 +4,7 @@ import { HttpRouteIntegrationConfig, HttpMethod, IHttpRouteIntegration, + ParameterMapping, PayloadFormatVersion, } from '@aws-cdk/aws-apigatewayv2'; @@ -21,6 +22,13 @@ export interface HttpProxyIntegrationProps { * @default HttpMethod.ANY */ readonly method?: HttpMethod; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } /** @@ -36,6 +44,7 @@ export class HttpProxyIntegration implements IHttpRouteIntegration { payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format type: HttpIntegrationType.HTTP_PROXY, uri: this.props.url, + parameterMapping: this.props.parameterMapping, }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index 220d3dca57210..358263f724bda 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -4,6 +4,7 @@ import { HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion, + ParameterMapping, } from '@aws-cdk/aws-apigatewayv2'; import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; @@ -24,6 +25,13 @@ export interface LambdaProxyIntegrationProps { * @default PayloadFormatVersion.VERSION_2_0 */ readonly payloadFormatVersion?: PayloadFormatVersion; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } /** @@ -50,6 +58,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { type: HttpIntegrationType.LAMBDA_PROXY, uri: this.props.handler.functionArn, payloadFormatVersion: this.props.payloadFormatVersion ?? PayloadFormatVersion.VERSION_2_0, + parameterMapping: this.props.parameterMapping, }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts index 1c405b51b3bfd..7aae0aa002354 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts @@ -44,6 +44,7 @@ export class HttpNlbIntegration extends HttpPrivateIntegration { connectionId: vpcLink.vpcLinkId, uri: this.props.listener.listenerArn, secureServerName: this.props.secureServerName, + parameterMapping: this.props.parameterMapping, }; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts index f9f204b6eba3e..6f3b8eedbea0a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts @@ -34,6 +34,7 @@ export class HttpServiceDiscoveryIntegration extends HttpPrivateIntegration { connectionId: this.props.vpcLink.vpcLinkId, uri: this.props.service.serviceArn, secureServerName: this.props.secureServerName, + parameterMapping: this.props.parameterMapping, }; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts index e5871da260bc2..3c25e92fe6d21 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink, ParameterMapping, MappingValue } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -143,4 +143,34 @@ describe('HttpAlbIntegration', () => { }, }); }); + + test('parameterMapping option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpAlbIntegration({ + listener, + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts index 0c76996fe7867..0f29ec0915fd9 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpIntegration, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteKey, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpIntegration, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; import { Stack } from '@aws-cdk/core'; import { HttpProxyIntegration } from '../../lib'; @@ -71,4 +71,26 @@ describe('HttpProxyIntegration', () => { IntegrationUri: 'some-target-url', }); }); + + test('parameterMapping is correctly recognized', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpIntegration(stack, 'HttpInteg', { + httpApi: api, + integrationType: HttpIntegrationType.HTTP_PROXY, + integrationUri: 'some-target-url', + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'some-target-url', + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts index d0ead43945ec4..85bb624a25d54 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpRoute, HttpRouteKey, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; import { App, Stack } from '@aws-cdk/core'; import { LambdaProxyIntegration } from '../../lib'; @@ -41,6 +41,28 @@ describe('LambdaProxyIntegration', () => { }); }); + test('parameterMapping selection', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'LambdaProxyRoute', { + httpApi: api, + integration: new LambdaProxyIntegration({ + handler: fooFunction(stack, 'Fn'), + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); + test('no dependency cycles', () => { const app = new App(); const lambdaStack = new Stack(app, 'lambdaStack'); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts index a32d448d8e448..e1e18c43f49aa 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, VpcLink } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -140,4 +140,34 @@ describe('HttpNlbIntegration', () => { }, }); }); + + test('paramaterMapping option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpNlbIntegration({ + listener, + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts index 4d3bef328a637..e037004cada0e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, VpcLink } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; import { Stack } from '@aws-cdk/core'; @@ -125,4 +125,38 @@ describe('HttpServiceDiscoveryIntegration', () => { }, }); }); + + test('parameterMapping option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', + vpc, + }); + const service = namespace.createService('Service'); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpServiceDiscoveryIntegration({ + vpcLink, + service, + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 254a29ea6d28b..32dfe0a0c2120 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -305,6 +305,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, secureServerName: config.secureServerName, + parameterMapping: config.parameterMapping, }); this._integrationCache.saveIntegration(scope, config, integration); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index f832b5b7e3b21..df0cf84c13da0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -3,6 +3,7 @@ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; import { IIntegration } from '../common'; +import { ParameterMapping } from '../parameter-mapping'; import { IHttpApi } from './api'; import { HttpMethod, IHttpRoute } from './route'; @@ -128,6 +129,13 @@ export interface HttpIntegrationProps { * @default undefined private integration traffic will use HTTP protocol */ readonly secureServerName?: string; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } /** @@ -149,6 +157,7 @@ export class HttpIntegration extends Resource implements IHttpIntegration { connectionId: props.connectionId, connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, + requestParameters: props.parameterMapping?.mappings, }); if (props.secureServerName) { @@ -237,4 +246,11 @@ export interface HttpRouteIntegrationConfig { * @default undefined private integration traffic will use HTTP protocol */ readonly secureServerName?: string; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts index 12dd8113f8b4c..81df171d98aa1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -2,3 +2,4 @@ export * from './apigatewayv2.generated'; export * from './common'; export * from './http'; export * from './websocket'; +export * from './parameter-mapping'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts new file mode 100644 index 0000000000000..deb967d572de2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts @@ -0,0 +1,145 @@ +/** + * Represents a Mapping Value. + */ +export interface IMappingValue { + /** + * Represents a Mapping Value. + */ + readonly value: string; +}; + +/** + * Represents a Mapping Value. + */ +export class MappingValue implements IMappingValue { + /** + * Creates an empty mapping value. + */ + public static readonly NONE = new MappingValue(''); + + /** + * Creates a header mapping value. + */ + public static requestHeader(name: string) { return new MappingValue(`$request.header.${name}`); } + + /** + * Creates a query string mapping value. + */ + public static requestQueryString(name: string) { return new MappingValue(`$request.querystring.${name}`); } + + /** + * Creates a request body mapping value. + */ + public static requestBody(name: string) { return new MappingValue(`$request.body.${name}`); } + + /** + * Creates a request path mapping value. + */ + public static requestPath() { return new MappingValue('$request.path'); } + + /** + * Creates a request path parameter mapping value. + */ + public static requestPathParam(name: string) { return new MappingValue(`$request.path.${name}`); } + + /** + * Creates a context variable mapping value. + */ + public static contextVariable(variableName: string) { return new MappingValue(`$context.${variableName}`); } + + /** + * Creates a stage variable mapping value. + */ + public static stageVariable(variableName: string) { return new MappingValue(`$stageVariables.${variableName}`); } + + /** + * Creates a custom mapping value. + */ + public static custom(value: string) { return new MappingValue(value); } + + /** + * Represents a Mapping Value. + */ + public readonly value: string + + protected constructor(value: string) { + this.value = value; + } +} + +/** + * Represents a Parameter Mapping. + */ +export class ParameterMapping { + /** + * Represents all created parameter mappings. + */ + public readonly mappings: { [key: string]: string } + constructor() { + this.mappings = {}; + } + + /** + * Creates a mapping to append a header. + */ + public appendHeader(name: string, value: MappingValue): ParameterMapping { + this.mappings[`append:header.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to overwrite a header. + */ + public overwriteHeader(name: string, value: MappingValue): ParameterMapping { + this.mappings[`overwrite:header.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to remove a header. + */ + public removeHeader(name: string): ParameterMapping { + this.mappings[`remove:header.${name}`] = ''; + return this; + } + + /** + * Creates a mapping to append a query string. + */ + public appendQueryString(name: string, value: MappingValue): ParameterMapping { + this.mappings[`append:querystring.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to overwrite a querystring. + */ + public overwriteQueryString(name: string, value: MappingValue): ParameterMapping { + this.mappings[`overwrite:querystring.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to remove a querystring. + */ + public removeQueryString(name: string): ParameterMapping { + this.mappings[`remove:querystring.${name}`] = ''; + return this; + } + + /** + * Creates a mapping to overwrite a path. + */ + public overwritePath(value: MappingValue): ParameterMapping { + this.mappings['overwrite:path'] = value.value; + return this; + } + + /** + * Creates a custom mapping. + */ + public custom(key: string, value: string): ParameterMapping { + this.mappings[key] = value; + return this; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 9f64cfdfbd123..75d744b6b5bcc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,8 +1,11 @@ import { Template } from '@aws-cdk/assertions'; import { Stack, App } from '@aws-cdk/core'; import { - HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteAuthorizerBindOptions, - HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, + HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, + MappingValue, + ParameterMapping, + PayloadFormatVersion, } from '../../lib'; describe('HttpRoute', () => { @@ -174,6 +177,9 @@ describe('HttpRoute', () => { connectionType: HttpConnectionType.VPC_LINK, uri: 'some-target-arn', secureServerName: 'some-server-name', + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), }; } } @@ -201,6 +207,47 @@ describe('HttpRoute', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::VpcLink', 0); }); + test('configures private integration correctly when parameter mappings are passed', () => { + // GIVEN + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + class PrivateIntegration implements IHttpRouteIntegration { + public bind(): HttpRouteIntegrationConfig { + return { + method: HttpMethod.ANY, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + type: HttpIntegrationType.HTTP_PROXY, + uri: 'some-target-arn', + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }; + } + } + + // WHEN + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new PrivateIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationMethod: 'ANY', + IntegrationUri: 'some-target-arn', + PayloadFormatVersion: '1.0', + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::VpcLink', 0); + }); + test('can create route with an authorizer attached', () => { const stack = new Stack(); const httpApi = new HttpApi(stack, 'HttpApi');