feat(aio): implement `GithubApi.getPaginated()`

This commit is contained in:
Georgios Kalpakas 2017-02-28 14:01:20 +02:00 committed by Chuck Jazdzewski
parent 37348989f0
commit 951e653b0c
4 changed files with 110 additions and 88 deletions

View File

@ -51,6 +51,23 @@ export class GithubApi {
return `${pathname}${joiner}${search}`; return `${pathname}${joiner}${search}`;
} }
protected getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
const perPage = 100;
const params = {
...baseParams,
page: currentPage,
per_page: perPage,
};
return this.get<T[]>(pathname, params).then(items => {
if (items.length < perPage) {
return items;
}
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
});
}
protected request<T>(method: string, path: string, data: any = null): Promise<T> { protected request<T>(method: string, path: string, data: any = null): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const options = { const options = {

View File

@ -22,30 +22,11 @@ export class GithubPullRequests extends GithubApi {
} }
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> { public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
process.stdout.write(`Fetching ${state} pull requests...`); console.log(`Fetching ${state} pull requests...`);
return this.fetchUntilDone(state, 0);
}
// Methods - Protected
protected fetchUntilDone(state: PullRequestState, currentPage: number): Promise<PullRequest[]> {
process.stdout.write('.');
const perPage = 100;
const pathname = `/repos/${this.repoSlug}/pulls`; const pathname = `/repos/${this.repoSlug}/pulls`;
const params = { const params = {state};
page: currentPage,
per_page: perPage,
state,
};
return this.get<PullRequest[]>(pathname, params).then(pullRequests => { return this.getPaginated<PullRequest>(pathname, params);
if (pullRequests.length < perPage) {
console.log('done');
return pullRequests;
}
return this.fetchUntilDone(state, currentPage + 1).
then(morePullRequests => [...pullRequests, ...morePullRequests]);
});
} }
} }

View File

@ -148,6 +148,81 @@ describe('GithubApi', () => {
}); });
describe('getPaginated()', () => {
let deferreds: {resolve: Function, reject: Function}[];
beforeEach(() => {
deferreds = [];
spyOn(api, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
});
it('should return a promise', () => {
expect((api as any).getPaginated()).toEqual(jasmine.any(Promise));
});
it('should call \'get()\' with the correct pathname and params', () => {
(api as any).getPaginated('/foo/bar');
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
});
it('should reject if the request fails', done => {
(api as any).getPaginated('/foo/bar').catch(err => {
expect(err).toBe('Test');
done();
});
deferreds[0].reject('Test');
});
it('should resolve with the returned items', done => {
const items = [{id: 1}, {id: 2}];
(api as any).getPaginated('/foo/bar').then((data: any) => {
expect(data).toEqual(items);
done();
});
deferreds[0].resolve(items);
});
it('should iteratively call \'get()\' to fetch all items', done => {
// Create an array or 250 objects.
const allItems = '.'.repeat(250).split('').map((_, i) => ({id: i}));
const apiGetSpy = api.get as jasmine.Spy;
(api as any).getPaginated('/foo/bar', {baz: 'qux'}).then((data: any) => {
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
expect(apiGetSpy).toHaveBeenCalledTimes(3);
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
expect(data).toEqual(allItems);
done();
});
deferreds[0].resolve(allItems.slice(0, 100));
setTimeout(() => {
deferreds[1].resolve(allItems.slice(100, 200));
setTimeout(() => {
deferreds[2].resolve(allItems.slice(200));
}, 0);
}, 0);
});
});
describe('request()', () => { describe('request()', () => {
let httpsRequestSpy: jasmine.Spy; let httpsRequestSpy: jasmine.Spy;
let latestRequest: ClientRequest; let latestRequest: ClientRequest;

View File

@ -78,89 +78,38 @@ describe('GithubPullRequests', () => {
describe('fetchAll()', () => { describe('fetchAll()', () => {
let prs: GithubPullRequests; let prs: GithubPullRequests;
let deferreds: {resolve: Function, reject: Function}[]; let prsGetPaginatedSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
prs = new GithubPullRequests('foo/bar', '12345'); prs = new GithubPullRequests('foo/bar', '12345');
deferreds = []; prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
spyOn(console, 'log');
spyOn(process.stdout, 'write');
spyOn(prs, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
}); });
it('should return a promise', () => { it('should call \'getPaginated()\' with the correct pathname and params', () => {
expect(prs.fetchAll()).toEqual(jasmine.any(Promise)); const expectedPathname = '/repos/foo/bar/pulls';
});
prs.fetchAll('all');
it('should call \'get()\' with the correct pathname and params', () => { prs.fetchAll('closed');
prs.fetchAll('open'); prs.fetchAll('open');
expect(prs.get).toHaveBeenCalledWith('/repos/foo/bar/pulls', { expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3);
page: 0, expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
per_page: 100, expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
state: 'open', expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
});
}); });
it('should default to \'all\' if no state is specified', () => { it('should default to \'all\' if no state is specified', () => {
prs.fetchAll(); prs.fetchAll();
expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
expect(prs.get).toHaveBeenCalledWith('/repos/foo/bar/pulls', {
page: 0,
per_page: 100,
state: 'all',
});
}); });
it('should reject if the request fails', done => { it('should forward the value returned by \'getPaginated()\'', () => {
prs.fetchAll().catch(err => { prsGetPaginatedSpy.and.returnValue('Test');
expect(err).toBe('Test'); expect(prs.fetchAll()).toBe('Test');
done();
});
deferreds[0].reject('Test');
});
it('should resolve with the returned pull requests', done => {
const pullRequests = [{id: 1}, {id: 2}];
prs.fetchAll().then(data => {
expect(data).toEqual(pullRequests);
done();
});
deferreds[0].resolve(pullRequests);
});
it('should iteratively call \'get()\' to fetch all pull requests', done => {
// Create an array or 250 objects.
const allPullRequests = '.'.repeat(250).split('').map((_, i) => ({id: i}));
const prsGetApy = prs.get as jasmine.Spy;
prs.fetchAll().then(data => {
const paramsForPage = (page: number) => ({page, per_page: 100, state: 'all'});
expect(prsGetApy).toHaveBeenCalledTimes(3);
expect(prsGetApy.calls.argsFor(0)).toEqual(['/repos/foo/bar/pulls', paramsForPage(0)]);
expect(prsGetApy.calls.argsFor(1)).toEqual(['/repos/foo/bar/pulls', paramsForPage(1)]);
expect(prsGetApy.calls.argsFor(2)).toEqual(['/repos/foo/bar/pulls', paramsForPage(2)]);
expect(data).toEqual(allPullRequests);
done();
});
deferreds[0].resolve(allPullRequests.slice(0, 100));
setTimeout(() => {
deferreds[1].resolve(allPullRequests.slice(100, 200));
setTimeout(() => deferreds[2].resolve(allPullRequests.slice(200)), 0);
}, 0);
}); });
}); });