Skip to content

Commit 373985b

Browse files
mingchunoChris Armstrong
andauthored
use CloudFormation DocumentationPart resources instead of direct creation (#1)
* use CloudFormation DocumentationPart resources instead of creating them directly * make required parameter available in documentation Co-authored-by: Chris Armstrong <chris.armstrong@gorillastack.com>
1 parent 26fc84d commit 373985b

File tree

2 files changed

+84
-22
lines changed

2 files changed

+84
-22
lines changed

src/documentation.js

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,69 @@ function determinePropertiesToGet (type) {
3535
switch (type) {
3636
case 'API':
3737
result.push('tags', 'info')
38-
break
38+
break;
3939
case 'METHOD':
4040
result.push('tags')
41-
break
41+
break;
42+
case 'PATH_PARAMETER':
43+
case 'QUERY_PARAMETER':
44+
case 'REQUEST_HEADER':
45+
case 'REQUEST_BODY':
46+
result.push('required')
47+
break;
4248
}
4349
return result
4450

4551
}
4652

53+
function mapPathLogicalPart(path) {
54+
return path.split('/').map((x) => {
55+
if (x.startsWith('{') && x.endsWith('}'))
56+
return x.slice(1, x.length - 1);
57+
return x[0].toUpperCase() + x.slice(1);
58+
}).join('')
59+
}
60+
61+
function mapStringToSafeHex(string) {
62+
return string.split().map((x) => x.charCodeAt(0).toString(16)).join('');
63+
}
64+
65+
function logicalIdCompatible(text) {
66+
const alphanumericRegex = /[^A-Za-z0-9]/g;
67+
return text.replace(alphanumericRegex, mapStringToSafeHex);
68+
}
69+
70+
function logicalIdForPart(location) {
71+
switch (location.type) {
72+
case 'API':
73+
return 'RestApiDocPart';
74+
case 'RESOURCE':
75+
return mapPathLogicalPart(location.path) + 'ResourceDocPart';
76+
case 'METHOD':
77+
return mapPathLogicalPart(location.path) + location.method + 'MethodDocPart';
78+
case 'QUERY_PARAMETER':
79+
return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + 'QueryParamDocPart';
80+
case 'REQUEST_BODY':
81+
return mapPathLogicalPart(location.path) + location.method + 'ReqBodyDocPart';
82+
case 'REQUEST_HEADER':
83+
return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + 'ReqHeadDocPart';
84+
case 'PATH_PARAMETER':
85+
return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + 'PathParamDocPart';
86+
case 'RESPONSE':
87+
return mapPathLogicalPart(location.path) + location.method + location.statusCode + 'ResDocPart';
88+
case 'RESPONSE_HEADER':
89+
return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + location.statusCode + 'ResHeadDocPart';
90+
case 'RESPONSE_BODY':
91+
return mapPathLogicalPart(location.path) + location.method + location.statusCode + 'ResBodyDocPart';
92+
case 'AUTHORIZER':
93+
return logicalIdCompatible(location.name) + 'AuthorizerDocPart';
94+
case 'MODEL':
95+
return logicalIdCompatible(location.name) + 'ModelDocPart';
96+
default:
97+
throw new Error('Unknown location type ' + location.type);
98+
}
99+
}
100+
47101
var autoVersion;
48102

49103
module.exports = function() {
@@ -110,25 +164,6 @@ module.exports = function() {
110164

111165
return Promise.reject(err);
112166
})
113-
.then(() =>
114-
aws.request('APIGateway', 'getDocumentationParts', {
115-
restApiId: this.restApiId,
116-
limit: 9999,
117-
})
118-
)
119-
.then(results => results.items.map(
120-
part => aws.request('APIGateway', 'deleteDocumentationPart', {
121-
documentationPartId: part.id,
122-
restApiId: this.restApiId,
123-
})
124-
))
125-
.then(promises => Promise.all(promises))
126-
.then(() => this.documentationParts.reduce((promise, part) => {
127-
return promise.then(() => {
128-
part.properties = JSON.stringify(part.properties);
129-
return aws.request('APIGateway', 'createDocumentationPart', part);
130-
});
131-
}, Promise.resolve()))
132167
.then(() => aws.request('APIGateway', 'createDocumentationVersion', {
133168
restApiId: this.restApiId,
134169
documentationVersion: this.getDocumentationVersion(),
@@ -190,6 +225,12 @@ module.exports = function() {
190225
.filter(output => output.OutputKey === 'AwsDocApiId')
191226
.map(output => output.OutputValue)[0];
192227

228+
return this._updateDocumentation();
229+
},
230+
231+
updateCfTemplateWithEndpoints: function updateCfTemplateWithEndpoints(restApiId) {
232+
this.restApiId = restApiId;
233+
193234
this.getGlobalDocumentationParts();
194235
this.getFunctionDocumentationParts();
195236

@@ -200,7 +241,25 @@ module.exports = function() {
200241
return;
201242
}
202243

203-
return this._updateDocumentation();
244+
const documentationPartResources = this.documentationParts.reduce((docParts, docPart) => {
245+
docParts[logicalIdForPart(docPart.location)] = {
246+
Type: 'AWS::ApiGateway::DocumentationPart',
247+
Properties: {
248+
Location: {
249+
Type: docPart.location.type,
250+
Name: docPart.location.name,
251+
Path: docPart.location.path,
252+
StatusCode: docPart.location.statusCode,
253+
Method: docPart.location.method,
254+
},
255+
Properties: JSON.stringify(docPart.properties),
256+
RestApiId: docPart.restApiId,
257+
}
258+
};
259+
return docParts;
260+
}, {});
261+
262+
Object.assign(this.cfTemplate.Resources, documentationPartResources);
204263
},
205264

206265
addDocumentationToApiGateway: function addDocumentationToApiGateway(resource, documentationPart, mapPath) {

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ class ServerlessAWSDocumentation {
170170
func.events.forEach(this.updateCfTemplateFromHttp.bind(this));
171171
});
172172

173+
// Add documentation parts for HTTP endpoints
174+
this.updateCfTemplateWithEndpoints(restApiId);
175+
173176
// Add models
174177
this.cfTemplate.Outputs.AwsDocApiId = {
175178
Description: 'API ID',

0 commit comments

Comments
 (0)