Skip to content

Commit 0b8c5f1

Browse files
authored
Merge pull request #46 from erezrokah/feat/allow_passing_method_request_parameters
feat: support customizing method request parameters
2 parents f36ef47 + 77d4a8a commit 0b8c5f1

13 files changed

+321
-6
lines changed

README.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ custom:
194194
requestParameters:
195195
# if requestParameters has a 'integration.request.path.object' property you should remove the key setting
196196
'integration.request.path.object': 'context.requestId'
197-
"integration.request.header.cache-control": "'public, max-age=31536000, immutable'"
197+
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
198198
```
199199

200200
### SNS
@@ -353,6 +353,34 @@ resources:
353353
Type: 'AWS::IAM::Role'
354354
```
355355

356+
### Customizing API Gateway parameters
357+
358+
The plugin allows one to specify which [parameters the API Gateway method accepts](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestparameters).
359+
360+
A common use case is to pass custom data to the integration request:
361+
362+
```yaml
363+
custom:
364+
apiGatewayServiceProxies:
365+
- sqs:
366+
path: /sqs
367+
method: post
368+
queueName: { 'Fn::GetAtt': ['SqsQueue', 'QueueName'] }
369+
cors: true
370+
acceptParameters:
371+
'method.request.header.Custom-Header': true
372+
requestParameters:
373+
'integration.request.querystring.MessageAttribute.1.Name': "'custom-Header'"
374+
'integration.request.querystring.MessageAttribute.1.Value.StringValue': 'method.request.header.Custom-Header'
375+
'integration.request.querystring.MessageAttribute.1.Value.DataType': "'String'"
376+
resources:
377+
Resources:
378+
SqsQueue:
379+
Type: 'AWS::SQS::Queue'
380+
```
381+
382+
Any published SQS message will have the `Custom-Header` value added as a message attribute.
383+
356384
### Customizing request body mapping templates
357385

358386
#### Kinesis
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
service: accept-parameters-proxy
2+
3+
provider:
4+
name: aws
5+
runtime: nodejs10.x
6+
7+
plugins:
8+
localPath: './../../../../../../'
9+
modules:
10+
- serverless-apigateway-service-proxy
11+
12+
custom:
13+
apiGatewayServiceProxies:
14+
- sqs:
15+
path: /sqs
16+
method: post
17+
queueName: { 'Fn::GetAtt': ['SqsQueue', 'QueueName'] }
18+
cors: true
19+
acceptParameters:
20+
'method.request.header.Custom-Header': true
21+
requestParameters:
22+
'integration.request.querystring.MessageAttribute.1.Name': "'custom-Header'"
23+
'integration.request.querystring.MessageAttribute.1.Value.StringValue': 'method.request.header.Custom-Header'
24+
'integration.request.querystring.MessageAttribute.1.Value.DataType': "'String'"
25+
26+
resources:
27+
Resources:
28+
SqsQueue:
29+
Type: 'AWS::SQS::Queue'
30+
Outputs:
31+
SqsQueueUrl:
32+
Value: { Ref: SqsQueue }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict'
2+
const AWS = require('aws-sdk')
3+
const expect = require('chai').expect
4+
5+
const fetch = require('node-fetch')
6+
const { deployWithRandomStage, removeService } = require('../../../utils')
7+
8+
describe('Single SQS Proxy Integration Test', () => {
9+
let endpoint
10+
let region
11+
let stage
12+
let queueUrl
13+
const config = '__tests__/integration/common/accept-parameters/service/serverless.yml'
14+
15+
beforeAll(async () => {
16+
const result = await deployWithRandomStage(config)
17+
18+
region = result.region
19+
stage = result.stage
20+
endpoint = result.endpoint
21+
queueUrl = result.outputs.SqsQueueUrl
22+
})
23+
24+
afterAll(() => {
25+
removeService(stage, config)
26+
})
27+
28+
it('should pass custom header to sqs message attribute', async () => {
29+
const testEndpoint = `${endpoint}/sqs`
30+
31+
const body = JSON.stringify({ message: 'test accept parameters' })
32+
const response = await fetch(testEndpoint, {
33+
method: 'POST',
34+
headers: { 'Content-Type': 'application/json', 'Custom-Header': 'custom header value' },
35+
body
36+
})
37+
38+
expect(response.headers.get('access-control-allow-origin')).to.deep.equal('*')
39+
expect(response.status).to.be.equal(200)
40+
41+
const json = await response.json()
42+
43+
expect(json.SendMessageResponse.SendMessageResult).to.have.own.property(
44+
'MD5OfMessageAttributes'
45+
)
46+
expect(json.SendMessageResponse.SendMessageResult).to.have.own.property('MD5OfMessageBody')
47+
expect(json.SendMessageResponse.SendMessageResult).to.have.own.property('MessageId')
48+
expect(json.SendMessageResponse.SendMessageResult).to.have.own.property('SequenceNumber')
49+
expect(json.SendMessageResponse.ResponseMetadata).to.have.own.property('RequestId')
50+
51+
const sqs = new AWS.SQS({ region })
52+
const { Messages = [] } = await sqs
53+
.receiveMessage({ QueueUrl: queueUrl, WaitTimeSeconds: 20, MessageAttributeNames: ['.*'] })
54+
.promise()
55+
56+
expect(Messages).to.have.length(1)
57+
expect(Messages[0].Body).to.deep.equal(body)
58+
59+
expect(Messages[0].MessageAttributes).to.deep.equal({
60+
'custom-Header': {
61+
StringValue: 'custom header value',
62+
StringListValues: [],
63+
BinaryListValues: [],
64+
DataType: 'String'
65+
}
66+
})
67+
})
68+
})

lib/apiGateway/schema.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,17 @@ const stringOrGetAtt = (propertyName, attributeName) => {
8989

9090
const roleArn = stringOrGetAtt('roleArn', 'Arn')
9191

92+
const acceptParameters = Joi.object().pattern(Joi.string(), Joi.boolean().required())
93+
9294
const proxy = Joi.object({
9395
path,
9496
method,
9597
cors,
9698
authorizationType,
9799
authorizerId,
98100
authorizationScopes,
99-
roleArn
101+
roleArn,
102+
acceptParameters
100103
})
101104
.oxor('authorizerId', 'authorizationScopes') // can have one of them, but not required
102105
.error(

lib/apiGateway/validate.test.js

+43
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,49 @@ describe('#validateServiceProxies()', () => {
673673
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
674674
})
675675

676+
it('should throw if "acceptParameters" is not a string to boolean mapping', () => {
677+
serverlessApigatewayServiceProxy.serverless.service.custom = {
678+
apiGatewayServiceProxies: [
679+
{
680+
kinesis: {
681+
path: '/kinesis',
682+
streamName: 'streamName',
683+
method: 'post',
684+
acceptParameters: {
685+
'method.request.header.customHeader1': 'this is not a boolean',
686+
'method.request.header.customHeader2': 1000
687+
}
688+
}
689+
}
690+
]
691+
}
692+
693+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
694+
serverless.classes.Error,
695+
'child "kinesis" fails because [child "acceptParameters" fails because [child "method.request.header.customHeader1" fails because ["method.request.header.customHeader1" must be a boolean]]'
696+
)
697+
})
698+
699+
it('should not throw if "acceptParameters" is a string to boolean mapping', () => {
700+
serverlessApigatewayServiceProxy.serverless.service.custom = {
701+
apiGatewayServiceProxies: [
702+
{
703+
kinesis: {
704+
path: '/kinesis',
705+
streamName: 'streamName',
706+
method: 'post',
707+
acceptParameters: {
708+
'method.request.header.customHeader1': true,
709+
'method.request.header.customHeader2': false
710+
}
711+
}
712+
}
713+
]
714+
}
715+
716+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
717+
})
718+
676719
const proxiesToTest = [
677720
{ proxy: 'kinesis', props: { streamName: 'streamName' } },
678721
{ proxy: 'sns', props: { topicName: 'topicName' } }

lib/package/kinesis/compileMethodsToKinesis.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = {
1313
Type: 'AWS::ApiGateway::Method',
1414
Properties: {
1515
HttpMethod: event.http.method.toUpperCase(),
16-
RequestParameters: {},
16+
RequestParameters: event.http.acceptParameters || {},
1717
AuthorizationType: event.http.auth.authorizationType,
1818
AuthorizationScopes: event.http.auth.authorizationScopes,
1919
AuthorizerId: event.http.auth.authorizerId,

lib/package/kinesis/compileMethodsToKinesis.test.js

+33
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,37 @@ describe('#compileMethodsToKinesis()', () => {
734734
const resource = serverlessApigatewayServiceProxy.getKinesisMethodIntegration(http)
735735
expect(resource.Properties.Integration.Credentials).to.be.equal('roleArn')
736736
})
737+
738+
it('should set RequestParameters to acceptParameters when configured', () => {
739+
serverlessApigatewayServiceProxy.validated = {
740+
events: [
741+
{
742+
serviceName: 'kinesis',
743+
http: {
744+
streamName: 'myStream',
745+
path: 'kinesis',
746+
method: 'post',
747+
auth: {
748+
authorizationType: 'NONE'
749+
},
750+
acceptParameters: { 'method.request.header.Custom-Header': true }
751+
}
752+
}
753+
]
754+
}
755+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
756+
serverlessApigatewayServiceProxy.apiGatewayResources = {
757+
kinesis: {
758+
name: 'kinesis',
759+
resourceLogicalId: 'ApiGatewayResourceKinesis'
760+
}
761+
}
762+
763+
serverlessApigatewayServiceProxy.compileMethodsToKinesis()
764+
765+
expect(
766+
serverless.service.provider.compiledCloudFormationTemplate.Resources
767+
.ApiGatewayMethodkinesisPost.Properties.RequestParameters
768+
).to.be.deep.equal({ 'method.request.header.Custom-Header': true })
769+
})
737770
})

lib/package/s3/compileMethodsToS3.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = {
1313
Type: 'AWS::ApiGateway::Method',
1414
Properties: {
1515
HttpMethod: event.http.method.toUpperCase(),
16-
RequestParameters: {},
16+
RequestParameters: event.http.acceptParameters || {},
1717
AuthorizationType: event.http.auth.authorizationType,
1818
AuthorizationScopes: event.http.auth.authorizationScopes,
1919
AuthorizerId: event.http.auth.authorizerId,

lib/package/s3/compileMethodsToS3.test.js

+42
Original file line numberDiff line numberDiff line change
@@ -639,4 +639,46 @@ describe('#compileMethodsToS3()', () => {
639639
const resource = serverlessApigatewayServiceProxy.getS3MethodIntegration(http)
640640
expect(resource.Properties.Integration.Credentials).to.be.equal('roleArn')
641641
})
642+
643+
it('should set RequestParameters to acceptParameters when configured', () => {
644+
serverlessApigatewayServiceProxy.validated = {
645+
events: [
646+
{
647+
serviceName: 's3',
648+
http: {
649+
path: 's3',
650+
method: 'post',
651+
bucket: {
652+
Ref: 'MyBucket'
653+
},
654+
action: 'PutObject',
655+
key: {
656+
pathParam: 'key'
657+
},
658+
cors: true,
659+
auth: { authorizationType: 'NONE' },
660+
acceptParameters: { 'method.request.header.Custom-Header': false }
661+
}
662+
}
663+
]
664+
}
665+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
666+
serverlessApigatewayServiceProxy.apiGatewayResources = {
667+
s3: {
668+
name: 's3',
669+
resourceLogicalId: 'ApiGatewayResourceS3'
670+
}
671+
}
672+
673+
serverlessApigatewayServiceProxy.compileMethodsToS3()
674+
675+
expect(
676+
serverless.service.provider.compiledCloudFormationTemplate.Resources.ApiGatewayMethods3Post
677+
.Properties.RequestParameters
678+
).to.be.deep.equal({
679+
'method.request.header.Custom-Header': false,
680+
'method.request.header.Content-Type': true,
681+
'method.request.path.key': true
682+
})
683+
})
642684
})

lib/package/sns/compileMethodsToSns.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = {
1313
Type: 'AWS::ApiGateway::Method',
1414
Properties: {
1515
HttpMethod: event.http.method.toUpperCase(),
16-
RequestParameters: {},
16+
RequestParameters: event.http.acceptParameters || {},
1717
AuthorizationType: event.http.auth.authorizationType,
1818
AuthorizationScopes: event.http.auth.authorizationScopes,
1919
AuthorizerId: event.http.auth.authorizerId,

lib/package/sns/compileMethodsToSns.test.js

+33
Original file line numberDiff line numberDiff line change
@@ -657,4 +657,37 @@ describe('#compileMethodsToSns()', () => {
657657
const resource = serverlessApigatewayServiceProxy.getSnsMethodIntegration(http)
658658
expect(resource.Properties.Integration.Credentials).to.be.equal('roleArn')
659659
})
660+
661+
it('should set RequestParameters to acceptParameters when configured', () => {
662+
serverlessApigatewayServiceProxy.validated = {
663+
events: [
664+
{
665+
serviceName: 'sns',
666+
http: {
667+
topicName: 'myTopic',
668+
path: 'sns',
669+
method: 'post',
670+
auth: {
671+
authorizationType: 'NONE'
672+
},
673+
acceptParameters: { 'method.request.header.Custom-Header': true }
674+
}
675+
}
676+
]
677+
}
678+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
679+
serverlessApigatewayServiceProxy.apiGatewayResources = {
680+
sns: {
681+
name: 'Sns',
682+
resourceLogicalId: 'ApiGatewayResourceSns'
683+
}
684+
}
685+
686+
serverlessApigatewayServiceProxy.compileMethodsToSns()
687+
688+
expect(
689+
serverless.service.provider.compiledCloudFormationTemplate.Resources.ApiGatewayMethodSnsPost
690+
.Properties.RequestParameters
691+
).to.be.deep.equal({ 'method.request.header.Custom-Header': true })
692+
})
660693
})

lib/package/sqs/compileMethodsToSqs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = {
1313
Type: 'AWS::ApiGateway::Method',
1414
Properties: {
1515
HttpMethod: event.http.method.toUpperCase(),
16-
RequestParameters: {},
16+
RequestParameters: event.http.acceptParameters || {},
1717
AuthorizationScopes: event.http.auth.authorizationScopes,
1818
AuthorizationType: event.http.auth.authorizationType,
1919
AuthorizerId: event.http.auth.authorizerId,

0 commit comments

Comments
 (0)