diff --git a/lambdas/backend/_common/customers-controller.js b/lambdas/backend/_common/customers-controller.js index 82f7509b8..7f2021e62 100644 --- a/lambdas/backend/_common/customers-controller.js +++ b/lambdas/backend/_common/customers-controller.js @@ -3,6 +3,7 @@ 'use strict' const AWS = require('aws-sdk') +const { getAllUsagePlans } = require('../shared/get-all-usage-plans') const dynamoDb = new AWS.DynamoDB.DocumentClient() const apigateway = new AWS.APIGateway() @@ -195,10 +196,9 @@ function getUsagePlansForCustomer(cognitoIdentityId, error, callback) { keyId, limit: 1000 } - apigateway.getUsagePlans(params, (err, usagePlansData) => { - if (err) error(err) - else callback(usagePlansData) - }) + getAllUsagePlans(apigateway, params) + .then(usagePlansData => callback({ items: usagePlansData })) + .catch(err => error(err)) } }) } @@ -209,26 +209,22 @@ function getUsagePlanForProductCode(productCode, error, callback) { // do a linear scan of usage plans for name matching productCode var params = { limit: 1000 - }; - apigateway.getUsagePlans(params, function(err, data) { - if (err) { - error(err) - } else { - console.log(`Got usage plans ${JSON.stringify(data.items)}`) + } + getAllUsagePlans(apigateway, params).then(usagePlans => { + console.log(`Got usage plans ${JSON.stringify(usagePlans)}`) - // note: ensure that only one usage plan maps to a given marketplace product code - const usageplan = data.items.find(function (item) { - return item.productCode !== undefined && item.productCode === productCode - }) - if (usageplan !== undefined) { - console.log(`Found usage plan matching ${productCode}`) - callback(usageplan) - } else { - console.log(`Couldn't find usageplan matching product code ${productCode}`) - error(`Couldn't find usageplan matching product code ${productCode}`) - } + // note: ensure that only one usage plan maps to a given marketplace product code + const usageplan = usagePlans.find(function (item) { + return item.productCode !== undefined && item.productCode === productCode + }) + if (usageplan !== undefined) { + console.log(`Found usage plan matching ${productCode}`) + callback(usageplan) + } else { + console.log(`Couldn't find usageplan matching product code ${productCode}`) + error(`Couldn't find usageplan matching product code ${productCode}`) } - }); + }).catch(err => error(err)) } function updateCustomerMarketplaceId(cognitoIdentityId, marketplaceCustomerId, error, success) { @@ -323,16 +319,6 @@ function updateCustomerApiKeyId(cognitoIdentityId, apiKeyId, error, success) { }) } -// function getUsagePlans(error, callback) { -// const params = { -// limit: 1000 -// } -// apigateway.getUsagePlans(params, (err, data) => { -// if (err) error(err) -// else callback(data) -// }) -// } - module.exports = { ensureCustomerItem, subscribe, diff --git a/lambdas/backend/express-route-handlers.js b/lambdas/backend/express-route-handlers.js index f989c6e70..f65c33988 100644 --- a/lambdas/backend/express-route-handlers.js +++ b/lambdas/backend/express-route-handlers.js @@ -3,6 +3,7 @@ const feedbackController = require('./_common/feedback-controller.js') const AWS = require('aws-sdk') const catalog = require('./catalog/index') const hash = require('object-hash') +const { getAllUsagePlans } = require('./shared/get-all-usage-plans') const Datauri = require('datauri') @@ -421,7 +422,7 @@ async function getAdminCatalogVisibility(req, res) { }) }) - let usagePlans = await exports.apigateway.getUsagePlans().promise() + let usagePlans = await getAllUsagePlans(exports.apigateway) // In the case of apiGateway APIs, the client doesn't know if there are usage plan associated or not // so we need to provide that information. This can't be merged with the above loop: @@ -431,7 +432,7 @@ async function getAdminCatalogVisibility(req, res) { visibility.apiGateway.map((apiEntry) => { apiEntry.subscribable = false - usagePlans.items.forEach((usagePlan) => { + usagePlans.forEach((usagePlan) => { usagePlan.apiStages.forEach((apiStage) => { if(apiEntry.id === apiStage.apiId && apiEntry.stage === apiStage.stage) { apiEntry.subscribable = true diff --git a/lambdas/backend/shared/get-all-usage-plans.js b/lambdas/backend/shared/get-all-usage-plans.js new file mode 100644 index 000000000..c27dbebaf --- /dev/null +++ b/lambdas/backend/shared/get-all-usage-plans.js @@ -0,0 +1,33 @@ +/** + * Fetches all usage plans, combining all pages into a single array. + * + * @param apiGateway + * an instance of `AWS.APIGateway` to use for API calls + * + * @param paramOverrides + * a parameter object passed in calls to `APIGateway.getUsagePlans` + * + * @returns + * a Promise resolving with an array of items returned from + * `APIGateway.getUsagePlans` calls + */ +async function getAllUsagePlans(apiGateway, paramOverrides = {}) { + // The maximum allowed value of `limit` is 500 according to + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getUsagePlans-property + const defaultParams = {limit: 500, ...paramOverrides} + + console.log('Fetching first page of usage plans') + let response = await apiGateway.getUsagePlans(defaultParams).promise() + const usagePlans = response.items + + while (response.position) { + console.log(`Fetching next page of usage plans, at position=[${response.position}]`) + const nextParams = {...defaultParams, position: response.position} + response = await apiGateway.getUsagePlans(nextParams).promise() + usagePlans.push(...response.items) + } + + return usagePlans +} + +module.exports = { getAllUsagePlans } diff --git a/lambdas/catalog-updater/index.js b/lambdas/catalog-updater/index.js index b30b2c221..068066c90 100644 --- a/lambdas/catalog-updater/index.js +++ b/lambdas/catalog-updater/index.js @@ -12,6 +12,8 @@ let AWS = require('aws-sdk'), bucketName = '', hash = require('object-hash') +const { getAllUsagePlans } = require('./shared/get-all-usage-plans') + /** * Takes in an s3 listObjectsV2 object and returns whether it's a "swagger file" (one ending in .JSON, .YAML, or .YML), * and whether it's in the catalog folder (S3 Key starts with "catalog/"). @@ -185,10 +187,9 @@ function buildCatalog(swaggerFiles, sdkGeneration) { generic: [] } - return exports.gateway.getUsagePlans({}).promise() - .then((result) => { - console.log(`usagePlans: ${JSON.stringify(result.items, null, 4)}`) - let usagePlans = result.items + return getAllUsagePlans(exports.gateway) + .then(usagePlans => { + console.log(`usagePlans: ${JSON.stringify(usagePlans, null, 4)}`) for (let i = 0; i < usagePlans.length; i++) { catalog.apiGateway[i] = usagePlanToCatalogObject(usagePlans[i], swaggerFiles, sdkGeneration) } @@ -252,4 +253,4 @@ exports = module.exports = { gateway: new AWS.APIGateway(), handler, hash -} \ No newline at end of file +} diff --git a/lambdas/catalog-updater/shared/get-all-usage-plans.js b/lambdas/catalog-updater/shared/get-all-usage-plans.js new file mode 100644 index 000000000..c27dbebaf --- /dev/null +++ b/lambdas/catalog-updater/shared/get-all-usage-plans.js @@ -0,0 +1,33 @@ +/** + * Fetches all usage plans, combining all pages into a single array. + * + * @param apiGateway + * an instance of `AWS.APIGateway` to use for API calls + * + * @param paramOverrides + * a parameter object passed in calls to `APIGateway.getUsagePlans` + * + * @returns + * a Promise resolving with an array of items returned from + * `APIGateway.getUsagePlans` calls + */ +async function getAllUsagePlans(apiGateway, paramOverrides = {}) { + // The maximum allowed value of `limit` is 500 according to + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getUsagePlans-property + const defaultParams = {limit: 500, ...paramOverrides} + + console.log('Fetching first page of usage plans') + let response = await apiGateway.getUsagePlans(defaultParams).promise() + const usagePlans = response.items + + while (response.position) { + console.log(`Fetching next page of usage plans, at position=[${response.position}]`) + const nextParams = {...defaultParams, position: response.position} + response = await apiGateway.getUsagePlans(nextParams).promise() + usagePlans.push(...response.items) + } + + return usagePlans +} + +module.exports = { getAllUsagePlans } diff --git a/lambdas/shared/__tests__/get-all-usage-plans-test.js b/lambdas/shared/__tests__/get-all-usage-plans-test.js new file mode 100644 index 000000000..77ac7c16a --- /dev/null +++ b/lambdas/shared/__tests__/get-all-usage-plans-test.js @@ -0,0 +1,106 @@ +const { getAllUsagePlans } = require('../get-all-usage-plans') + +const promiser = require('../../setup-jest').promiser + +const mockUsagePlanItem = () => ({ + id: '1a2b3c', + name: '1a2b3c', + apiStages: [ + { + apiId: 'anmlcrckrs', + stage: 'prod', + }, + { + apiId: 'jlpnochips', + stage: 'beta', + }, + ], + throttle: { + burstLimit: 10, + rateLimit: 10, + }, + quota: { + limit: 10000, + offset: 0, + period: 'DAY', + } +}) + +describe('getAllUsagePlans', () => { + test('returns all usage plans, when none exist', async () => { + const mockApiGateway = { + getUsagePlans: jest.fn().mockReturnValueOnce(promiser({ + items: [] + })) + } + + const result = await getAllUsagePlans(mockApiGateway) + const mocked = mockApiGateway.getUsagePlans.mock + expect(mocked.calls.length).toBe(1) + expect(mocked.calls[0][0]).not.toHaveProperty('position') + expect(result).toHaveLength(0) + }) + + test('returns all usage plans, when only one page of usage plans exists', async () => { + const mockApiGateway = { + getUsagePlans: jest.fn().mockReturnValueOnce(promiser({ + items: [ + mockUsagePlanItem(), + mockUsagePlanItem(), + mockUsagePlanItem(), + mockUsagePlanItem(), + ] + })) + } + + const result = await getAllUsagePlans(mockApiGateway) + const mocked = mockApiGateway.getUsagePlans.mock + expect(mocked.calls.length).toBe(1) + expect(mocked.calls[0][0]).not.toHaveProperty('position') + expect(result).toHaveLength(4) + }) + + test('returns all usage plans, when multiple pages of usage plans exist', async () => { + const mockApiGateway = { + getUsagePlans: jest.fn().mockReturnValueOnce(promiser({ + items: [ + mockUsagePlanItem(), + mockUsagePlanItem(), + mockUsagePlanItem(), + mockUsagePlanItem(), + ], + position: 'qwertyuiopasdf%3D%3D', + })).mockReturnValueOnce(promiser({ + items: [ + mockUsagePlanItem(), + mockUsagePlanItem(), + mockUsagePlanItem(), + mockUsagePlanItem(), + ], + position: 'zxcvbnm1234567%3D%3D', + })).mockReturnValueOnce(promiser({ + items: [ + mockUsagePlanItem(), + mockUsagePlanItem(), + ], + })) + } + + const result = await getAllUsagePlans(mockApiGateway) + const mocked = mockApiGateway.getUsagePlans.mock + expect(mocked.calls.length).toBe(3) + expect(mocked.calls[0][0]).not.toHaveProperty('position') + expect(mocked.calls[1][0]).toHaveProperty('position', 'qwertyuiopasdf%3D%3D') + expect(mocked.calls[2][0]).toHaveProperty('position', 'zxcvbnm1234567%3D%3D') + expect(result).toHaveLength(10) + }) + + test('passes through an API Gateway request error', async () => { + const expectedError = {} + const mockApiGateway = { + getUsagePlans: jest.fn().mockReturnValueOnce(promiser(null, expectedError)) + } + + await expect(getAllUsagePlans(mockApiGateway)).rejects.toStrictEqual(expectedError) + }) +}) diff --git a/lambdas/shared/get-all-usage-plans.js b/lambdas/shared/get-all-usage-plans.js new file mode 100644 index 000000000..c27dbebaf --- /dev/null +++ b/lambdas/shared/get-all-usage-plans.js @@ -0,0 +1,33 @@ +/** + * Fetches all usage plans, combining all pages into a single array. + * + * @param apiGateway + * an instance of `AWS.APIGateway` to use for API calls + * + * @param paramOverrides + * a parameter object passed in calls to `APIGateway.getUsagePlans` + * + * @returns + * a Promise resolving with an array of items returned from + * `APIGateway.getUsagePlans` calls + */ +async function getAllUsagePlans(apiGateway, paramOverrides = {}) { + // The maximum allowed value of `limit` is 500 according to + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getUsagePlans-property + const defaultParams = {limit: 500, ...paramOverrides} + + console.log('Fetching first page of usage plans') + let response = await apiGateway.getUsagePlans(defaultParams).promise() + const usagePlans = response.items + + while (response.position) { + console.log(`Fetching next page of usage plans, at position=[${response.position}]`) + const nextParams = {...defaultParams, position: response.position} + response = await apiGateway.getUsagePlans(nextParams).promise() + usagePlans.push(...response.items) + } + + return usagePlans +} + +module.exports = { getAllUsagePlans }