From 87fd3c17fb7d975b37c952293480bdc5ea4a8226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Tue, 1 Sep 2020 12:53:26 +0200 Subject: [PATCH 1/4] feat(Config Schema): Configure schema for AWS `sns` event (#8112) --- .../aws/package/compile/events/sns/index.js | 192 +++++++----------- .../package/compile/events/sns/index.test.js | 45 ---- lib/plugins/aws/provider/awsProvider.js | 86 +++++++- 3 files changed, 151 insertions(+), 172 deletions(-) diff --git a/lib/plugins/aws/package/compile/events/sns/index.js b/lib/plugins/aws/package/compile/events/sns/index.js index 24994ec5e8cc..ac2f0318800f 100644 --- a/lib/plugins/aws/package/compile/events/sns/index.js +++ b/lib/plugins/aws/package/compile/events/sns/index.js @@ -11,38 +11,47 @@ class AwsCompileSNSEvents { 'package:compileEvents': this.compileSNSEvents.bind(this), }; - // TODO: Complete schema, see https://github.com/serverless/serverless/issues/8035 this.serverless.configSchemaHandler.defineFunctionEvent('aws', 'sns', { - anyOf: [{ type: 'string' }, { type: 'object' }], + oneOf: [ + { type: 'string', maxLength: 256, pattern: '^[\\w-]+$' }, + { $ref: '#/definitions/awsArnString' }, + { + type: 'object', + properties: { + arn: { $ref: '#/definitions/awsArn' }, + topicName: { type: 'string', maxLength: 256, pattern: '^[\\w-]+$' }, + displayName: { type: 'string', minLength: 1 }, + filterPolicy: { type: 'object' }, + redrivePolicy: { + type: 'object', + properties: { + deadLetterTargetArn: { $ref: '#/definitions/awsArnString' }, + deadLetterTargetRef: { type: 'string', minLength: 1 }, + deadLetterTargetImport: { + type: 'object', + properties: { + arn: { $ref: '#/definitions/awsArnString' }, + url: { type: 'string', minLength: 1 }, + }, + required: ['arn', 'url'], + additionalProperties: false, + }, + }, + oneOf: [ + { required: ['deadLetterTargetArn'] }, + { required: ['deadLetterTargetRef'] }, + { required: ['deadLetterTargetImport'] }, + ], + additionalProperties: false, + }, + }, + oneOf: [{ required: ['arn'] }, { required: ['topicName', 'displayName'] }], + additionalProperties: false, + }, + ], }); } - invalidPropertyErrorMessage(functionName, property) { - return [ - `Missing or invalid ${property} property for sns event`, - ` in function "${functionName}"`, - ' The correct syntax is: sns: topic-name-or-arn', - ' OR an object with ', - ' arn and topicName OR', - ' topicName and displayName.', - ' Please check the docs for more info.', - ].join(''); - } - - isValidStackImport(variable) { - if (Object.keys(variable).length !== 1) { - return false; - } - if ( - variable['Fn::ImportValue'] && - (variable['Fn::ImportValue']['Fn::GetAtt'] || variable['Fn::ImportValue'].Ref) - ) { - return false; - } - const intrinsicFunctions = ['Fn::ImportValue', 'Ref', 'Fn::GetAtt', 'Fn::Sub', 'Fn::Join']; - return intrinsicFunctions.some(func => variable[func]); - } - compileSNSEvents() { const template = this.serverless.service.provider.compiledCloudFormationTemplate; @@ -60,72 +69,37 @@ class AwsCompileSNSEvents { if (typeof event.sns === 'object') { if (event.sns.arn) { topicArn = event.sns.arn; - if (typeof topicArn === 'object') { - if (!this.isValidStackImport(topicArn)) { - throw new this.serverless.classes.Error( - this.invalidPropertyErrorMessage(functionName, 'arn') - ); - } - } else if (typeof topicArn === 'string') { - if (topicArn.indexOf('arn:') === 0) { - // NOTE: we need to process the topic ARN that way due to lacking - // support of regex lookbehind in Node.js 6. - // Once Node.js 6 support is dropped we can change this to: - // `const splitArn = topicArn.split(/(? s.replace(/@@/g, '::')); - topicName = splitArn[splitArn.length - 1]; - if (splitArn[3] !== this.options.region) { - region = splitArn[3]; - } - } else { - throw new this.serverless.classes.Error( - this.invalidPropertyErrorMessage(functionName, 'arn') - ); + if (typeof topicArn === 'string') { + // NOTE: we need to process the topic ARN that way due to lacking + // support of regex lookbehind in Node.js 6. + // Once Node.js 6 support is dropped we can change this to: + // `const splitArn = topicArn.split(/(? s.replace(/@@/g, '::')); + topicName = splitArn[splitArn.length - 1]; + if (splitArn[3] !== this.options.region) { + region = splitArn[3]; } - } else { - throw new this.serverless.classes.Error( - this.invalidPropertyErrorMessage(functionName, 'arn') - ); } topicName = event.sns.topicName || topicName; - if (!topicName || typeof topicName !== 'string') { + // Only true when providing CFN intrinsic function (not string) to arn property without topicName property + if (!topicName) { throw new this.serverless.classes.Error( - this.invalidPropertyErrorMessage(functionName, 'topicName') + 'Missing "sns.topicName" setting (it needs to be provided if "sns.arn" is not provided as plain ARN string)' ); } } else { - ['topicName', 'displayName'].forEach(property => { - if (typeof event.sns[property] === 'string') { - return; - } - throw new this.serverless.classes.Error( - this.invalidPropertyErrorMessage(functionName, property) - ); - }); displayName = event.sns.displayName; topicName = event.sns.topicName; } - } else if (typeof event.sns === 'string') { - if (event.sns.indexOf('arn:') === 0) { - topicArn = event.sns; - const splitArn = topicArn.split(':'); - topicName = splitArn[splitArn.length - 1]; - } else { - topicName = event.sns; - } + } else if (event.sns.indexOf('arn:') === 0) { + topicArn = event.sns; + const splitArn = topicArn.split(':'); + topicName = splitArn[splitArn.length - 1]; } else { - const errorMessage = [ - `SNS event of function ${functionName} is not an object nor a string`, - ' The correct syntax is: sns: topic-name-or-arn', - ' OR an object with ', - ' arn and topicName OR', - ' topicName and displayName.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); + topicName = event.sns; } if (event.sns.redrivePolicy) { @@ -136,41 +110,20 @@ class AwsCompileSNSEvents { } = event.sns.redrivePolicy; let targetArn; let targetUrl; - if (!deadLetterTargetArn && !deadLetterTargetRef && !deadLetterTargetImport) { - throw new this.serverless.classes.Error( - 'redrivePolicy must be specified with deadLetterTargetArn, deadLetterTargetRef or deadLetterTargetImport' - ); - } if (deadLetterTargetArn) { - if ( - typeof deadLetterTargetArn !== 'string' || - !deadLetterTargetArn.startsWith('arn:') - ) { - throw new this.serverless.classes.Error( - 'Invalid deadLetterTargetArn specified, it must be an string starting with arn:' - ); - } else { - targetArn = deadLetterTargetArn; - // arn:aws:sqs:us-east-1:11111111111:myDLQ - const [deQueueName, deAccount, deRegion] = deadLetterTargetArn - .split(':') - .reverse(); - targetUrl = { - 'Fn::Join': [ - '', - `https://sqs.${deRegion}.`, - { Ref: 'AWS::URLSuffix' }, - `/${deAccount}/${deQueueName}`, - ], - }; - } + targetArn = deadLetterTargetArn; + // arn:aws:sqs:us-east-1:11111111111:myDLQ + const [deQueueName, deAccount, deRegion] = deadLetterTargetArn.split(':').reverse(); + targetUrl = { + 'Fn::Join': [ + '', + `https://sqs.${deRegion}.`, + { Ref: 'AWS::URLSuffix' }, + `/${deAccount}/${deQueueName}`, + ], + }; } else if (deadLetterTargetRef) { - if (typeof deadLetterTargetRef !== 'string') { - throw new this.serverless.classes.Error( - 'Invalid deadLetterTargetRef specified, it must be a string' - ); - } targetArn = { 'Fn::GetAtt': [deadLetterTargetRef, 'Arn'], }; @@ -178,15 +131,6 @@ class AwsCompileSNSEvents { Ref: deadLetterTargetRef, }; } else { - if ( - typeof deadLetterTargetImport !== 'object' || - !deadLetterTargetImport.arn || - !deadLetterTargetImport.url - ) { - throw new this.serverless.classes.Error( - 'Invalid deadLetterTargetImport specified, it must be an object containing arn and url fields' - ); - } targetArn = { 'Fn::ImportValue': deadLetterTargetImport.arn, }; diff --git a/lib/plugins/aws/package/compile/events/sns/index.test.js b/lib/plugins/aws/package/compile/events/sns/index.test.js index e17a6045c553..4e74f0aa514d 100644 --- a/lib/plugins/aws/package/compile/events/sns/index.test.js +++ b/lib/plugins/aws/package/compile/events/sns/index.test.js @@ -514,51 +514,6 @@ describe('AwsCompileSNSEvents', () => { }).to.throw(Error); }); - // eslint-disable-next-line max-len - it('should throw an error when invalid imported arn object is given as object properties', () => { - awsCompileSNSEvents.serverless.service.functions = { - first: { - events: [ - { - sns: { - topicName: 'bar', - arn: { - 'Fn::ImportValue': { - Ref: 'BarTopic', - }, - }, - }, - }, - ], - }, - }; - - expect(() => { - awsCompileSNSEvents.compileSNSEvents(); - }).to.throw(Error); - - awsCompileSNSEvents.serverless.service.functions = { - first: { - events: [ - { - sns: { - topicName: 'bar', - arn: { - 'Fn::ImportValue': { - 'Fn::GetAtt': ['BarTopic', 'Arn'], - }, - }, - }, - }, - ], - }, - }; - - expect(() => { - awsCompileSNSEvents.compileSNSEvents(); - }).to.throw(Error); - }); - it('should create SNS topic when arn, topicName, and filterPolicy are given as object', () => { awsCompileSNSEvents.serverless.service.functions = { first: { diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index 37ea43c1a911..2181e799b1d8 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -141,11 +141,27 @@ class AwsProvider { // TODO: Complete schema, see https://github.com/serverless/serverless/issues/8016 serverless.configSchemaHandler.defineProvider('aws', { definitions: { - awsArn: { + awsArnString: { type: 'string', pattern: '^arn:', }, - awsCfImport: { + awsArn: { + oneOf: [ + { $ref: '#/definitions/awsArnString' }, + { $ref: '#/definitions/awsCfFunction' }, + ], + }, + awsCfFunction: { + oneOf: [ + { $ref: '#/definitions/awsCfImport' }, + { $ref: '#/definitions/awsCfJoin' }, + { $ref: '#/definitions/awsCfGetAtt' }, + { $ref: '#/definitions/awsCfRef' }, + { $ref: '#/definitions/awsCfSub' }, + ], + }, + // currently used by lib/plugins/aws/utils/resolveCfImportValue.js for non nested import expressions + awsCfImportLocallyResolvable: { type: 'object', properties: { 'Fn::ImportValue': { type: 'string' }, @@ -153,6 +169,65 @@ class AwsProvider { additionalProperties: false, required: ['Fn::ImportValue'], }, + awsCfImport: { + type: 'object', + properties: { + 'Fn::ImportValue': {}, + }, + additionalProperties: false, + required: ['Fn::ImportValue'], + }, + awsCfJoin: { + type: 'object', + properties: { + 'Fn::Join': { + type: 'array', + minItems: 2, + maxItems: 2, + items: [{ type: 'string', minLength: 1 }, { type: 'array' }], + additionalItems: false, + }, + }, + required: ['Fn::Join'], + additionalProperties: false, + }, + awsCfGetAtt: { + type: 'object', + properties: { + 'Fn::GetAtt': { + type: 'array', + minItems: 2, + maxItems: 2, + items: { type: 'string', minLength: 1 }, + }, + }, + required: ['Fn::GetAtt'], + additionalProperties: false, + }, + awsCfRef: { + type: 'object', + properties: { + Ref: { type: 'string', minLength: 1 }, + }, + required: ['Ref'], + additionalProperties: false, + }, + awsCfSub: { + oneOf: [ + { type: 'string', minLength: 1 }, + { + type: 'object', + properties: { + 'Fn::Sub': { + type: 'array', + minItems: 2, + }, + }, + required: ['Fn::Sub'], + additionalProperties: false, + }, + ], + }, }, provider: { properties: { @@ -200,7 +275,12 @@ class AwsProvider { }, ], }, - id: { oneOf: [{ type: 'string' }, { $ref: '#/definitions/awsCfImport' }] }, + id: { + oneOf: [ + { type: 'string' }, + { $ref: '#/definitions/awsCfImportLocallyResolvable' }, + ], + }, name: { type: 'string' }, payload: { type: 'string' }, timeout: { type: 'number', minimum: 0.05, maximum: 30 }, From 06ed01b8742260c01411d5e371ab56a6c02219f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Tue, 1 Sep 2020 14:53:08 +0200 Subject: [PATCH 2/4] feat(AWS Local Invocation): Resolve "Fn::ImportValue" instructions in env vars (PR #8157) --- lib/plugins/aws/info/getStackInfo.js | 24 +++--- lib/plugins/aws/info/getStackInfo.test.js | 18 ++++- lib/plugins/aws/invokeLocal/index.js | 81 ++++++++++++------- lib/plugins/aws/utils/resolveCfImportValue.js | 7 +- 4 files changed, 81 insertions(+), 49 deletions(-) diff --git a/lib/plugins/aws/info/getStackInfo.js b/lib/plugins/aws/info/getStackInfo.js index edee86479824..82341ca86be4 100644 --- a/lib/plugins/aws/info/getStackInfo.js +++ b/lib/plugins/aws/info/getStackInfo.js @@ -34,24 +34,22 @@ module.exports = { if (httpApiId) { sdkRequests.push( (httpApiId['Fn::ImportValue'] - ? resolveCfImportValue(this.provider, httpApiId['Fn::ImportValue']).then(id => { - if (!id) { - throw new this.serverless.classes.Error( - `Could not resolve provider.httpApi.id parameter. Configured value: ${JSON.stringify( - httpApiId - )}` - ); - } - return id; - }) + ? resolveCfImportValue(this.provider, httpApiId['Fn::ImportValue']) : BbPromise.resolve(httpApiId) ) .then(id => { return this.provider.request('ApiGatewayV2', 'getApi', { ApiId: id }); }) - .then(result => { - if (result) stackData.externalHttpApiEndpoint = result.ApiEndpoint; - }) + .then( + result => { + stackData.externalHttpApiEndpoint = result.ApiEndpoint; + }, + error => { + throw new this.serverless.classes.Error( + `Could not resolve provider.httpApi.id parameter. ${error.message}` + ); + } + ) ); } diff --git a/lib/plugins/aws/info/getStackInfo.test.js b/lib/plugins/aws/info/getStackInfo.test.js index f44d2431a91d..2cb545737858 100644 --- a/lib/plugins/aws/info/getStackInfo.test.js +++ b/lib/plugins/aws/info/getStackInfo.test.js @@ -195,15 +195,25 @@ describe('#getStackInfo()', () => { id: 'http-api-id', }; - const describeStacksResponse = null; - - describeStacksStub.resolves(describeStacksResponse); + describeStacksStub + .withArgs('CloudFormation', 'describeStacks', { + StackName: awsInfo.provider.naming.getStackName(), + }) + .resolves(null); + + describeStacksStub + .withArgs('ApiGatewayV2', 'getApi', { + ApiId: 'http-api-id', + }) + .resolves({ + ApiEndpoint: 'my-endpoint', + }); const expectedGatheredDataObj = { info: { functions: [], layers: [], - endpoints: [], + endpoints: ['httpApi: my-endpoint'], service: 'my-service', stage: 'dev', region: 'us-east-1', diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index 6c501e5bf4ff..61a729775c29 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -19,6 +19,7 @@ const dirExists = require('../../../utils/fs/dirExists'); const fileExists = require('../../../utils/fs/fileExists'); const isStandalone = require('../../../utils/isStandaloneExecutable'); const getEnsureArtifact = require('../../../utils/getEnsureArtifact'); +const resolveCfImportValue = require('../utils/resolveCfImportValue'); const mkdirpAsync = BbPromise.promisify(mkdirp); @@ -150,41 +151,59 @@ class AwsInvokeLocal { } loadEnvVars() { - const lambdaName = this.options.functionObj.name; - const memorySize = - Number(this.options.functionObj.memorySize) || - Number(this.serverless.service.provider.memorySize) || - 1024; - - const lambdaDefaultEnvVars = { - LANG: 'en_US.UTF-8', - LD_LIBRARY_PATH: - '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len - LAMBDA_TASK_ROOT: '/var/task', - LAMBDA_RUNTIME_DIR: '/var/runtime', - AWS_REGION: this.provider.getRegion(), - AWS_DEFAULT_REGION: this.provider.getRegion(), - AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName), - AWS_LAMBDA_LOG_STREAM_NAME: '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad', - AWS_LAMBDA_FUNCTION_NAME: lambdaName, - AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize, - AWS_LAMBDA_FUNCTION_VERSION: '$LATEST', - NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules', - }; - - const credentialEnvVars = this.getCredentialEnvVars(); + return BbPromise.try(() => { + const lambdaName = this.options.functionObj.name; + const memorySize = + Number(this.options.functionObj.memorySize) || + Number(this.serverless.service.provider.memorySize) || + 1024; + + const lambdaDefaultEnvVars = { + LANG: 'en_US.UTF-8', + LD_LIBRARY_PATH: + '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len + LAMBDA_TASK_ROOT: '/var/task', + LAMBDA_RUNTIME_DIR: '/var/runtime', + AWS_REGION: this.provider.getRegion(), + AWS_DEFAULT_REGION: this.provider.getRegion(), + AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName), + AWS_LAMBDA_LOG_STREAM_NAME: '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad', + AWS_LAMBDA_FUNCTION_NAME: lambdaName, + AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize, + AWS_LAMBDA_FUNCTION_VERSION: '$LATEST', + NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules', + }; - // profile override from config - const profileOverride = this.provider.getProfile(); - if (profileOverride) { - lambdaDefaultEnvVars.AWS_PROFILE = profileOverride; - } + const credentialEnvVars = this.getCredentialEnvVars(); - const configuredEnvVars = this.getConfiguredEnvVars(); + // profile override from config + const profileOverride = this.provider.getProfile(); + if (profileOverride) { + lambdaDefaultEnvVars.AWS_PROFILE = profileOverride; + } - _.merge(process.env, lambdaDefaultEnvVars, credentialEnvVars, configuredEnvVars); + const configuredEnvVars = this.getConfiguredEnvVars(); + + const promises = _.entries(configuredEnvVars).map(([name, value]) => { + return (value['Fn::ImportValue'] + ? resolveCfImportValue(this.provider, value['Fn::ImportValue']) + : BbPromise.resolve(value) + ).then( + resolvedValue => { + configuredEnvVars[name] = resolvedValue; + }, + error => { + throw new this.serverless.classes.Error( + `Could not resolve ${name} environment variable. ${error.message}` + ); + } + ); + }); - return BbPromise.resolve(); + return BbPromise.all(promises).then(() => { + _.merge(process.env, lambdaDefaultEnvVars, credentialEnvVars, configuredEnvVars); + }); + }); } invokeLocal() { diff --git a/lib/plugins/aws/utils/resolveCfImportValue.js b/lib/plugins/aws/utils/resolveCfImportValue.js index c6250b755f6f..53facaa2337f 100644 --- a/lib/plugins/aws/utils/resolveCfImportValue.js +++ b/lib/plugins/aws/utils/resolveCfImportValue.js @@ -1,5 +1,7 @@ 'use strict'; +const ServerlessError = require('../../../classes/Error').ServerlessError; + function resolveCfImportValue(provider, name, sdkParams = {}) { return provider.request('CloudFormation', 'listExports', sdkParams).then(result => { const targetExportMeta = result.Exports.find(exportMeta => exportMeta.Name === name); @@ -7,7 +9,10 @@ function resolveCfImportValue(provider, name, sdkParams = {}) { if (result.NextToken) { return resolveCfImportValue(provider, name, { NextToken: result.NextToken }); } - return null; + + throw new ServerlessError( + `Could not resolve Fn::ImportValue with name ${name}. Are you sure this value is exported ?` + ); }); } From a2d1031fb88ac750685b5940b60c0b241c90e319 Mon Sep 17 00:00:00 2001 From: Christian Musa <1450075+crash7@users.noreply.github.com> Date: Tue, 1 Sep 2020 10:10:17 -0300 Subject: [PATCH 3/4] feat(CLI): Deprecate "slss" alias (#8156) --- bin/slss.js | 12 ++++++++++++ docs/deprecations.md | 6 ++++++ package.json | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 bin/slss.js diff --git a/bin/slss.js b/bin/slss.js new file mode 100755 index 000000000000..a8dd90ae50c5 --- /dev/null +++ b/bin/slss.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +// TODO (BREAKING): Remove this file with next major release + +'use strict'; + +require('../lib/utils/logDeprecation')( + 'SLSS_CLI_ALIAS', + 'Support for "slss" command will be removed with v2.0.0. Use "sls" or "serverless" instead' +); + +require('./serverless.js'); diff --git a/docs/deprecations.md b/docs/deprecations.md index 6e5c0ef9848e..cb043f4ee36c 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -6,6 +6,12 @@ layout: Doc # Serverless Framework Deprecations +
 
+ +## `slss` alias + +Support for `slss` command will be removed with v2.0.0. Use `sls` or `serverless` instead. +
 
## AWS Lambda Function Destinations `maximumEventAge` & `maximumRetryAttempts` diff --git a/package.json b/package.json index 0751360066d1..fd5c2701a7be 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "main": "lib/Serverless.js", "bin": { "serverless": "./bin/serverless.js", - "slss": "./bin/serverless.js", + "slss": "./bin/slss.js", "sls": "./bin/serverless.js" }, "dependencies": { From 7aad8193787f591cd3186b2f86e0f9bec23f4dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ormaechea?= Date: Tue, 1 Sep 2020 11:15:33 -0300 Subject: [PATCH 4/4] feat(AWS API Gateway): Allow to opt-out from default request templates (PR #8159) --- docs/providers/aws/events/apigateway.md | 16 ++++++++++ .../apiGateway/lib/method/index.test.js | 31 +++++++++++++++++++ .../apiGateway/lib/method/integration.js | 8 ++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index 77c99fa11b93..6680fc31ccae 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -994,6 +994,22 @@ functions: You can then access the query string `https://example.com/dev/whatever?bar=123` by `event.foo` in the lambda function. If you want to spread a string into multiple lines, you can use the `>` or `|` syntax, but the following strings have to be all indented with the same amount, [read more about `>` syntax](http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines). +In order to remove one of the default request templates you just need to pass it as null, as follows: + +```yml +functions: + create: + handler: posts.create + events: + - http: + method: get + path: whatever + integration: lambda + request: + template: + application/x-www-form-urlencoded: null +``` + #### Pass Through Behavior [API Gateway](https://serverless.com/amazon-api-gateway/) provides multiple ways to handle requests where the Content-Type header does not match any of the specified mapping templates. When this happens, the request payload will either be passed through the integration request _without transformation_ or rejected with a `415 - Unsupported Media Type`, depending on the configuration. diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js index acfde4b271ab..c1ab20a1c2e2 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js @@ -1304,6 +1304,37 @@ describe('#compileMethods()', () => { }); }); + it('should delete the default "application/x-www-form-urlencoded" template if it\'s overriden with null', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + integration: 'AWS', + request: { + template: { + 'application/x-www-form-urlencoded': null, + }, + }, + response: { + statusCodes: { + 200: { + pattern: '', + }, + }, + }, + }, + }, + ]; + return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.RequestTemplates + ).to.not.have.key('application/x-www-form-urlencoded'); + }); + }); + it('should use defined pass-through behavior', () => { awsCompileApigEvents.validated.events = [ { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js index 3be224959108..140b18edd6bb 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js @@ -209,7 +209,13 @@ module.exports = { // set custom request templates if provided if (http.request && typeof http.request.template === 'object') { - Object.assign(integrationRequestTemplates, http.request.template); + _.entries(http.request.template).forEach(([contentType, template]) => { + if (template === null) { + delete integrationRequestTemplates[contentType]; + } else { + integrationRequestTemplates[contentType] = template; + } + }); } return Object.keys(integrationRequestTemplates).length