Skip to content

Commit ff2ff8c

Browse files
authored
Merge pull request #86 from metrics-credit-partners/master
feat(sqs): add request template support for sqs
2 parents fcac549 + 9c1278a commit ff2ff8c

File tree

5 files changed

+468
-22
lines changed

5 files changed

+468
-22
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ This Serverless Framework plugin supports the AWS service proxy integration feat
2929
- [Customizing API Gateway parameters](#customizing-api-gateway-parameters)
3030
- [Customizing request body mapping templates](#customizing-request-body-mapping-templates)
3131
- [Kinesis](#kinesis-1)
32+
- [SQS](#sqs-1)
3233
- [SNS](#sns-1)
3334

3435
## Install
@@ -156,6 +157,12 @@ custom:
156157
'integration.request.querystring.MessageAttribute.2.Value.DataType': "'String'"
157158
```
158159

160+
The preferred way to pass `MessageAttribute` parameters is via a request body mapping template. Any `requestParameters` keys that begin with `integration.request.querystring.` will be automatically placed in the request body to maintain backward compatibility with existing implementations.
161+
162+
#### Customizing request body mapping templates
163+
164+
See the [SQS section](#sqs-1) under [Customizing request body mapping templates](#customizing-request-body-mapping-templates)
165+
159166
#### Customizing responses
160167

161168
##### Simplified response template customization
@@ -705,6 +712,36 @@ custom:
705712

706713
Source: [How to connect SNS to Kinesis for cross-account delivery via API Gateway](https://theburningmonk.com/2019/07/how-to-connect-sns-to-kinesis-for-cross-account-delivery-via-api-gateway/)
707714

715+
#### SQS
716+
717+
Customizing SQS request templates requires us to force all requests to use an `application/x-www-form-urlencoded` style body. The plugin sets the `Content-Type` to `application/x-www-form-urlencoded` for you, but API Gateway will still look for the template under the `application/json` request template type, so that is where you need to configure you request body in `serverless.yml`:
718+
719+
```yml
720+
custom:
721+
apiGatewayServiceProxies:
722+
- sqs:
723+
path: /{version}/event/receiver
724+
method: post
725+
queueName: { 'Fn::GetAtt': ['SqsQueue', 'QueueName'] }
726+
request:
727+
template:
728+
application/json: |-
729+
#set ($body = $util.parseJson($input.body))
730+
Action=SendMessage##
731+
&MessageGroupId=$util.urlEncode($body.event_type)##
732+
&MessageDeduplicationId=$util.urlEncode($body.event_id)##
733+
&MessageAttribute.1.Name=$util.urlEncode("X-Custom-Signature")##
734+
&MessageAttribute.1.Value.DataType=String##
735+
&MessageAttribute.1.Value.StringValue=$util.urlEncode($input.params("X-Custom-Signature"))##
736+
&MessageBody=$util.urlEncode($input.body)
737+
```
738+
739+
Note that the `##` at the end of each line is an empty comment. In VTL this has the effect of stripping the newline from the end of the line (as it is commented out), which makes API Gateway read all the lines in the template as one line.
740+
741+
Be careful when mixing additional `requestParameters` into your SQS endpoint as you may overwrite the `integration.request.header.Content-Type` and stop the request template from being parsed correctly. You may also unintentionally create conflicts between parameters passed using `requestParameters` and those in your request template. Typically you should only use the request template if you need to manipulate the incoming request body in some way.
742+
743+
Your custom template must also set the `Action` and `MessageBody` parameters, as these will not be added for you by the plugin.
744+
708745
#### SNS
709746

710747
Similar to the [Kinesis](#kinesis-1) support, you can customize the default request mapping templates in `serverless.yml` like so:

lib/apiGateway/schema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ const proxiesSchemas = {
266266
sqs: proxy.append({
267267
queueName: stringOrGetAtt('queueName', 'QueueName').required(),
268268
requestParameters,
269+
request,
269270
response: sqsResponse
270271
})
271272
}),

lib/apiGateway/validate.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,63 @@ describe('#validateServiceProxies()', () => {
15551555

15561556
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
15571557
})
1558+
1559+
it('should throw error if request is missing the template property', () => {
1560+
serverlessApigatewayServiceProxy.serverless.service.custom = {
1561+
apiGatewayServiceProxies: [
1562+
{
1563+
sqs: {
1564+
path: '/sqs',
1565+
method: 'post',
1566+
queueName: 'queueName',
1567+
request: { xxx: { 'application/json': 'mappingTemplate' } }
1568+
}
1569+
}
1570+
]
1571+
}
1572+
1573+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
1574+
serverless.classes.Error,
1575+
'child "sqs" fails because [child "request" fails because [child "template" fails because ["template" is required]]]'
1576+
)
1577+
})
1578+
1579+
it('should throw error if request is not a mapping template object', () => {
1580+
serverlessApigatewayServiceProxy.serverless.service.custom = {
1581+
apiGatewayServiceProxies: [
1582+
{
1583+
sqs: {
1584+
path: '/sqs',
1585+
method: 'post',
1586+
queueName: 'queueName',
1587+
request: { template: [] }
1588+
}
1589+
}
1590+
]
1591+
}
1592+
1593+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
1594+
serverless.classes.Error,
1595+
'child "sqs" fails because [child "request" fails because [child "template" fails because ["template" must be an object]]]'
1596+
)
1597+
})
1598+
1599+
it('should not throw error if request is a mapping template object', () => {
1600+
serverlessApigatewayServiceProxy.serverless.service.custom = {
1601+
apiGatewayServiceProxies: [
1602+
{
1603+
sqs: {
1604+
path: '/sqs',
1605+
method: 'post',
1606+
queueName: 'queueName',
1607+
request: { template: { 'application/json': 'mappingTemplate' } }
1608+
}
1609+
}
1610+
]
1611+
}
1612+
1613+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
1614+
})
15581615
})
15591616

15601617
describe('dynamodb', () => {

lib/package/sqs/compileMethodsToSqs.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
const _ = require('lodash')
44

5+
const requestParameterIsQuerystring = (_value, parameter) =>
6+
parameter.trim().startsWith('integration.request.querystring.')
7+
58
module.exports = {
69
compileMethodsToSqs() {
710
this.validated.events.forEach((event) => {
@@ -58,14 +61,14 @@ module.exports = {
5861
{ queueName: http.queueName }
5962
]
6063
},
64+
PassthroughBehavior: 'NEVER',
6165
RequestParameters: _.merge(
6266
{
63-
'integration.request.querystring.Action': "'SendMessage'",
64-
'integration.request.querystring.MessageBody': 'method.request.body'
67+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
6568
},
66-
http.requestParameters
69+
_.omitBy(http.requestParameters, requestParameterIsQuerystring)
6770
),
68-
RequestTemplates: { 'application/json': '{statusCode:200}' }
71+
RequestTemplates: this.getSqsIntegrationRequestTemplates(http)
6972
}
7073

7174
let integrationResponse
@@ -140,6 +143,45 @@ module.exports = {
140143
}
141144
},
142145

146+
getSqsIntegrationRequestTemplates(http) {
147+
const defaultRequestTemplates = this.getDefaultSqsRequestTemplates(http)
148+
const customRequestTemplates = _.get(http, ['request', 'template'])
149+
let requestParametersQuerystrings = _.pickBy(
150+
http.requestParameters,
151+
requestParameterIsQuerystring
152+
)
153+
154+
if (_.isEmpty(customRequestTemplates) && !_.isEmpty(requestParametersQuerystrings)) {
155+
requestParametersQuerystrings = _.map(requestParametersQuerystrings, (value, parameter) => {
156+
return [
157+
parameter.trim().replace('integration.request.querystring.', ''),
158+
value
159+
.trim()
160+
.replace(/^context\.(.+)$/, '$$util.urlEncode($$context.$1)')
161+
.replace(/(^')|('$)/g, '')
162+
].join('=')
163+
})
164+
165+
return {
166+
'application/json': `${
167+
defaultRequestTemplates['application/json']
168+
}&${requestParametersQuerystrings.join('&')}`
169+
}
170+
}
171+
172+
return Object.assign(defaultRequestTemplates, customRequestTemplates)
173+
},
174+
175+
getDefaultSqsRequestTemplates() {
176+
return {
177+
'application/json': this.buildDefaultSqsRequestTemplate()
178+
}
179+
},
180+
181+
buildDefaultSqsRequestTemplate() {
182+
return 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
183+
},
184+
143185
getSQSIntegrationResponseTemplate(http, statusType) {
144186
const template = _.get(http, ['response', 'template', statusType])
145187
return Object.assign({}, template && { 'application/json': template })

0 commit comments

Comments
 (0)