Skip to content

Commit 3fd93da

Browse files
committed
Added support for Shared API Gateway
1 parent eeb0cb0 commit 3fd93da

File tree

4 files changed

+255
-34
lines changed

4 files changed

+255
-34
lines changed

lib/deploy/events/apiGateway/deployment.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = {
1212
[this.apiGatewayDeploymentLogicalId]: {
1313
Type: 'AWS::ApiGateway::Deployment',
1414
Properties: {
15-
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
15+
RestApiId: this.provider.getApiGatewayRestApiId(),
1616
StageName: this.options.stage,
1717
},
1818
DependsOn: this.apiGatewayMethodLogicalIds,
@@ -26,7 +26,7 @@ module.exports = {
2626
'Fn::Join': ['',
2727
[
2828
'https://',
29-
{ Ref: this.apiGatewayRestApiLogicalId },
29+
this.provider.getApiGatewayRestApiId(),
3030
`.execute-api.${this.options.region}.amazonaws.com/${this.options.stage}`,
3131
],
3232
],

lib/deploy/events/apiGateway/methods.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module.exports = {
2020
AuthorizationType: 'NONE',
2121
ApiKeyRequired: Boolean(event.http.private),
2222
ResourceId: resourceId,
23-
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
23+
RestApiId: this.provider.getApiGatewayRestApiId(),
2424
},
2525
};
2626

lib/deploy/events/apiGateway/resources.js

Lines changed: 214 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,248 @@ const _ = require('lodash');
66
module.exports = {
77

88
compileResources() {
9-
const resourcePaths = this.getResourcePaths();
9+
this.apiGatewayResources = this.getResourcePaths();
1010

11-
this.apiGatewayResourceNames = {};
12-
this.apiGatewayResourceLogicalIds = {};
11+
// ['users', 'users/create', 'users/create/something']
12+
_.keys(this.apiGatewayResources).forEach((path) => {
13+
const resource = this.apiGatewayResources[path];
14+
if (resource.resourceId) {
15+
return;
16+
}
1317

14-
resourcePaths.forEach(path => {
15-
const pathArray = path.split('/');
16-
const resourceName = this.provider.naming.normalizePath(path);
17-
const resourceLogicalId = this.provider.naming.getResourceLogicalId(path);
18-
const pathPart = pathArray.pop();
19-
const parentPath = pathArray.join('/');
20-
const parentRef = this.getResourceId(parentPath);
18+
resource.resourceLogicalId = this.provider.naming.getResourceLogicalId(path);
19+
resource.resourceId = { Ref: resource.resourceLogicalId };
2120

22-
this.apiGatewayResourceNames[path] = resourceName;
23-
this.apiGatewayResourceLogicalIds[path] = resourceLogicalId;
21+
const parentRef = resource.parent
22+
? resource.parent.resourceId : this.getResourceId();
2423

2524
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
26-
[resourceLogicalId]: {
25+
[resource.resourceLogicalId]: {
2726
Type: 'AWS::ApiGateway::Resource',
2827
Properties: {
2928
ParentId: parentRef,
30-
PathPart: pathPart,
31-
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
29+
PathPart: resource.pathPart,
30+
RestApiId: this.provider.getApiGatewayRestApiId(),
3231
},
3332
},
3433
});
3534
});
35+
3636
return BbPromise.resolve();
3737
},
3838

39+
combineResourceTrees(trees) {
40+
const self = this;
41+
42+
function getNodePaths(result, node) {
43+
const r = result;
44+
r[node.path] = node;
45+
if (!node.name) {
46+
r[node.path].name = self.provider.naming.normalizePath(node.path);
47+
}
48+
49+
node.children.forEach((child) => getNodePaths(result, child));
50+
}
51+
52+
return _.reduce(trees, (result, tree) => {
53+
getNodePaths(result, tree);
54+
return result;
55+
}, {});
56+
},
57+
3958
getResourcePaths() {
40-
const paths = _.reduce(this.pluginhttpValidated.events, (resourcePaths, event) => {
41-
let path = event.http.path;
59+
const trees = [];
60+
const predefinedResourceNodes = [];
61+
const methodNodes = [];
62+
const predefinedResources = this.provider.getApiGatewayPredefinedResources();
63+
64+
65+
function cutBranch(node) {
66+
if (!node.parent) {
67+
return;
68+
}
69+
70+
const n = node;
71+
if (node.parent.children.length <= 1) {
72+
n.parent.children = [];
73+
} else {
74+
n.parent.children = node.parent.children.filter((c) => c.path !== n.path);
75+
n.parent.isCut = true;
76+
}
77+
n.parent = null;
78+
}
79+
80+
// organize all resource paths into N-ary tree
81+
function applyResource(resource, isMethod) {
82+
let root;
83+
let parent;
84+
let currentPath;
85+
const path = resource.path.replace(/^\//, '').replace(/\/$/, '');
86+
const pathParts = path.split('/');
87+
88+
function applyNodeResource(node, parts, index) {
89+
const n = node;
90+
if (index === parts.length - 1) {
91+
n.name = resource.name;
92+
if (resource.resourceId) {
93+
n.resourceId = resource.resourceId;
94+
if (_.every(predefinedResourceNodes, (iter) => iter.path !== n.path)) {
95+
predefinedResourceNodes.push(node);
96+
}
97+
}
98+
if (isMethod && !node.hasMethod) {
99+
n.hasMethod = true;
100+
if (_.every(methodNodes, (iter) => iter.path !== n.path)) {
101+
methodNodes.push(node);
102+
}
103+
}
104+
}
105+
106+
parent = node;
107+
}
108+
109+
pathParts.forEach((pathPart, index) => {
110+
currentPath = currentPath ? `${currentPath}/${pathPart}` : pathPart;
111+
root = root || _.find(trees, (node) => node.path === currentPath);
112+
parent = parent || root;
113+
114+
let node;
115+
if (parent) {
116+
if (parent.path === currentPath) {
117+
applyNodeResource(parent, pathParts, index);
118+
return;
119+
} else if (parent.children.length > 0) {
120+
node = _.find(parent.children, (n) => n.path === currentPath);
121+
if (node) {
122+
applyNodeResource(node, pathParts, index);
123+
return;
124+
}
125+
}
126+
}
127+
128+
node = {
129+
path: currentPath,
130+
pathPart,
131+
parent,
132+
133+
level: index,
134+
children: [],
135+
};
136+
137+
if (parent) {
138+
parent.children.push(node);
139+
}
140+
141+
if (!root) {
142+
root = node;
143+
trees.push(root);
144+
}
145+
146+
applyNodeResource(node, pathParts, index);
147+
});
148+
}
149+
150+
predefinedResources.forEach(applyResource);
151+
this.pluginhttpValidated.events.forEach((event) => {
152+
if (event.http.path) {
153+
applyResource(event.http, true);
154+
}
155+
});
156+
157+
// if predefinedResources array is empty, return all paths
158+
if (predefinedResourceNodes.length === 0) {
159+
return this.combineResourceTrees(trees);
160+
}
161+
162+
// if all methods have resource ID already, no need to validate resource trees
163+
if (_.every(this.pluginhttpValidated.events, (event) =>
164+
_.some(predefinedResourceNodes, (node) =>
165+
node.path === event.http.path))) {
166+
return _.reduce(predefinedResources, (resourceMap, resource) => {
167+
const r = resourceMap;
168+
r[resource.path] = resource;
169+
170+
if (!resource.name) {
171+
r[resource.path].name = this.provider.naming.normalizePath(resource.path);
172+
}
173+
return r;
174+
}, {});
175+
}
176+
177+
// cut resource branches from trees
178+
const sortedResourceNodes = _.sortBy(predefinedResourceNodes,
179+
node => node.level);
180+
const validatedTrees = [];
181+
182+
for (let i = sortedResourceNodes.length - 1; i >= 0; i--) {
183+
const node = sortedResourceNodes[i];
184+
let parent = node;
42185

43-
while (path !== '') {
44-
if (resourcePaths.indexOf(path) === -1) {
45-
resourcePaths.push(path);
186+
while (parent && parent.parent) {
187+
if (parent.parent.hasMethod && !parent.parent.resourceId) {
188+
throw new Error(`Resource ID for path ${parent.parent.path} is required`);
46189
}
47190

48-
const splittedPath = path.split('/');
49-
splittedPath.pop();
50-
path = splittedPath.join('/');
191+
if (parent.parent.resourceId || parent.parent.children.length > 1) {
192+
cutBranch(parent);
193+
break;
194+
}
195+
196+
parent = parent.parent;
51197
}
52-
return resourcePaths;
53-
}, []);
54-
return _.sortBy(paths, path => path.split('/').length);
198+
}
199+
200+
// get branches that begin from root resource
201+
methodNodes.forEach((node) => {
202+
let iter = node;
203+
while (iter) {
204+
if (iter.resourceId) {
205+
cutBranch(iter);
206+
if (_.every(validatedTrees, (t) => t.path !== node.path)) {
207+
validatedTrees.push(iter);
208+
}
209+
210+
break;
211+
}
212+
213+
if (iter.isCut || (!iter.parent && iter.level > 0)) {
214+
throw new Error(`Resource ID for path ${iter.path} is required`);
215+
}
216+
217+
if (!iter.parent) {
218+
validatedTrees.push(iter);
219+
break;
220+
}
221+
222+
iter = iter.parent;
223+
}
224+
});
225+
226+
return this.combineResourceTrees(validatedTrees);
55227
},
56228

57229
getResourceId(path) {
58-
if (path === '') {
59-
return { 'Fn::GetAtt': [this.apiGatewayRestApiLogicalId, 'RootResourceId'] };
230+
if (!path) {
231+
return this.provider.getApiGatewayRestApiRootResourceId();
232+
}
233+
234+
if (!this.apiGatewayResources || !this.apiGatewayResources[path]) {
235+
throw new Error(`Can not find API Gateway resource from path ${path}`);
60236
}
61-
return { Ref: this.apiGatewayResourceLogicalIds[path] };
237+
238+
if (!this.apiGatewayResources[path].resourceId
239+
&& this.apiGatewayResources[path].resourceLogicalId) {
240+
this.apiGatewayResources[path].resourceId =
241+
{ Ref: this.apiGatewayResources[path].resourceLogicalId };
242+
}
243+
return this.apiGatewayResources[path].resourceId;
62244
},
63245

64246
getResourceName(path) {
65-
if (path === '') {
247+
if (path === '' || !this.apiGatewayResources) {
66248
return '';
67249
}
68-
return this.apiGatewayResourceNames[path];
250+
251+
return this.apiGatewayResources[path].name;
69252
},
70253
};

lib/deploy/events/apiGateway/restApi.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,55 @@ const BbPromise = require('bluebird');
55

66
module.exports = {
77
compileRestApi() {
8+
if (this.serverless.service.provider.apiGateway &&
9+
this.serverless.service.provider.apiGateway.restApiId) {
10+
return BbPromise.resolve();
11+
}
12+
813
this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId();
914

15+
let endpointType = 'EDGE';
16+
17+
if (this.serverless.service.provider.endpointType) {
18+
const validEndpointTypes = ['REGIONAL', 'EDGE', 'PRIVATE'];
19+
endpointType = this.serverless.service.provider.endpointType;
20+
21+
if (typeof endpointType !== 'string') {
22+
throw new this.serverless.classes.Error('endpointType must be a string');
23+
}
24+
25+
26+
if (!_.includes(validEndpointTypes, endpointType.toUpperCase())) {
27+
const message = 'endpointType must be one of "REGIONAL" or "EDGE" or "PRIVATE". ' +
28+
`You provided ${endpointType}.`;
29+
throw new this.serverless.classes.Error(message);
30+
}
31+
endpointType = endpointType.toUpperCase();
32+
}
33+
1034
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
1135
[this.apiGatewayRestApiLogicalId]: {
1236
Type: 'AWS::ApiGateway::RestApi',
1337
Properties: {
1438
Name: this.provider.naming.getApiGatewayName(),
39+
EndpointConfiguration: {
40+
Types: [endpointType],
41+
},
1542
},
1643
},
1744
});
1845

46+
if (!_.isEmpty(this.serverless.service.provider.resourcePolicy)) {
47+
const policy = {
48+
Version: '2012-10-17',
49+
Statement: this.serverless.service.provider.resourcePolicy,
50+
};
51+
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate
52+
.Resources[this.apiGatewayRestApiLogicalId].Properties, {
53+
Policy: policy,
54+
});
55+
}
56+
1957
return BbPromise.resolve();
2058
},
2159
};

0 commit comments

Comments
 (0)