From a377b52382389025711afbc044f723922675692f Mon Sep 17 00:00:00 2001 From: Andrew Haining Date: Mon, 5 Oct 2020 17:12:38 +0100 Subject: [PATCH 1/3] feat(sns): add response template support for SNS --- README.md | 52 ++++++ lib/apiGateway/schema.js | 10 +- lib/package/sns/compileMethodsToSns.js | 85 ++++++--- lib/package/sns/compileMethodsToSns.test.js | 185 ++++++++++++++++++++ 4 files changed, 308 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4d9dd6f..4e7d352 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,58 @@ 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`. diff --git a/lib/apiGateway/schema.js b/lib/apiGateway/schema.js index cd6778d..f676e75 100644 --- a/lib/apiGateway/schema.js +++ b/lib/apiGateway/schema.js @@ -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(), @@ -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({ diff --git a/lib/package/sns/compileMethodsToSns.js b/lib/package/sns/compileMethodsToSns.js index 27f2c6f..0a45bb5 100644 --- a/lib/package/sns/compileMethodsToSns.js +++ b/lib/package/sns/compileMethodsToSns.js @@ -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) @@ -128,5 +166,10 @@ module.exports = { ] ] } + }, + + getSnsIntegrationResponseTemplate(http, statusType) { + const template = _.get(http, ['response', 'template', statusType]) + return Object.assign({}, template && { 'application/json': template }) } } diff --git a/lib/package/sns/compileMethodsToSns.test.js b/lib/package/sns/compileMethodsToSns.test.js index bd0644f..1123719 100644 --- a/lib/package/sns/compileMethodsToSns.test.js +++ b/lib/package/sns/compileMethodsToSns.test.js @@ -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: {} + } + ]) + }) }) From f471f4dee8b3b6dfb92bd95a1f3565cbc391d811 Mon Sep 17 00:00:00 2001 From: Andrew Haining Date: Mon, 5 Oct 2020 17:15:45 +0100 Subject: [PATCH 2/3] chore: add link for SNS Customising responses --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e7d352..fb96557 100644 --- a/README.md +++ b/README.md @@ -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) @@ -372,7 +373,6 @@ 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 From 1ee810b8413c61a783279a8dbc64040ad55b268f Mon Sep 17 00:00:00 2001 From: Andrew Haining Date: Mon, 5 Oct 2020 17:35:17 +0100 Subject: [PATCH 3/3] chore: fix status code regex pattern --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb96557..07f0b06 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,7 @@ custom: cors: true response: - statusCode: 200 - selectionPattern: '2\\d{2}' + selectionPattern: '2\d{2}' responseParameters: {} responseTemplates: application/json: |-