From 3bf7628691b66c9df8b4d1f77f7dbf97b4f74714 Mon Sep 17 00:00:00 2001 From: Chris Armstrong Date: Wed, 29 Aug 2018 17:13:45 +1000 Subject: [PATCH 1/2] use CloudFormation DocumentationPart resources instead of creating them directly --- src/documentation.js | 96 ++++++++++++++++++++++++++++++++++---------- src/index.js | 3 ++ 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/documentation.js b/src/documentation.js index 4862832..bf4fe27 100644 --- a/src/documentation.js +++ b/src/documentation.js @@ -35,15 +35,63 @@ function determinePropertiesToGet (type) { switch (type) { case 'API': result.push('tags', 'info') - break + break; case 'METHOD': result.push('tags') - break + break; } return result } +function mapPathLogicalPart(path) { + return path.split('/').map((x) => { + if (x.startsWith('{') && x.endsWith('}')) + return x.slice(1, x.length - 1); + return x[0].toUpperCase() + x.slice(1); + }).join('') +} + +function mapStringToSafeHex(string) { + return string.split().map((x) => x.charCodeAt(0).toString(16)).join(''); +} + +function logicalIdCompatible(text) { + const alphanumericRegex = /[^A-Za-z0-9]/g; + return text.replace(alphanumericRegex, mapStringToSafeHex); +} + +function logicalIdForPart(location) { + switch (location.type) { + case 'API': + return 'RestApiDocPart'; + case 'RESOURCE': + return mapPathLogicalPart(location.path) + 'ResourceDocPart'; + case 'METHOD': + return mapPathLogicalPart(location.path) + location.method + 'MethodDocPart'; + case 'QUERY_PARAMETER': + return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + 'QueryParamDocPart'; + case 'REQUEST_BODY': + return mapPathLogicalPart(location.path) + location.method + 'ReqBodyDocPart'; + case 'REQUEST_HEADER': + return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + 'ReqHeadDocPart'; + case 'PATH_PARAMETER': + return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + 'PathParamDocPart'; + case 'RESPONSE': + return mapPathLogicalPart(location.path) + location.method + location.statusCode + 'ResDocPart'; + case 'RESPONSE_HEADER': + return mapPathLogicalPart(location.path) + location.method + logicalIdCompatible(location.name) + location.statusCode + 'ResHeadDocPart'; + case 'RESPONSE_BODY': + return mapPathLogicalPart(location.path) + location.method + location.statusCode + 'ResBodyDocPart'; + case 'AUTHORIZER': + return logicalIdCompatible(location.name) + 'AuthorizerDocPart'; + case 'MODEL': + return logicalIdCompatible(location.name) + 'ModelDocPart'; + default: + throw new Error('Unknown location type ' + location.type); + } +} + var autoVersion; module.exports = function() { @@ -110,25 +158,6 @@ module.exports = function() { return Promise.reject(err); }) - .then(() => - aws.request('APIGateway', 'getDocumentationParts', { - restApiId: this.restApiId, - limit: 9999, - }) - ) - .then(results => results.items.map( - part => aws.request('APIGateway', 'deleteDocumentationPart', { - documentationPartId: part.id, - restApiId: this.restApiId, - }) - )) - .then(promises => Promise.all(promises)) - .then(() => this.documentationParts.reduce((promise, part) => { - return promise.then(() => { - part.properties = JSON.stringify(part.properties); - return aws.request('APIGateway', 'createDocumentationPart', part); - }); - }, Promise.resolve())) .then(() => aws.request('APIGateway', 'createDocumentationVersion', { restApiId: this.restApiId, documentationVersion: this.getDocumentationVersion(), @@ -189,6 +218,11 @@ module.exports = function() { this.restApiId = result.Stacks[0].Outputs .filter(output => output.OutputKey === 'AwsDocApiId') .map(output => output.OutputValue)[0]; + return this._updateDocumentation(); + }, + + updateCfTemplateWithEndpoints: function updateCfTemplateWithEndpoints(restApiId) { + this.restApiId = restApiId; this.getGlobalDocumentationParts(); this.getFunctionDocumentationParts(); @@ -200,7 +234,25 @@ module.exports = function() { return; } - return this._updateDocumentation(); + const documentationPartResources = this.documentationParts.reduce((docParts, docPart) => { + docParts[logicalIdForPart(docPart.location)] = { + Type: 'AWS::ApiGateway::DocumentationPart', + Properties: { + Location: { + Type: docPart.location.type, + Name: docPart.location.name, + Path: docPart.location.path, + StatusCode: docPart.location.statusCode, + Method: docPart.location.method, + }, + Properties: JSON.stringify(docPart.properties), + RestApiId: docPart.restApiId, + } + }; + return docParts; + }, {}); + + Object.assign(this.cfTemplate.Resources, documentationPartResources); }, addDocumentationToApiGateway: function addDocumentationToApiGateway(resource, documentationPart, mapPath) { diff --git a/src/index.js b/src/index.js index 16f94f2..7c117f0 100644 --- a/src/index.js +++ b/src/index.js @@ -80,6 +80,9 @@ class ServerlessAWSDocumentation { func.events.forEach(this.updateCfTemplateFromHttp.bind(this)); }); + // Add documentation parts for HTTP endpoints + this.updateCfTemplateWithEndpoints(restApiId); + // Add models this.cfTemplate.Outputs.AwsDocApiId = { Description: 'API ID', From abe3680c0382811ad7e5f9840b6bb88a7f5cf854 Mon Sep 17 00:00:00 2001 From: Chris Armstrong Date: Thu, 30 Aug 2018 16:49:57 +1000 Subject: [PATCH 2/2] make required parameter available in documentation --- src/documentation.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/documentation.js b/src/documentation.js index bf4fe27..438f503 100644 --- a/src/documentation.js +++ b/src/documentation.js @@ -39,6 +39,12 @@ function determinePropertiesToGet (type) { case 'METHOD': result.push('tags') break; + case 'PATH_PARAMETER': + case 'QUERY_PARAMETER': + case 'REQUEST_HEADER': + case 'REQUEST_BODY': + result.push('required') + break; } return result @@ -218,6 +224,7 @@ module.exports = function() { this.restApiId = result.Stacks[0].Outputs .filter(output => output.OutputKey === 'AwsDocApiId') .map(output => output.OutputValue)[0]; + return this._updateDocumentation(); },