Skip to content

Commit be672a9

Browse files
committed
feat(sqs): add request template support for sqs
Allows the addition of `request.template.<content-type>` to `serverless.yml` under `apiGatewayServiceProxies`. Changes the approach to passing basic `Action` and `MessageBody` values by passing them in the POST request payload instead of via querystrings. This means that the prior default `RequestParameters` have been replaced with a single default which sets the `Content-Type` to `application/x-www-form-urlencoded`. An example config which satisfies the requirements for an SQS FIFO queue follows. Given a request payload of: ```json { "event_type": "message", "event_id": "ABC123", "message": "Hello!" } ``` Then the following config inside `serverless.yml`: ```yaml apiGatewayServiceProxies: - sqs: path: /{version}/slack/receiver method: post queueName: Fn::GetAtt: - eventQueue - "QueueName" request: template: application/json: |- #set ($body = $util.parseJson($input.body)) Action=SendMessage## &MessageGroupId=$util.urlEncode($body.event_type)## &MessageDeduplicationId=$util.urlEncode($body.event_id)## &MessageAttribute.1.Name=$util.urlEncode("X-Custom-Signature")## &MessageAttribute.1.Value.DataType=String## &MessageAttribute.1.Value.StringValue=$util.urlEncode($input.params("X-Custom-Signature"))## &MessageBody=$util.urlEncode($input.body) ``` Will produce the following request body (substituting some values with tags): ``` Action=SendMessage&MessageGroupId=message&MessageDeduplicationId=ABC123&MessageAttribute.1.Name=X-Custom-Signature&MessageAttribute.1.Value.DataType=String&MessageAttribute.1.Value.StringValue=<related-header-value>&MessageBody=<url-encoded-request-payload> ``` It is usually possible to omit the url encoding of the `$input.body` as SQS does not seem to care too much about that, but I have not tested with any potentially problematic characters.
1 parent 9121de8 commit be672a9

File tree

4 files changed

+198
-22
lines changed

4 files changed

+198
-22
lines changed

lib/apiGateway/schema.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ const proxiesSchemas = {
247247
sqs: Joi.object({
248248
sqs: proxy.append({
249249
queueName: stringOrGetAtt('queueName', 'QueueName').required(),
250-
requestParameters
250+
requestParameters,
251+
request
251252
})
252253
}),
253254
dynamodb: Joi.object({

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: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ module.exports = {
5858
{ queueName: http.queueName }
5959
]
6060
},
61+
PassthroughBehavior: 'NEVER',
6162
RequestParameters: _.merge(
6263
{
63-
'integration.request.querystring.Action': "'SendMessage'",
64-
'integration.request.querystring.MessageBody': 'method.request.body'
64+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
6565
},
6666
http.requestParameters
6767
),
68-
RequestTemplates: { 'application/json': '{statusCode:200}' }
68+
RequestTemplates: this.getSqsIntegrationRequestTemplates(http)
6969
}
7070

7171
const integrationResponse = {
@@ -100,5 +100,20 @@ module.exports = {
100100
Integration: integration
101101
}
102102
}
103+
},
104+
105+
getSqsIntegrationRequestTemplates(http) {
106+
const defaultRequestTemplates = this.getDefaultSqsRequestTemplates()
107+
return Object.assign(defaultRequestTemplates, _.get(http, ['request', 'template']))
108+
},
109+
110+
getDefaultSqsRequestTemplates() {
111+
return {
112+
'application/json': this.buildDefaultSqsRequestTemplate()
113+
}
114+
},
115+
116+
buildDefaultSqsRequestTemplate() {
117+
return 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
103118
}
104119
}

lib/package/sqs/compileMethodsToSqs.test.js

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ describe('#compileMethodsToSqs()', () => {
7373
}
7474
]
7575
},
76+
PassthroughBehavior: 'NEVER',
7677
RequestParameters: {
77-
'integration.request.querystring.Action': "'SendMessage'",
78-
'integration.request.querystring.MessageBody': 'method.request.body'
78+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
79+
},
80+
RequestTemplates: {
81+
'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
7982
},
80-
RequestTemplates: { 'application/json': '{statusCode:200}' },
8183
IntegrationResponses: [
8284
{
8385
StatusCode: 200,
@@ -173,11 +175,13 @@ describe('#compileMethodsToSqs()', () => {
173175
}
174176
]
175177
},
178+
PassthroughBehavior: 'NEVER',
176179
RequestParameters: {
177-
'integration.request.querystring.Action': "'SendMessage'",
178-
'integration.request.querystring.MessageBody': 'method.request.body'
180+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
181+
},
182+
RequestTemplates: {
183+
'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
179184
},
180-
RequestTemplates: { 'application/json': '{statusCode:200}' },
181185
IntegrationResponses: [
182186
{
183187
StatusCode: 200,
@@ -272,11 +276,13 @@ describe('#compileMethodsToSqs()', () => {
272276
}
273277
]
274278
},
279+
PassthroughBehavior: 'NEVER',
275280
RequestParameters: {
276-
'integration.request.querystring.Action': "'SendMessage'",
277-
'integration.request.querystring.MessageBody': 'method.request.body'
281+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
282+
},
283+
RequestTemplates: {
284+
'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
278285
},
279-
RequestTemplates: { 'application/json': '{statusCode:200}' },
280286
IntegrationResponses: [
281287
{
282288
StatusCode: 200,
@@ -399,11 +405,13 @@ describe('#compileMethodsToSqs()', () => {
399405
}
400406
]
401407
},
408+
PassthroughBehavior: 'NEVER',
402409
RequestParameters: {
403-
'integration.request.querystring.Action': "'SendMessage'",
404-
'integration.request.querystring.MessageBody': 'method.request.body'
410+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
411+
},
412+
RequestTemplates: {
413+
'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
405414
},
406-
RequestTemplates: { 'application/json': '{statusCode:200}' },
407415
IntegrationResponses: [
408416
{
409417
StatusCode: 200,
@@ -486,11 +494,13 @@ describe('#compileMethodsToSqs()', () => {
486494
}
487495
]
488496
},
497+
PassthroughBehavior: 'NEVER',
489498
RequestParameters: {
490-
'integration.request.querystring.Action': "'SendMessage'",
491-
'integration.request.querystring.MessageBody': 'method.request.body'
499+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
500+
},
501+
RequestTemplates: {
502+
'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
492503
},
493-
RequestTemplates: { 'application/json': '{statusCode:200}' },
494504
IntegrationResponses: [
495505
{
496506
StatusCode: 200,
@@ -573,13 +583,106 @@ describe('#compileMethodsToSqs()', () => {
573583
}
574584
]
575585
},
586+
PassthroughBehavior: 'NEVER',
576587
RequestParameters: {
577-
'integration.request.querystring.Action': "'SendMessage'",
578-
'integration.request.querystring.MessageBody': 'method.request.body',
588+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'",
579589
key1: 'value1',
580590
key2: 'value2'
581591
},
582-
RequestTemplates: { 'application/json': '{statusCode:200}' },
592+
RequestTemplates: {
593+
'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)'
594+
},
595+
IntegrationResponses: [
596+
{
597+
StatusCode: 200,
598+
SelectionPattern: 200,
599+
ResponseParameters: {},
600+
ResponseTemplates: {}
601+
},
602+
{
603+
StatusCode: 400,
604+
SelectionPattern: 400,
605+
ResponseParameters: {},
606+
ResponseTemplates: {}
607+
},
608+
{
609+
StatusCode: 500,
610+
SelectionPattern: 500,
611+
ResponseParameters: {},
612+
ResponseTemplates: {}
613+
}
614+
]
615+
},
616+
MethodResponses: [
617+
{ ResponseParameters: {}, ResponseModels: {}, StatusCode: 200 },
618+
{ ResponseParameters: {}, ResponseModels: {}, StatusCode: 400 },
619+
{ ResponseParameters: {}, ResponseModels: {}, StatusCode: 500 }
620+
]
621+
}
622+
}
623+
})
624+
})
625+
626+
it('should return a custom response template for application/json when one is given', () => {
627+
serverlessApigatewayServiceProxy.validated = {
628+
events: [
629+
{
630+
serviceName: 'sqs',
631+
http: {
632+
queueName: 'myQueue',
633+
path: 'sqs',
634+
method: 'post',
635+
auth: {
636+
authorizationType: 'NONE'
637+
},
638+
request: {
639+
template: {
640+
'application/json': '##This template is just a comment'
641+
}
642+
}
643+
}
644+
}
645+
]
646+
}
647+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
648+
serverlessApigatewayServiceProxy.apiGatewayResources = {
649+
sqs: {
650+
name: 'sqs',
651+
resourceLogicalId: 'ApiGatewayResourceSqs'
652+
}
653+
}
654+
655+
serverlessApigatewayServiceProxy.compileMethodsToSqs()
656+
657+
expect(serverless.service.provider.compiledCloudFormationTemplate.Resources).to.deep.equal({
658+
ApiGatewayMethodsqsPost: {
659+
Type: 'AWS::ApiGateway::Method',
660+
Properties: {
661+
HttpMethod: 'POST',
662+
RequestParameters: {},
663+
AuthorizationType: 'NONE',
664+
AuthorizationScopes: undefined,
665+
AuthorizerId: undefined,
666+
ApiKeyRequired: false,
667+
ResourceId: { Ref: 'ApiGatewayResourceSqs' },
668+
RestApiId: { Ref: 'ApiGatewayRestApi' },
669+
Integration: {
670+
IntegrationHttpMethod: 'POST',
671+
Type: 'AWS',
672+
Credentials: { 'Fn::GetAtt': ['ApigatewayToSqsRole', 'Arn'] },
673+
Uri: {
674+
'Fn::Sub': [
675+
'arn:aws:apigateway:${AWS::Region}:sqs:path//${AWS::AccountId}/${queueName}',
676+
{
677+
queueName: 'myQueue'
678+
}
679+
]
680+
},
681+
PassthroughBehavior: 'NEVER',
682+
RequestParameters: {
683+
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'"
684+
},
685+
RequestTemplates: { 'application/json': '##This template is just a comment' },
583686
IntegrationResponses: [
584687
{
585688
StatusCode: 200,

0 commit comments

Comments
 (0)