Skip to content

Commit

Permalink
Add helper for fetching all API pages
Browse files Browse the repository at this point in the history
  • Loading branch information
kumar303 committed Jan 9, 2018
1 parent b484ac3 commit 43daf30
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 124 deletions.
84 changes: 34 additions & 50 deletions src/amo/api/collections.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
/* @flow */
/* eslint-disable no-await-in-loop */
import { oneLine } from 'common-tags';

import { callApi } from 'core/api';
import log from 'core/logger';
import { callApi, allPages } from 'core/api';
import type {
ExternalCollectionAddon, ExternalCollectionDetail,
} from 'amo/reducers/collections';
Expand Down Expand Up @@ -64,30 +60,25 @@ export const getCollectionAddons = (
return callApi(request);
};

type GetAllCollectionAddonsParams = {|
...GetCollectionParams,
_allPages?: typeof allPages,
_getCollectionAddons?: typeof getCollectionAddons,
|};

export const getAllCollectionAddons = async (
{ api, slug, user }: GetCollectionParams
{
api,
slug,
user,
_allPages = allPages,
_getCollectionAddons = getCollectionAddons,
}: GetAllCollectionAddonsParams
): Promise<Array<ExternalCollectionAddon>> => {
let allResults = [];
let done = false;
let nextURL;

// TODO: make a helper function for this pattern.
while (!done) {
const response = await getCollectionAddons({
api, nextURL, slug, user,
});
allResults = allResults.concat(response.results);

if (response.next) {
nextURL = response.next;
log.debug(oneLine`Fetching next page "${nextURL}" of
getCollectionAddons for "${user}/${slug}"`);
} else {
done = true;
}
}

return allResults;
const { results } = await _allPages(
(nextURL) => _getCollectionAddons({ api, nextURL, slug, user })
);
return results;
};

type ListCollectionsParams = {|
Expand All @@ -107,27 +98,24 @@ export const listCollections = (
return callApi({ auth: true, endpoint, state: api });
};

type GetAllUserCollectionsParams = {|
...ListCollectionsParams,
_allPages?: typeof allPages,
_listCollections?: typeof listCollections,
|};

export const getAllUserCollections = async (
{ api, user }: ListCollectionsParams
{
api,
user,
_allPages = allPages,
_listCollections = listCollections,
}: GetAllUserCollectionsParams
): Promise<Array<ExternalCollectionDetail>> => {
let allResults = [];
let done = false;
let nextURL;

while (!done) {
const response = await listCollections({ api, user, nextURL });
allResults = allResults.concat(response.results);

if (response.next) {
nextURL = response.next;
log.debug(oneLine`Fetching next page "${nextURL}" of
listCollections for user "${user}"`);
} else {
done = true;
}
}

return allResults;
const { results } = await _allPages(
(nextURL) => _listCollections({ api, nextURL, user })
);
return results;
};

type GetAllUserAddonCollections = {|
Expand All @@ -147,10 +135,6 @@ export const getAllUserAddonCollections = async (
_getAllUserCollections = getAllUserCollections,
}: GetAllUserAddonCollections
): Promise<Array<ExternalCollectionDetail>> => {
// TODO: ultimately, we should query a single API endpoint to
// fetch all user collections that an add-on belongs to.
// https://github.com/mozilla/addons-server/issues/7167

// Fetch all collections belonging to the user.
const collectionResults = await _getAllUserCollections({ api, user });

Expand Down
8 changes: 5 additions & 3 deletions src/amo/reducers/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,15 @@ export const fetchCurrentCollectionPage = ({
};
};

type ExternalCollectionAddons = Array<{|
export type ExternalCollectionAddon = {|
addon: ExternalAddonType,
downloads: number,
notes: string | null,
|}>;
|};

type ExternalCollectionAddons = Array<ExternalCollectionAddon>;

type ExternalCollectionDetail = {|
export type ExternalCollectionDetail = {|
addon_count: number,
author: {|
id: number,
Expand Down
40 changes: 40 additions & 0 deletions src/core/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from 'core/searchUtils';
import type { ErrorHandlerType } from 'core/errorHandler';
import type { ApiStateType } from 'core/reducers/api';
import type { PaginatedApiResponse } from 'core/types/api';
import type { ReactRouterLocation } from 'core/types/router';


Expand Down Expand Up @@ -265,3 +266,42 @@ export function autocomplete({ api, filters }: AutocompleteParams) {
state: api,
});
}

type GetNextResponseType =
(nextURL?: string) => Promise<PaginatedApiResponse<any>>;

type AllPagesOptions = {| pageLimit: number |};

export const allPages = async (
getNextResponse: GetNextResponseType,
{ pageLimit = 100 }: AllPagesOptions = {},
): Promise<PaginatedApiResponse<any>> => {
let results = [];
let nextURL;
let count = 0;
let pageSize = 0;

for (let page = 1; page <= pageLimit; page++) {
// eslint-disable-next-line no-await-in-loop
const response = await getNextResponse(nextURL);
if (!count) {
// Every response page returns a count for all results.
count = response.count;
}
if (!pageSize) {
pageSize = response.page_size;
}
results = results.concat(response.results);

if (response.next) {
nextURL = response.next;
log.debug(oneLine`Fetching next page "${nextURL}" of
${getNextResponse}`);
} else {
return { count, page_size: pageSize, results };
}
}

// If we get this far the callback may not be advancing pages correctly.
throw new Error(`Fetched too many pages (the limit is ${pageLimit})`);
};
3 changes: 1 addition & 2 deletions src/core/client/base.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* global document */

import 'babel-polyfill';
import 'raf/polyfill';
import 'core/polyfill';
import { oneLine } from 'common-tags';
import config from 'config';
import FastClick from 'fastclick';
Expand Down
2 changes: 2 additions & 0 deletions src/core/polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import 'babel-polyfill';
import 'raf/polyfill';
3 changes: 1 addition & 2 deletions src/core/server/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import fs from 'fs';
import path from 'path';
import https from 'https';

import 'babel-polyfill';
import 'raf/polyfill';
import 'core/polyfill';
import { oneLine } from 'common-tags';
import defaultConfig from 'config';
import Express from 'express';
Expand Down
103 changes: 36 additions & 67 deletions tests/unit/amo/api/test_collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,48 +119,28 @@ describe(__filename, () => {
it('gets all pages of the collection add-ons list API', async () => {
const user = 'example-username';
const slug = 'example-collection-slug';
const addonResults = createFakeCollectionAddons().results;

const firstAddonSet = createFakeCollectionAddons({
addons: [{ ...fakeAddon, id: 1 }],
});
const secondAddonSet = createFakeCollectionAddons({
addons: [{ ...fakeAddon, id: 2 }],
});
const _getCollectionAddons = sinon.spy(
() => Promise.resolve(apiResponsePage({ results: addonResults }))
);

let page = 0;
const endpoint =
`accounts/account/${user}/collections/${slug}/addons`;
mockApi
.expects('callApi')
.withArgs({
auth: true,
endpoint: sinon.match(endpoint),
params: sinon.match.any,
state: apiState,
})
.twice()
.callsFake((request) => {
page += 1;
let next;
let results = [];
if (page === 1) {
next = `${endpoint}?page=2`;
results = firstAddonSet.results;
} else {
results = secondAddonSet.results;
// When we pass a next URL, it will include ?page=... so it
// is important that the page parameter is not sent.
expect(request.params).toEqual(undefined);
}
return Promise.resolve(apiResponsePage({ next, results }));
});
const nextURL = 'the-endpoint?page=2';
const _allPages = sinon.spy((nextPage) => nextPage(nextURL));

const addons = await getAllCollectionAddons({
api: apiState, user, slug,
api: apiState,
user,
slug,
_allPages,
_getCollectionAddons,
});

expect(addons).toEqual(addonResults);
sinon.assert.called(_getCollectionAddons);
expect(_getCollectionAddons.firstCall.args[0]).toEqual({
api: apiState, user, slug, nextURL,
});
expect(addons)
.toEqual(firstAddonSet.results.concat(secondAddonSet.results));
mockApi.verify();
});
});

Expand Down Expand Up @@ -285,39 +265,28 @@ describe(__filename, () => {
it('returns collections from multiple pages', async () => {
const user = 'some-user-id';

const firstCollection = createFakeCollectionDetail({
slug: 'first',
});
const secondCollection = createFakeCollectionDetail({
slug: 'second',
});
const collectionResults = [
createFakeCollectionDetail({ slug: 'first' }),
];

let page = 0;
const endpoint = `accounts/account/${user}/collections`;
mockApi
.expects('callApi')
.withArgs({
auth: true, endpoint: sinon.match(endpoint), state: apiState,
})
.twice()
.callsFake(() => {
page += 1;
let next;
const results = [];
if (page === 1) {
next = `${endpoint}?page=2`;
results.push(firstCollection);
} else {
results.push(secondCollection);
}
return Promise.resolve(apiResponsePage({ next, results }));
});
const _listCollections = sinon.spy(
() => Promise.resolve(
apiResponsePage({ results: collectionResults })
)
);

const nextURL = 'the-endpoint?page=2';
const _allPages = sinon.spy((nextPage) => nextPage(nextURL));

const results = await getAllUserCollections({
user, api: apiState,
const collections = await getAllUserCollections({
api: apiState, user, _allPages, _listCollections,
});

expect(collections).toEqual(collectionResults);
sinon.assert.called(_listCollections);
expect(_listCollections.firstCall.args[0]).toEqual({
api: apiState, user, nextURL,
});
expect(results).toEqual([firstCollection, secondCollection]);
mockApi.verify();
});
});

Expand Down
Loading

0 comments on commit 43daf30

Please sign in to comment.