Skip to content

Commit 2a832aa

Browse files
authored
Merge pull request #107 from ajhaining/master
feat(sns): add support for custom response mappings
2 parents fbe4a4f + 1ee810b commit 2a832aa

File tree

4 files changed

+308
-24
lines changed

4 files changed

+308
-24
lines changed

README.md

+52
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This Serverless Framework plugin supports the AWS service proxy integration feat
1919
- [Customize the Path Override in API Gateway](#customize-the-path-override-in-api-gateway)
2020
- [Can use greedy, for deeper Folders](#can-use-greedy--for-deeper-folders)
2121
- [SNS](#sns)
22+
- [Customizing responses](#customizing-responses-1)
2223
- [DynamoDB](#dynamodb)
2324
- [EventBridge](#eventbridge)
2425
- [Common API Gateway features](#common-api-gateway-features)
@@ -372,6 +373,57 @@ Sample request after deploying.
372373
curl https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/sns -d '{"message": "testtest"}' -H 'Content-Type:application/json'
373374
```
374375

376+
#### Customizing responses
377+
378+
##### Simplified response template customization
379+
380+
You can get a simple customization of the responses by providing a template for the possible responses. The template is assumed to be `application/json`.
381+
382+
```yml
383+
custom:
384+
apiGatewayServiceProxies:
385+
- sns:
386+
path: /sns
387+
method: post
388+
topicName: { 'Fn::GetAtt': ['SNSTopic', 'TopicName'] }
389+
cors: true
390+
response:
391+
template:
392+
# `success` is used when the integration response is 200
393+
success: |-
394+
{ "message: "accepted" }
395+
# `clientError` is used when the integration response is 400
396+
clientError: |-
397+
{ "message": "there is an error in your request" }
398+
# `serverError` is used when the integration response is 500
399+
serverError: |-
400+
{ "message": "there was an error handling your request" }
401+
```
402+
403+
##### Full response customization
404+
405+
If you want more control over the integration response, you can
406+
provide an array of objects for the `response` value:
407+
408+
```yml
409+
custom:
410+
apiGatewayServiceProxies:
411+
- sns:
412+
path: /sns
413+
method: post
414+
topicName: { 'Fn::GetAtt': ['SNSTopic', 'TopicName'] }
415+
cors: true
416+
response:
417+
- statusCode: 200
418+
selectionPattern: '2\d{2}'
419+
responseParameters: {}
420+
responseTemplates:
421+
application/json: |-
422+
{ "message": "accepted" }
423+
```
424+
425+
The object keys correspond to the API Gateway [integration response](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-method-integration-integrationresponse.html#cfn-apigateway-method-integration-integrationresponse-responseparameters) object.
426+
375427
### DynamoDB
376428

377429
Sample syntax for DynamoDB proxy in `serverless.yml`. Currently, the supported [DynamoDB Operations](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Operations.html) are `PutItem`, `GetItem` and `DeleteItem`.

lib/apiGateway/schema.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ const response = Joi.object({
218218
})
219219
})
220220

221-
const sqsResponse = Joi.alternatives().try([
221+
const extendedResponse = Joi.alternatives().try([
222222
Joi.object({
223223
template: Joi.object().keys({
224224
success: Joi.string(),
@@ -266,14 +266,18 @@ const proxiesSchemas = {
266266
})
267267
}),
268268
sns: Joi.object({
269-
sns: proxy.append({ topicName: stringOrGetAtt('topicName', 'TopicName').required(), request })
269+
sns: proxy.append({
270+
topicName: stringOrGetAtt('topicName', 'TopicName').required(),
271+
request,
272+
response: extendedResponse
273+
})
270274
}),
271275
sqs: Joi.object({
272276
sqs: proxy.append({
273277
queueName: stringOrGetAtt('queueName', 'QueueName').required(),
274278
requestParameters,
275279
request,
276-
response: sqsResponse
280+
response: extendedResponse
277281
})
278282
}),
279283
dynamodb: Joi.object({

lib/package/sns/compileMethodsToSns.js

+64-21
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,65 @@ module.exports = {
6262
RequestTemplates: this.getSnsIntegrationRequestTemplates(http)
6363
}
6464

65-
const integrationResponse = {
66-
IntegrationResponses: [
67-
{
68-
StatusCode: 200,
69-
SelectionPattern: 200,
70-
ResponseParameters: {},
71-
ResponseTemplates: {}
72-
},
73-
{
74-
StatusCode: 400,
75-
SelectionPattern: 400,
76-
ResponseParameters: {},
77-
ResponseTemplates: {}
78-
},
79-
{
80-
StatusCode: 500,
81-
SelectionPattern: 500,
82-
ResponseParameters: {},
83-
ResponseTemplates: {}
84-
}
85-
]
65+
let integrationResponse
66+
67+
if (_.get(http.response, 'template.success')) {
68+
// support a simplified model
69+
integrationResponse = {
70+
IntegrationResponses: [
71+
{
72+
StatusCode: 200,
73+
SelectionPattern: 200,
74+
ResponseParameters: {},
75+
ResponseTemplates: this.getSnsIntegrationResponseTemplate(http, 'success')
76+
},
77+
{
78+
StatusCode: 400,
79+
SelectionPattern: 400,
80+
ResponseParameters: {},
81+
ResponseTemplates: this.getSnsIntegrationResponseTemplate(http, 'clientError')
82+
},
83+
{
84+
StatusCode: 500,
85+
SelectionPattern: 500,
86+
ResponseParameters: {},
87+
ResponseTemplates: this.getSnsIntegrationResponseTemplate(http, 'serverError')
88+
}
89+
]
90+
}
91+
} else if (_.isArray(http.response)) {
92+
// support full usage
93+
integrationResponse = {
94+
IntegrationResponses: http.response.map((i) => ({
95+
StatusCode: i.statusCode,
96+
SelectionPattern: i.selectionPattern || i.statusCode,
97+
ResponseParameters: i.responseParameters || {},
98+
ResponseTemplates: i.responseTemplates || {}
99+
}))
100+
}
101+
} else {
102+
integrationResponse = {
103+
IntegrationResponses: [
104+
{
105+
StatusCode: 200,
106+
SelectionPattern: 200,
107+
ResponseParameters: {},
108+
ResponseTemplates: {}
109+
},
110+
{
111+
StatusCode: 400,
112+
SelectionPattern: 400,
113+
ResponseParameters: {},
114+
ResponseTemplates: {}
115+
},
116+
{
117+
StatusCode: 500,
118+
SelectionPattern: 500,
119+
ResponseParameters: {},
120+
ResponseTemplates: {}
121+
}
122+
]
123+
}
86124
}
87125

88126
this.addCors(http, integrationResponse)
@@ -128,5 +166,10 @@ module.exports = {
128166
]
129167
]
130168
}
169+
},
170+
171+
getSnsIntegrationResponseTemplate(http, statusType) {
172+
const template = _.get(http, ['response', 'template', statusType])
173+
return Object.assign({}, template && { 'application/json': template })
131174
}
132175
}

lib/package/sns/compileMethodsToSns.test.js

+185
Original file line numberDiff line numberDiff line change
@@ -815,4 +815,189 @@ describe('#compileMethodsToSns()', () => {
815815
.Properties.RequestParameters
816816
).to.be.deep.equal({ 'method.request.header.Custom-Header': true })
817817
})
818+
819+
it('should throw error if simplified response template uses an unsupported key', () => {
820+
serverlessApigatewayServiceProxy.serverless.service.custom = {
821+
apiGatewayServiceProxies: [
822+
{
823+
sns: {
824+
path: '/sns',
825+
method: 'post',
826+
topicName: 'topicName',
827+
response: {
828+
template: {
829+
test: 'test template'
830+
}
831+
}
832+
}
833+
}
834+
]
835+
}
836+
837+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
838+
serverless.classes.Error,
839+
'child "sns" fails because [child "response" fails because [child "template" fails because ["test" is not allowed], "response" must be an array]]'
840+
)
841+
})
842+
843+
it('should throw error if complex response template uses an unsupported key', () => {
844+
serverlessApigatewayServiceProxy.serverless.service.custom = {
845+
apiGatewayServiceProxies: [
846+
{
847+
sns: {
848+
path: '/sns',
849+
method: 'post',
850+
topicName: 'topicName',
851+
response: [
852+
{
853+
test: 'test'
854+
}
855+
]
856+
}
857+
}
858+
]
859+
}
860+
861+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
862+
serverless.classes.Error,
863+
'child "sns" fails because [child "response" fails because ["response" must be an object, "response" at position 0 fails because ["test" is not allowed]]]'
864+
)
865+
})
866+
867+
it('should transform simplified integration responses', () => {
868+
serverlessApigatewayServiceProxy.validated = {
869+
events: [
870+
{
871+
serviceName: 'sns',
872+
http: {
873+
topicName: 'topicName',
874+
path: 'sns',
875+
method: 'post',
876+
auth: {
877+
authorizationType: 'NONE'
878+
},
879+
response: {
880+
template: {
881+
success: 'success template',
882+
clientError: 'client error template',
883+
serverError: 'server error template'
884+
}
885+
}
886+
}
887+
}
888+
]
889+
}
890+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
891+
serverlessApigatewayServiceProxy.apiGatewayResources = {
892+
sns: {
893+
name: 'sns',
894+
resourceLogicalId: 'ApiGatewayResourceSns'
895+
}
896+
}
897+
898+
serverlessApigatewayServiceProxy.compileMethodsToSns()
899+
900+
expect(
901+
serverless.service.provider.compiledCloudFormationTemplate.Resources.ApiGatewayMethodsnsPost
902+
.Properties.Integration.IntegrationResponses
903+
).to.be.deep.equal([
904+
{
905+
StatusCode: 200,
906+
SelectionPattern: 200,
907+
ResponseParameters: {},
908+
ResponseTemplates: {
909+
'application/json': 'success template'
910+
}
911+
},
912+
{
913+
StatusCode: 400,
914+
SelectionPattern: 400,
915+
ResponseParameters: {},
916+
ResponseTemplates: {
917+
'application/json': 'client error template'
918+
}
919+
},
920+
{
921+
StatusCode: 500,
922+
SelectionPattern: 500,
923+
ResponseParameters: {},
924+
ResponseTemplates: {
925+
'application/json': 'server error template'
926+
}
927+
}
928+
])
929+
})
930+
931+
it('should transform complex integration responses', () => {
932+
serverlessApigatewayServiceProxy.validated = {
933+
events: [
934+
{
935+
serviceName: 'sns',
936+
http: {
937+
topicName: 'topicName',
938+
path: 'sns',
939+
method: 'post',
940+
auth: {
941+
authorizationType: 'NONE'
942+
},
943+
response: [
944+
{
945+
statusCode: 200,
946+
responseTemplates: {
947+
'text/plain': 'ok'
948+
}
949+
},
950+
{
951+
statusCode: 400,
952+
selectionPattern: '4\\d{2}',
953+
responseParameters: {
954+
a: 'b'
955+
}
956+
},
957+
{
958+
statusCode: 500
959+
}
960+
]
961+
}
962+
}
963+
]
964+
}
965+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
966+
serverlessApigatewayServiceProxy.apiGatewayResources = {
967+
sns: {
968+
name: 'sns',
969+
resourceLogicalId: 'ApiGatewayResourceSns'
970+
}
971+
}
972+
973+
serverlessApigatewayServiceProxy.compileMethodsToSns()
974+
975+
expect(
976+
serverless.service.provider.compiledCloudFormationTemplate.Resources.ApiGatewayMethodsnsPost
977+
.Properties.Integration.IntegrationResponses
978+
).to.be.deep.equal([
979+
{
980+
StatusCode: 200,
981+
SelectionPattern: 200,
982+
ResponseParameters: {},
983+
ResponseTemplates: {
984+
'text/plain': 'ok'
985+
}
986+
},
987+
{
988+
StatusCode: 400,
989+
SelectionPattern: '4\\d{2}',
990+
ResponseParameters: {
991+
a: 'b'
992+
},
993+
ResponseTemplates: {}
994+
},
995+
{
996+
StatusCode: 500,
997+
SelectionPattern: 500,
998+
ResponseParameters: {},
999+
ResponseTemplates: {}
1000+
}
1001+
])
1002+
})
8181003
})

0 commit comments

Comments
 (0)