Skip to content

Commit b5afda4

Browse files
Merge pull request #37 from erezrokah/feat/s3_request_parameters
Feature: allow passing requestParameters to s3 integration
2 parents 2bc837f + af855f2 commit b5afda4

File tree

5 files changed

+140
-8
lines changed

5 files changed

+140
-8
lines changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,27 @@ Sample request after deploying.
176176
curl https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/s3 -d '{"message": "testtest"}' -H 'Content-Type:application/json'
177177
```
178178

179+
#### Customizing request parameters
180+
181+
Similar to the [SQS](#sqs) support, you can customize the default request parameters `serverless.yml` like so:
182+
183+
```yml
184+
custom:
185+
apiGatewayServiceProxies:
186+
- s3:
187+
path: /s3
188+
method: post
189+
action: PutObject
190+
bucket:
191+
Ref: S3Bucket
192+
cors: true
193+
194+
requestParameters:
195+
# if requestParameters has a 'integration.request.path.object' property you should remove the key setting
196+
'integration.request.path.object': 'context.requestId'
197+
"integration.request.header.cache-control": "'public, max-age=31536000, immutable'"
198+
```
199+
179200
### SNS
180201

181202
Sample syntax for SNS proxy in `serverless.yml`.

lib/apiGateway/schema.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,15 @@ const proxiesSchemas = {
157157
.valid('GetObject', 'PutObject', 'DeleteObject')
158158
.required(),
159159
bucket: stringOrRef.required(),
160-
key: key.required()
160+
// don't accept a key when requestParameters has a 'integration.request.path.object' property
161+
key: Joi.when('requestParameters', {
162+
is: requestParameters
163+
.keys({ 'integration.request.path.object': Joi.string().required() })
164+
.required(),
165+
then: Joi.forbidden(),
166+
otherwise: key.required()
167+
}),
168+
requestParameters
161169
})
162170
}),
163171
sns: Joi.object({

lib/apiGateway/validate.test.js

+42-3
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,6 @@ describe('#validateServiceProxies()', () => {
581581

582582
const proxiesToTest = [
583583
{ proxy: 'kinesis', props: { streamName: 'streamName' } },
584-
{ proxy: 's3', props: { bucket: 'bucket', action: 'PutObject', key: 'myKey' } },
585584
{ proxy: 'sns', props: { topicName: 'topicName' } }
586585
]
587586
proxiesToTest.forEach(({ proxy, props }) => {
@@ -896,11 +895,11 @@ describe('#validateServiceProxies()', () => {
896895
)
897896
}
898897

899-
const shouldSucceed = (key, value) => {
898+
const shouldSucceed = (key, value, ...missing) => {
900899
serverlessApigatewayServiceProxy.serverless.service.custom = {
901900
apiGatewayServiceProxies: [
902901
{
903-
s3: getProxy(key, value)
902+
s3: getProxy(key, value, ...missing)
904903
}
905904
]
906905
}
@@ -1015,6 +1014,46 @@ describe('#validateServiceProxies()', () => {
10151014
{ param: 'myKey' }
10161015
)
10171016
})
1017+
1018+
it('should throw if requestParameters is not a string to string mapping', () => {
1019+
shouldError(
1020+
'child "s3" fails because [child "requestParameters" fails because [child "key2" fails because ["key2" must be a string]]]',
1021+
'requestParameters',
1022+
{ key1: 'value', key2: [] }
1023+
)
1024+
})
1025+
1026+
it('should not throw if requestParameters is a string to string mapping', () => {
1027+
shouldSucceed('requestParameters', { key1: 'value1', key2: 'value2' })
1028+
})
1029+
1030+
it('should throw if requestParameters has "integration.request.path.object" and key is defined', () => {
1031+
shouldError(
1032+
'child "s3" fails because [child "key" fails because ["key" is not allowed]]',
1033+
'requestParameters',
1034+
{
1035+
'integration.request.path.object': 'context.requestId',
1036+
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
1037+
}
1038+
)
1039+
})
1040+
1041+
it('should not throw if requestParameters has "integration.request.path.object" and key is not defined', () => {
1042+
shouldSucceed(
1043+
'requestParameters',
1044+
{
1045+
'integration.request.path.object': 'context.requestId',
1046+
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
1047+
},
1048+
'key'
1049+
)
1050+
})
1051+
1052+
it(`should not throw if requestParameters doesn't have "integration.request.path.object" and key is defined`, () => {
1053+
shouldSucceed('requestParameters', {
1054+
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
1055+
})
1056+
})
10181057
})
10191058

10201059
describe('sns', () => {

lib/package/s3/compileMethodsToS3.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,20 @@ module.exports = {
122122
getS3MethodIntegration(http) {
123123
const bucket = http.bucket
124124
const httpMethod = this.getIntegrationHttpMethod(http)
125-
const objectRequestParam = this.getObjectRequestParameter(http)
126-
const requestParams = _.merge(this.getIntegrationRequestParameters(http), {
127-
'integration.request.path.object': objectRequestParam,
125+
126+
let requestParams = _.merge(this.getIntegrationRequestParameters(http), {
128127
'integration.request.path.bucket': {
129128
'Fn::Sub': ["'${bucket}'", { bucket }]
130129
}
131130
})
131+
132+
if (_.has(http, 'key')) {
133+
const objectRequestParam = this.getObjectRequestParameter(http)
134+
requestParams = _.merge(requestParams, {
135+
'integration.request.path.object': objectRequestParam
136+
})
137+
}
138+
132139
const responseParams = this.getIntegrationResponseParameters(http)
133140

134141
const integration = {
@@ -141,7 +148,7 @@ module.exports = {
141148
'Fn::Sub': ['arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{object}', {}]
142149
},
143150
PassthroughBehavior: 'WHEN_NO_MATCH',
144-
RequestParameters: requestParams
151+
RequestParameters: _.merge(requestParams, http.requestParameters)
145152
}
146153

147154
const integrationResponse = {

lib/package/s3/compileMethodsToS3.test.js

+57
Original file line numberDiff line numberDiff line change
@@ -567,4 +567,61 @@ describe('#compileMethodsToS3()', () => {
567567

568568
expect(serverless.service.provider.compiledCloudFormationTemplate.Resources).to.be.empty
569569
})
570+
571+
const testRequestParameters = (requestParametersOverride) => {
572+
const http = {
573+
path: 's3',
574+
method: 'post',
575+
bucket: {
576+
Ref: 'MyBucket'
577+
},
578+
action: 'PutObject',
579+
auth: { authorizationType: 'NONE' },
580+
requestParameters: requestParametersOverride
581+
}
582+
583+
const requestParams = {
584+
'method.request.header.Content-Type': true
585+
}
586+
587+
const intRequestParams = {
588+
'integration.request.path.bucket': {
589+
'Fn::Sub': [
590+
"'${bucket}'",
591+
{
592+
bucket: {
593+
Ref: 'MyBucket'
594+
}
595+
}
596+
]
597+
},
598+
'integration.request.header.x-amz-acl': "'authenticated-read'",
599+
'integration.request.header.Content-Type': 'method.request.header.Content-Type'
600+
}
601+
602+
const responseParams = {
603+
'method.response.header.Content-Type': true,
604+
'method.response.header.Content-Length': true
605+
}
606+
607+
const intResponseParams = {
608+
'method.response.header.Content-Type': 'integration.response.header.Content-Type',
609+
'method.response.header.Content-Length': 'integration.response.header.Content-Length'
610+
}
611+
612+
testSingleProxy({
613+
http,
614+
logicalId: 'ApiGatewayMethods3Post',
615+
method: 'POST',
616+
intMethod: 'PUT',
617+
requestParams: requestParams,
618+
intRequestParams: _.merge(intRequestParams, requestParametersOverride),
619+
responseParams,
620+
intResponseParams
621+
})
622+
}
623+
624+
it('should add custom request parameters mapping', () => {
625+
testRequestParameters({ 'integration.request.path.object': 'context.requestId' })
626+
})
570627
})

0 commit comments

Comments
 (0)