Skip to content

Commit

Permalink
Merge pull request #107 from ajhaining/master
Browse files Browse the repository at this point in the history
feat(sns): add support for custom response mappings
  • Loading branch information
horike37 authored Oct 26, 2020
2 parents fbe4a4f + 1ee810b commit 2a832aa
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 24 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This Serverless Framework plugin supports the AWS service proxy integration feat
- [Customize the Path Override in API Gateway](#customize-the-path-override-in-api-gateway)
- [Can use greedy, for deeper Folders](#can-use-greedy--for-deeper-folders)
- [SNS](#sns)
- [Customizing responses](#customizing-responses-1)
- [DynamoDB](#dynamodb)
- [EventBridge](#eventbridge)
- [Common API Gateway features](#common-api-gateway-features)
Expand Down Expand Up @@ -372,6 +373,57 @@ Sample request after deploying.
curl https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/sns -d '{"message": "testtest"}' -H 'Content-Type:application/json'
```

#### Customizing responses

##### Simplified response template customization

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`.

```yml
custom:
apiGatewayServiceProxies:
- sns:
path: /sns
method: post
topicName: { 'Fn::GetAtt': ['SNSTopic', 'TopicName'] }
cors: true
response:
template:
# `success` is used when the integration response is 200
success: |-
{ "message: "accepted" }
# `clientError` is used when the integration response is 400
clientError: |-
{ "message": "there is an error in your request" }
# `serverError` is used when the integration response is 500
serverError: |-
{ "message": "there was an error handling your request" }
```
##### Full response customization
If you want more control over the integration response, you can
provide an array of objects for the `response` value:

```yml
custom:
apiGatewayServiceProxies:
- sns:
path: /sns
method: post
topicName: { 'Fn::GetAtt': ['SNSTopic', 'TopicName'] }
cors: true
response:
- statusCode: 200
selectionPattern: '2\d{2}'
responseParameters: {}
responseTemplates:
application/json: |-
{ "message": "accepted" }
```

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.

### DynamoDB

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`.
Expand Down
10 changes: 7 additions & 3 deletions lib/apiGateway/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ const response = Joi.object({
})
})

const sqsResponse = Joi.alternatives().try([
const extendedResponse = Joi.alternatives().try([
Joi.object({
template: Joi.object().keys({
success: Joi.string(),
Expand Down Expand Up @@ -266,14 +266,18 @@ const proxiesSchemas = {
})
}),
sns: Joi.object({
sns: proxy.append({ topicName: stringOrGetAtt('topicName', 'TopicName').required(), request })
sns: proxy.append({
topicName: stringOrGetAtt('topicName', 'TopicName').required(),
request,
response: extendedResponse
})
}),
sqs: Joi.object({
sqs: proxy.append({
queueName: stringOrGetAtt('queueName', 'QueueName').required(),
requestParameters,
request,
response: sqsResponse
response: extendedResponse
})
}),
dynamodb: Joi.object({
Expand Down
85 changes: 64 additions & 21 deletions lib/package/sns/compileMethodsToSns.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,65 @@ module.exports = {
RequestTemplates: this.getSnsIntegrationRequestTemplates(http)
}

const integrationResponse = {
IntegrationResponses: [
{
StatusCode: 200,
SelectionPattern: 200,
ResponseParameters: {},
ResponseTemplates: {}
},
{
StatusCode: 400,
SelectionPattern: 400,
ResponseParameters: {},
ResponseTemplates: {}
},
{
StatusCode: 500,
SelectionPattern: 500,
ResponseParameters: {},
ResponseTemplates: {}
}
]
let integrationResponse

if (_.get(http.response, 'template.success')) {
// support a simplified model
integrationResponse = {
IntegrationResponses: [
{
StatusCode: 200,
SelectionPattern: 200,
ResponseParameters: {},
ResponseTemplates: this.getSnsIntegrationResponseTemplate(http, 'success')
},
{
StatusCode: 400,
SelectionPattern: 400,
ResponseParameters: {},
ResponseTemplates: this.getSnsIntegrationResponseTemplate(http, 'clientError')
},
{
StatusCode: 500,
SelectionPattern: 500,
ResponseParameters: {},
ResponseTemplates: this.getSnsIntegrationResponseTemplate(http, 'serverError')
}
]
}
} else if (_.isArray(http.response)) {
// support full usage
integrationResponse = {
IntegrationResponses: http.response.map((i) => ({
StatusCode: i.statusCode,
SelectionPattern: i.selectionPattern || i.statusCode,
ResponseParameters: i.responseParameters || {},
ResponseTemplates: i.responseTemplates || {}
}))
}
} else {
integrationResponse = {
IntegrationResponses: [
{
StatusCode: 200,
SelectionPattern: 200,
ResponseParameters: {},
ResponseTemplates: {}
},
{
StatusCode: 400,
SelectionPattern: 400,
ResponseParameters: {},
ResponseTemplates: {}
},
{
StatusCode: 500,
SelectionPattern: 500,
ResponseParameters: {},
ResponseTemplates: {}
}
]
}
}

this.addCors(http, integrationResponse)
Expand Down Expand Up @@ -128,5 +166,10 @@ module.exports = {
]
]
}
},

getSnsIntegrationResponseTemplate(http, statusType) {
const template = _.get(http, ['response', 'template', statusType])
return Object.assign({}, template && { 'application/json': template })
}
}
185 changes: 185 additions & 0 deletions lib/package/sns/compileMethodsToSns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -815,4 +815,189 @@ describe('#compileMethodsToSns()', () => {
.Properties.RequestParameters
).to.be.deep.equal({ 'method.request.header.Custom-Header': true })
})

it('should throw error if simplified response template uses an unsupported key', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
sns: {
path: '/sns',
method: 'post',
topicName: 'topicName',
response: {
template: {
test: 'test template'
}
}
}
}
]
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
serverless.classes.Error,
'child "sns" fails because [child "response" fails because [child "template" fails because ["test" is not allowed], "response" must be an array]]'
)
})

it('should throw error if complex response template uses an unsupported key', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
sns: {
path: '/sns',
method: 'post',
topicName: 'topicName',
response: [
{
test: 'test'
}
]
}
}
]
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
serverless.classes.Error,
'child "sns" fails because [child "response" fails because ["response" must be an object, "response" at position 0 fails because ["test" is not allowed]]]'
)
})

it('should transform simplified integration responses', () => {
serverlessApigatewayServiceProxy.validated = {
events: [
{
serviceName: 'sns',
http: {
topicName: 'topicName',
path: 'sns',
method: 'post',
auth: {
authorizationType: 'NONE'
},
response: {
template: {
success: 'success template',
clientError: 'client error template',
serverError: 'server error template'
}
}
}
}
]
}
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
serverlessApigatewayServiceProxy.apiGatewayResources = {
sns: {
name: 'sns',
resourceLogicalId: 'ApiGatewayResourceSns'
}
}

serverlessApigatewayServiceProxy.compileMethodsToSns()

expect(
serverless.service.provider.compiledCloudFormationTemplate.Resources.ApiGatewayMethodsnsPost
.Properties.Integration.IntegrationResponses
).to.be.deep.equal([
{
StatusCode: 200,
SelectionPattern: 200,
ResponseParameters: {},
ResponseTemplates: {
'application/json': 'success template'
}
},
{
StatusCode: 400,
SelectionPattern: 400,
ResponseParameters: {},
ResponseTemplates: {
'application/json': 'client error template'
}
},
{
StatusCode: 500,
SelectionPattern: 500,
ResponseParameters: {},
ResponseTemplates: {
'application/json': 'server error template'
}
}
])
})

it('should transform complex integration responses', () => {
serverlessApigatewayServiceProxy.validated = {
events: [
{
serviceName: 'sns',
http: {
topicName: 'topicName',
path: 'sns',
method: 'post',
auth: {
authorizationType: 'NONE'
},
response: [
{
statusCode: 200,
responseTemplates: {
'text/plain': 'ok'
}
},
{
statusCode: 400,
selectionPattern: '4\\d{2}',
responseParameters: {
a: 'b'
}
},
{
statusCode: 500
}
]
}
}
]
}
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
serverlessApigatewayServiceProxy.apiGatewayResources = {
sns: {
name: 'sns',
resourceLogicalId: 'ApiGatewayResourceSns'
}
}

serverlessApigatewayServiceProxy.compileMethodsToSns()

expect(
serverless.service.provider.compiledCloudFormationTemplate.Resources.ApiGatewayMethodsnsPost
.Properties.Integration.IntegrationResponses
).to.be.deep.equal([
{
StatusCode: 200,
SelectionPattern: 200,
ResponseParameters: {},
ResponseTemplates: {
'text/plain': 'ok'
}
},
{
StatusCode: 400,
SelectionPattern: '4\\d{2}',
ResponseParameters: {
a: 'b'
},
ResponseTemplates: {}
},
{
StatusCode: 500,
SelectionPattern: 500,
ResponseParameters: {},
ResponseTemplates: {}
}
])
})
})

0 comments on commit 2a832aa

Please sign in to comment.