Skip to content

Commit 9212b17

Browse files
authored
Merge pull request #49 from Hans-Seek/pathoverride
Adding Dynamic Path Override
2 parents 0b8c5f1 + 249cd00 commit 9212b17

File tree

5 files changed

+309
-2
lines changed

5 files changed

+309
-2
lines changed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,62 @@ custom:
197197
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
198198
```
199199

200+
#### Customize the Path Override in API Gateway
201+
202+
Added the new customization parameter that lets the user set a custom Path Override in API Gateway other than the `{bucket}/{object}`
203+
This parameter is optional and if not set, will fall back to `{bucket}/{object}`
204+
The Path Override will add `{bucket}/` automatically in front
205+
206+
Please keep in mind, that key or path.object still needs to be set at the moment (maybe this will be made optional later on with this)
207+
208+
Usage (With 2 Path Parameters (folder and file and a fixed file extension)):
209+
210+
```yaml
211+
custom:
212+
apiGatewayServiceProxies:
213+
- s3:
214+
path: /s3/{folder}/{file}
215+
method: get
216+
action: GetObject
217+
pathOverride: '{folder}/{file}.xml'
218+
bucket:
219+
Ref: S3Bucket
220+
cors: true
221+
222+
requestParameters:
223+
# if requestParameters has a 'integration.request.path.object' property you should remove the key setting
224+
'integration.request.path.folder': 'method.request.path.folder'
225+
'integration.request.path.file': 'method.request.path.file'
226+
'integration.request.path.object': 'context.requestId'
227+
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
228+
```
229+
This will result in API Gateway setting the Path Override attribute to `{bucket}/{folder}/{file}.xml`
230+
So for example if you navigate to the API Gatway endpoint `/language/en` it will fetch the file in S3 from `{bucket}/language/en.xml`
231+
232+
##### Can use greedy, for deeper Folders
233+
The forementioned example can also be shortened by a greedy approach. Thanks to @taylorreece for mentioning this.
234+
235+
```yaml
236+
custom:
237+
apiGatewayServiceProxies:
238+
- s3:
239+
path: /s3/{myPath+}
240+
method: get
241+
action: GetObject
242+
pathOverride: '{myPath}.xml'
243+
bucket:
244+
Ref: S3Bucket
245+
cors: true
246+
247+
requestParameters:
248+
# if requestParameters has a 'integration.request.path.object' property you should remove the key setting
249+
'integration.request.path.myPath': 'method.request.path.myPath'
250+
'integration.request.path.object': 'context.requestId'
251+
'integration.request.header.cache-control': "'public, max-age=31536000, immutable'"
252+
```
253+
254+
This will translate for example `/s3/a/b/c` to `a/b/c.xml`
255+
200256
### SNS
201257

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

lib/apiGateway/schema.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@ const roleArn = stringOrGetAtt('roleArn', 'Arn')
9191

9292
const acceptParameters = Joi.object().pattern(Joi.string(), Joi.boolean().required())
9393

94+
const pathOverride = Joi.string()
95+
9496
const proxy = Joi.object({
9597
path,
98+
pathOverride,
9699
method,
97100
cors,
98101
authorizationType,

lib/package/s3/compileMethodsToS3.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,18 @@ module.exports = {
142142
'Fn::GetAtt': ['ApigatewayToS3Role', 'Arn']
143143
}
144144

145+
let pather = '{bucket}/{object}'
146+
147+
if (_.has(http, 'pathOverride')) {
148+
pather = '{bucket}/' + http.pathOverride
149+
}
150+
145151
const integration = {
146152
IntegrationHttpMethod: httpMethod,
147153
Type: 'AWS',
148154
Credentials: roleArn,
149155
Uri: {
150-
'Fn::Sub': ['arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{object}', {}]
156+
'Fn::Sub': ['arn:aws:apigateway:${AWS::Region}:s3:path/' + pather, {}]
151157
},
152158
PassthroughBehavior: 'WHEN_NO_MATCH',
153159
RequestParameters: _.merge(requestParams, http.requestParameters)

lib/package/s3/compileMethodsToS3.test.js

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,4 +681,246 @@ describe('#compileMethodsToS3()', () => {
681681
'method.request.path.key': true
682682
})
683683
})
684+
685+
it('should create corresponding resources when s3 GetObject proxy is given with path override', () => {
686+
serverlessApigatewayServiceProxy.validated = {
687+
events: [
688+
{
689+
serviceName: 's3',
690+
http: {
691+
path: '/{folder}/{item}',
692+
method: 'get',
693+
bucket: {
694+
Ref: 'MyBucket'
695+
},
696+
action: 'GetObject',
697+
key: {
698+
pathParam: 'item'
699+
},
700+
pathOverride: '{folder}/{item}.xml',
701+
auth: { authorizationType: 'NONE' },
702+
requestParameters: {
703+
'integration.request.path.folder': 'method.request.path.folder',
704+
'integration.request.path.item': 'method.request.path.item'
705+
}
706+
}
707+
}
708+
]
709+
}
710+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
711+
serverlessApigatewayServiceProxy.apiGatewayResources = {
712+
'/{folder}/{item}': {
713+
name: 'po',
714+
resourceLogicalId: 'ApiGatewayPathOverrideS3'
715+
}
716+
}
717+
718+
serverlessApigatewayServiceProxy.compileMethodsToS3()
719+
expect(serverless.service.provider.compiledCloudFormationTemplate.Resources).to.deep.equal({
720+
ApiGatewayMethodpoGet: {
721+
Type: 'AWS::ApiGateway::Method',
722+
Properties: {
723+
HttpMethod: 'GET',
724+
RequestParameters: {
725+
'method.request.path.folder': true,
726+
'method.request.path.item': true
727+
},
728+
AuthorizationType: 'NONE',
729+
AuthorizationScopes: undefined,
730+
AuthorizerId: undefined,
731+
ApiKeyRequired: false,
732+
ResourceId: { Ref: 'ApiGatewayPathOverrideS3' },
733+
RestApiId: { Ref: 'ApiGatewayRestApi' },
734+
Integration: {
735+
Type: 'AWS',
736+
IntegrationHttpMethod: 'GET',
737+
Credentials: { 'Fn::GetAtt': ['ApigatewayToS3Role', 'Arn'] },
738+
Uri: {
739+
'Fn::Sub': [
740+
'arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{folder}/{item}.xml',
741+
{}
742+
]
743+
},
744+
PassthroughBehavior: 'WHEN_NO_MATCH',
745+
RequestParameters: {
746+
'integration.request.path.bucket': {
747+
'Fn::Sub': [
748+
"'${bucket}'",
749+
{
750+
bucket: {
751+
Ref: 'MyBucket'
752+
}
753+
}
754+
]
755+
},
756+
'integration.request.path.object': 'method.request.path.item',
757+
'integration.request.path.folder': 'method.request.path.folder',
758+
'integration.request.path.item': 'method.request.path.item'
759+
},
760+
IntegrationResponses: [
761+
{
762+
StatusCode: 400,
763+
SelectionPattern: '4\\d{2}',
764+
ResponseParameters: {},
765+
ResponseTemplates: {}
766+
},
767+
{
768+
StatusCode: 500,
769+
SelectionPattern: '5\\d{2}',
770+
ResponseParameters: {},
771+
ResponseTemplates: {}
772+
},
773+
{
774+
StatusCode: 200,
775+
SelectionPattern: '2\\d{2}',
776+
ResponseParameters: {
777+
'method.response.header.Content-Type': 'integration.response.header.Content-Type',
778+
'method.response.header.content-type': 'integration.response.header.content-type'
779+
},
780+
ResponseTemplates: {}
781+
}
782+
]
783+
},
784+
MethodResponses: [
785+
{
786+
ResponseParameters: {
787+
'method.response.header.Content-Type': true,
788+
'method.response.header.content-type': true
789+
},
790+
ResponseModels: {},
791+
StatusCode: 200
792+
},
793+
{
794+
ResponseParameters: {},
795+
ResponseModels: {},
796+
StatusCode: 400
797+
},
798+
{
799+
ResponseParameters: {},
800+
ResponseModels: {},
801+
StatusCode: 500
802+
}
803+
]
804+
}
805+
}
806+
})
807+
})
808+
809+
it('should create corresponding resources when s3 GetObject proxy is given with a greedy path override', () => {
810+
serverlessApigatewayServiceProxy.validated = {
811+
events: [
812+
{
813+
serviceName: 's3',
814+
http: {
815+
path: '/{myPath+}',
816+
method: 'get',
817+
bucket: {
818+
Ref: 'MyBucket'
819+
},
820+
action: 'GetObject',
821+
key: {
822+
pathParam: 'myPath'
823+
},
824+
pathOverride: '{myPath}.xml',
825+
auth: { authorizationType: 'NONE' },
826+
requestParameters: {
827+
'integration.request.path.myPath': 'method.request.path.myPath'
828+
}
829+
}
830+
}
831+
]
832+
}
833+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
834+
serverlessApigatewayServiceProxy.apiGatewayResources = {
835+
'/{myPath+}': {
836+
name: 'greedyPath',
837+
resourceLogicalId: 'ApiGatewayPathOverrideS3'
838+
}
839+
}
840+
841+
serverlessApigatewayServiceProxy.compileMethodsToS3()
842+
expect(serverless.service.provider.compiledCloudFormationTemplate.Resources).to.deep.equal({
843+
ApiGatewayMethodgreedyPathGet: {
844+
Type: 'AWS::ApiGateway::Method',
845+
Properties: {
846+
HttpMethod: 'GET',
847+
RequestParameters: {
848+
'method.request.path.myPath': true
849+
},
850+
AuthorizationType: 'NONE',
851+
AuthorizationScopes: undefined,
852+
AuthorizerId: undefined,
853+
ApiKeyRequired: false,
854+
ResourceId: { Ref: 'ApiGatewayPathOverrideS3' },
855+
RestApiId: { Ref: 'ApiGatewayRestApi' },
856+
Integration: {
857+
Type: 'AWS',
858+
IntegrationHttpMethod: 'GET',
859+
Credentials: { 'Fn::GetAtt': ['ApigatewayToS3Role', 'Arn'] },
860+
Uri: {
861+
'Fn::Sub': ['arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{myPath}.xml', {}]
862+
},
863+
PassthroughBehavior: 'WHEN_NO_MATCH',
864+
RequestParameters: {
865+
'integration.request.path.bucket': {
866+
'Fn::Sub': [
867+
"'${bucket}'",
868+
{
869+
bucket: {
870+
Ref: 'MyBucket'
871+
}
872+
}
873+
]
874+
},
875+
'integration.request.path.object': 'method.request.path.myPath',
876+
'integration.request.path.myPath': 'method.request.path.myPath'
877+
},
878+
IntegrationResponses: [
879+
{
880+
StatusCode: 400,
881+
SelectionPattern: '4\\d{2}',
882+
ResponseParameters: {},
883+
ResponseTemplates: {}
884+
},
885+
{
886+
StatusCode: 500,
887+
SelectionPattern: '5\\d{2}',
888+
ResponseParameters: {},
889+
ResponseTemplates: {}
890+
},
891+
{
892+
StatusCode: 200,
893+
SelectionPattern: '2\\d{2}',
894+
ResponseParameters: {
895+
'method.response.header.Content-Type': 'integration.response.header.Content-Type',
896+
'method.response.header.content-type': 'integration.response.header.content-type'
897+
},
898+
ResponseTemplates: {}
899+
}
900+
]
901+
},
902+
MethodResponses: [
903+
{
904+
ResponseParameters: {
905+
'method.response.header.Content-Type': true,
906+
'method.response.header.content-type': true
907+
},
908+
ResponseModels: {},
909+
StatusCode: 200
910+
},
911+
{
912+
ResponseParameters: {},
913+
ResponseModels: {},
914+
StatusCode: 400
915+
},
916+
{
917+
ResponseParameters: {},
918+
ResponseModels: {},
919+
StatusCode: 500
920+
}
921+
]
922+
}
923+
}
924+
})
925+
})
684926
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@
6363
"type": "git",
6464
"url": "https://github.com/horike37/serverless-apigateway-service-proxy.git"
6565
}
66-
}
66+
}

0 commit comments

Comments
 (0)