Skip to content

Commit

Permalink
Merge pull request #345 from amplify-education/improve_response_paging
Browse files Browse the repository at this point in the history
Add utility function for doing paging and use it for ACM certs and API mappings
  • Loading branch information
aoskotsky-amplify committed May 18, 2020
2 parents 19a37f7 + 0dc1ede commit 36bd088
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 52 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [4.1.0] - 2020-05-18

### Changed
- Fixed issue when there are multiple pages of base path mappings. Also refactored how paging is handled throughout the code. Thanks @kzhou57 for discovering this ([#345](https://github.com/amplify-education/serverless-domain-manager/pull/345))

## [4.0.1] - 2020-05-12

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "serverless-domain-manager",
"version": "4.0.1",
"version": "4.1.0",
"engines": {
"node": ">=4.0"
},
Expand Down
71 changes: 50 additions & 21 deletions src/index.ts
@@ -1,5 +1,6 @@
"use strict";

import {Service} from "aws-sdk";
import chalk from "chalk";
import DomainConfig = require("./DomainConfig");
import DomainInfo = require("./DomainInfo");
Expand Down Expand Up @@ -315,14 +316,14 @@ class ServerlessCustomDomain {
let certificateName = domain.certificateName; // The certificate name

try {
let certificates = [];
let nextToken;
do {
const certData = await this.acm.listCertificates(
{ CertificateStatuses: certStatuses, NextToken: nextToken }).promise();
certificates = certificates.concat(certData.CertificateSummaryList);
nextToken = certData.NextToken;
} while (nextToken);
const certificates = await this.getAWSPagedResults(
this.acm,
"listCertificates",
"CertificateSummaryList",
"NextToken",
"NextToken",
{ CertificateStatuses: certStatuses },
);

// The more specific name will be the longest
let nameLength = 0;
Expand Down Expand Up @@ -555,20 +556,19 @@ class ServerlessCustomDomain {
}

public async getBasePathMapping(domain: DomainConfig): Promise<AWS.ApiGatewayV2.GetApiMappingResponse> {
const params = {
DomainName: domain.givenDomainName,
};
try {
const mappings = await this.apigatewayV2.getApiMappings(params).promise();

if (mappings.Items.length === 0) {
return;
} else {
for (const mapping of mappings.Items) {
if (mapping.ApiId === domain.apiId
|| (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching) ) {
return mapping;
}
const mappings = await this.getAWSPagedResults(
this.apigatewayV2,
"getApiMappings",
"Items",
"NextToken",
"NextToken",
{ DomainName: domain.givenDomainName },
);
for (const mapping of mappings) {
if (mapping.ApiId === domain.apiId
|| (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching) ) {
return mapping;
}
}
} catch (err) {
Expand Down Expand Up @@ -774,6 +774,35 @@ class ServerlessCustomDomain {
}
}

/**
* Iterate through the pages of a AWS SDK response and collect them into a single array
*
* @param service - The AWS service instance to use to make the calls
* @param funcName - The function name in the service to call
* @param resultsKey - The key name in the response that contains the items to return
* @param nextTokenKey - The request key name to append to the request that has the paging token value
* @param nextRequestTokenKey - The response key name that has the next paging token value
* @param params - Parameters to send in the request
*/
public async getAWSPagedResults(
service: Service,
funcName: string,
resultsKey: string,
nextTokenKey: string,
nextRequestTokenKey: string,
params: object,
): Promise<any[]> {
let results = [];
let response = await service[funcName](params).promise();
results = results.concat(response[resultsKey]);
while (response.hasOwnProperty(nextRequestTokenKey) && response[nextRequestTokenKey]) {
params[nextTokenKey] = response[nextRequestTokenKey];
response = await service[funcName](params).promise();
results = results.concat(response[resultsKey]);
}
return results;
}

/**
* Prints out a summary of all domain manager related info
*/
Expand Down
42 changes: 15 additions & 27 deletions test/integration-tests/test-utilities.ts
Expand Up @@ -56,52 +56,40 @@ async function createTempDir(tempDir, folderName) {
/**
* Gets endpoint type of given URL from AWS
* @param url
* @returns {Promise<String|null>} Resolves to String if endpoint exists, else null.
* @returns {Promise<String>}
*/
async function getEndpointType(url) {
const params = {
const result = await apiGateway.getDomainName({
domainName: url,
};
try {
const result = await apiGateway.getDomainName(params).promise();
return result.endpointConfiguration.types[0];
} catch (err) {
return null;
}
}).promise();

return result.endpointConfiguration.types[0];
}

/**
* Gets stage of given URL from AWS
* @param url
* @returns {Promise<String|null>} Resolves to String if stage exists, else null.
* @returns {Promise<String>}
*/
async function getStage(url) {
const params = {
const result = await apiGateway.getBasePathMappings({
domainName: url,
};
try {
const result = await apiGateway.getBasePathMappings(params).promise();
return result.items[0].stage;
} catch (err) {
return null;
}
}).promise();

return result.items[0].stage;
}

/**
* Gets basePath of given URL from AWS
* @param url
* @returns {Promise<String|null>} Resolves to String if basePath exists, else null.
* @returns {Promise<String>}
*/
async function getBasePath(url) {
const params = {
const result = await apiGateway.getBasePathMappings({
domainName: url,
};
try {
const result = await apiGateway.getBasePathMappings(params).promise();
return result.items[0].basePath;
} catch (err) {
return null;
}
}).promise();

return result.items[0].basePath;
}

/**
Expand Down
35 changes: 35 additions & 0 deletions test/unit-tests/index.test.ts
Expand Up @@ -1478,4 +1478,39 @@ describe("Custom Domain Plugin", () => {
consoleOutput = [];
});
});

describe("AWS paged results", () => {
it("Should combine paged results into a list", async () => {
let callCount = 0;
const responses = [{
Items: ["a", "b"],
NextToken: "1",
},
{
Items: ["c", "d"],
NextToken: "2",
},
{
Items: ["e"],
},
{
Items: ["f"], // this call should never happen since its after the last request that included a token
}];
AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
callback(null, responses[callCount++]);
});

const plugin = constructPlugin({});
const results = await plugin.getAWSPagedResults(
new aws.ApiGatewayV2(),
"getApiMappings",
"Items",
"NextToken",
"NextToken",
{ DomainName: "example.com"},
);
expect(results).to.deep.equal([ "a", "b", "c", "d", "e" ]);
AWS.restore();
});
});
});
3 changes: 0 additions & 3 deletions test/unit-tests/tslint.json

This file was deleted.

0 comments on commit 36bd088

Please sign in to comment.