Skip to content

Commit

Permalink
feat(apigatewayv2-integrations): http api - support for request param…
Browse files Browse the repository at this point in the history
…eter 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*
  • Loading branch information
dan-lind authored Oct 13, 2021
1 parent 59950dd commit 0452aed
Show file tree
Hide file tree
Showing 17 changed files with 419 additions and 8 deletions.
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2-integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HttpRouteIntegrationConfig,
HttpMethod,
IHttpRouteIntegration,
ParameterMapping,
PayloadFormatVersion,
} from '@aws-cdk/aws-apigatewayv2';

Expand All @@ -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;
}

/**
Expand All @@ -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,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}

/**
Expand All @@ -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,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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': '',
},
});
});
});
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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': '',
},
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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': '',
},
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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': '',
},
});
});
});
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading

0 comments on commit 0452aed

Please sign in to comment.