Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(bitbucket): add support for pagelen (#22278)
  • Loading branch information
setchy committed May 20, 2023
1 parent 7a1d242 commit 06f7134
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 48 deletions.
24 changes: 18 additions & 6 deletions lib/modules/platform/bitbucket/index.spec.ts
Expand Up @@ -813,7 +813,9 @@ describe('modules/platform/bitbucket/index', () => {
};
const scope = await initRepoMock();
scope
.get('/2.0/repositories/some/repo/effective-default-reviewers')
.get(
'/2.0/repositories/some/repo/effective-default-reviewers?pagelen=100'
)
.reply(200, {
values: [projectReviewer, repoReviewer],
})
Expand Down Expand Up @@ -855,7 +857,9 @@ describe('modules/platform/bitbucket/index', () => {
};
const scope = await initRepoMock();
scope
.get('/2.0/repositories/some/repo/effective-default-reviewers')
.get(
'/2.0/repositories/some/repo/effective-default-reviewers?pagelen=100'
)
.reply(200, {
values: [
activeReviewerWithinWorkspace,
Expand Down Expand Up @@ -924,7 +928,9 @@ describe('modules/platform/bitbucket/index', () => {
};
const scope = await initRepoMock();
scope
.get('/2.0/repositories/some/repo/effective-default-reviewers')
.get(
'/2.0/repositories/some/repo/effective-default-reviewers?pagelen=100'
)
.reply(200, {
values: [memberReviewer, notMemberReviewer],
})
Expand Down Expand Up @@ -973,7 +979,9 @@ describe('modules/platform/bitbucket/index', () => {
};
const scope = await initRepoMock();
scope
.get('/2.0/repositories/some/repo/effective-default-reviewers')
.get(
'/2.0/repositories/some/repo/effective-default-reviewers?pagelen=100'
)
.reply(200, {
values: [reviewer],
})
Expand Down Expand Up @@ -1017,7 +1025,9 @@ describe('modules/platform/bitbucket/index', () => {

const scope = await initRepoMock();
scope
.get('/2.0/repositories/some/repo/effective-default-reviewers')
.get(
'/2.0/repositories/some/repo/effective-default-reviewers?pagelen=100'
)
.reply(200, {
values: [reviewer],
})
Expand Down Expand Up @@ -1054,7 +1064,9 @@ describe('modules/platform/bitbucket/index', () => {

const scope = await initRepoMock();
scope
.get('/2.0/repositories/some/repo/effective-default-reviewers')
.get(
'/2.0/repositories/some/repo/effective-default-reviewers?pagelen=100'
)
.reply(200, {
values: [reviewer],
})
Expand Down
77 changes: 70 additions & 7 deletions lib/util/http/bitbucket.spec.ts
Expand Up @@ -56,27 +56,90 @@ describe('util/http/bitbucket', () => {
});
});

it('paginates', async () => {
it('paginates: adds default pagelen if non is present', async () => {
httpMock
.scope(baseUrl)
.get('/some-url')
.get('/some-url?foo=bar&pagelen=100')
.reply(200, {
values: ['a'],
page: '1',
next: `${baseUrl}/some-url?page=2`,
next: `${baseUrl}/some-url?foo=bar&pagelen=100&page=2`,
})
.get('/some-url?page=2')
.get('/some-url?foo=bar&pagelen=100&page=2')
.reply(200, {
values: ['b', 'c'],
page: '2',
next: `${baseUrl}/some-url?page=3`,
next: `${baseUrl}/some-url?foo=bar&pagelen=100&page=3`,
})
.get('/some-url?page=3')
.get('/some-url?foo=bar&pagelen=100&page=3')
.reply(200, {
values: ['d'],
page: '3',
});
const res = await api.getJson('some-url', { paginate: true });
const res = await api.getJson('/some-url?foo=bar', { paginate: true });
expect(res.body).toEqual({
page: '1',
pagelen: 4,
size: 4,
values: ['a', 'b', 'c', 'd'],
next: undefined,
});
});

it('paginates: respects pagelen if already set in path', async () => {
httpMock
.scope(baseUrl)
.get('/some-url?pagelen=10')
.reply(200, {
values: ['a'],
page: '1',
next: `${baseUrl}/some-url?pagelen=10&page=2`,
})
.get('/some-url?pagelen=10&page=2')
.reply(200, {
values: ['b', 'c'],
page: '2',
next: `${baseUrl}/some-url?pagelen=10&page=3`,
})
.get('/some-url?pagelen=10&page=3')
.reply(200, {
values: ['d'],
page: '3',
});
const res = await api.getJson('some-url?pagelen=10', { paginate: true });
expect(res.body).toEqual({
page: '1',
pagelen: 4,
size: 4,
values: ['a', 'b', 'c', 'd'],
next: undefined,
});
});

it('paginates: respects pagelen if set in options', async () => {
httpMock
.scope(baseUrl)
.get('/some-url?pagelen=20')
.reply(200, {
values: ['a'],
page: '1',
next: `${baseUrl}/some-url?pagelen=20&page=2`,
})
.get('/some-url?pagelen=20&page=2')
.reply(200, {
values: ['b', 'c'],
page: '2',
next: `${baseUrl}/some-url?pagelen=20&page=3`,
})
.get('/some-url?pagelen=20&page=3')
.reply(200, {
values: ['d'],
page: '3',
});
const res = await api.getJson('some-url', {
paginate: true,
pagelen: 20,
});
expect(res.body).toEqual({
page: '1',
pagelen: 4,
Expand Down
63 changes: 28 additions & 35 deletions lib/util/http/bitbucket.ts
@@ -1,9 +1,13 @@
import is from '@sindresorhus/is';
import { logger } from '../../logger';
import type { PagedResult } from '../../modules/platform/bitbucket/types';
import { parseUrl, resolveBaseUrl } from '../url';
import type { HttpOptions, HttpResponse } from './types';
import { Http } from '.';

const MAX_PAGES = 100;
const MAX_PAGELEN = 100;

let baseUrl = 'https://api.bitbucket.org/';

export const setBaseUrl = (url: string): void => {
Expand All @@ -12,6 +16,7 @@ export const setBaseUrl = (url: string): void => {

export interface BitbucketHttpOptions extends HttpOptions {
paginate?: boolean;
pagelen?: number;
}

export class BitbucketHttp extends Http<BitbucketHttpOptions> {
Expand All @@ -25,62 +30,50 @@ export class BitbucketHttp extends Http<BitbucketHttpOptions> {
): Promise<HttpResponse<T>> {
const opts = { baseUrl, ...options };

const result = await super.request<T>(path, opts);
const resolvedURL = parseUrl(resolveBaseUrl(baseUrl, path));

if (opts.paginate && isPagedResult(result.body)) {
const resultBody = result.body as PagedResult<T>;
// istanbul ignore if: this should never happen
if (is.nullOrUndefined(resolvedURL)) {
logger.error(`Bitbucket: cannot parse path ${path}`);
throw new Error(`Bitbucket: cannot parse path ${path}`);
}

let nextPage = getPageFromURL(resultBody.next);
if (opts.paginate && !hasPagelen(resolvedURL)) {
const pagelen = opts.pagelen ?? MAX_PAGELEN;
resolvedURL.searchParams.set('pagelen', pagelen.toString());
}

while (is.nonEmptyString(nextPage)) {
const nextPath = getNextPagePath(path, nextPage);
const result = await super.request<T>(resolvedURL.toString(), opts);

// istanbul ignore if
if (is.nullOrUndefined(nextPath)) {
break;
}
if (opts.paginate && isPagedResult(result.body)) {
const resultBody = result.body as PagedResult<T>;
let page = 1;
let nextURL = resultBody.next;

while (is.nonEmptyString(nextURL) && page <= MAX_PAGES) {
const nextResult = await super.request<PagedResult<T>>(
nextPath,
nextURL,
options
);

resultBody.values.push(...nextResult.body.values);

nextPage = getPageFromURL(nextResult.body?.next);
nextURL = nextResult.body?.next;
page += 1;
}

// Override other page-related attributes
resultBody.pagelen = resultBody.values.length;
resultBody.size = resultBody.values.length;
resultBody.next = undefined;
resultBody.size = page > MAX_PAGES ? undefined : resultBody.values.length;
resultBody.next = page > MAX_PAGES ? undefined : nextURL;
}

return result;
}
}

function getPageFromURL(url: string | undefined): string | null {
const resolvedURL = parseUrl(url);

if (is.nullOrUndefined(resolvedURL)) {
return null;
}

return resolvedURL.searchParams.get('page');
}

function getNextPagePath(path: string, nextPage: string): string | null {
const resolvedURL = parseUrl(resolveBaseUrl(baseUrl, path));

// istanbul ignore if
if (is.nullOrUndefined(resolvedURL)) {
return null;
}

resolvedURL.searchParams.set('page', nextPage);

return resolvedURL.toString();
function hasPagelen(url: URL): boolean {
return !is.nullOrUndefined(url.searchParams.get('pagelen'));
}

function isPagedResult(obj: any): obj is PagedResult {
Expand Down

0 comments on commit 06f7134

Please sign in to comment.