diff --git a/lib/util/http/bitbucket.spec.ts b/lib/util/http/bitbucket.spec.ts index ac7bb06e010322..7f74909d1c8175 100644 --- a/lib/util/http/bitbucket.spec.ts +++ b/lib/util/http/bitbucket.spec.ts @@ -55,4 +55,34 @@ describe('util/http/bitbucket', () => { statusCode: 200, }); }); + + it('paginates', async () => { + httpMock + .scope(baseUrl) + .get('/some-url') + .reply(200, { + values: ['a'], + page: '1', + next: `${baseUrl}/some-url?page=2`, + }) + .get('/some-url?page=2') + .reply(200, { + values: ['b', 'c'], + page: '2', + next: `${baseUrl}/some-url?page=3`, + }) + .get('/some-url?page=3') + .reply(200, { + values: ['d'], + page: '3', + }); + const res = await api.getJson('some-url', { paginate: true }); + expect(res.body).toEqual({ + page: '1', + pagelen: 4, + size: 4, + values: ['a', 'b', 'c', 'd'], + next: undefined, + }); + }); }); diff --git a/lib/util/http/bitbucket.ts b/lib/util/http/bitbucket.ts index 628ef154cba1fe..15bea99b04860a 100644 --- a/lib/util/http/bitbucket.ts +++ b/lib/util/http/bitbucket.ts @@ -1,4 +1,7 @@ -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; +import is from '@sindresorhus/is'; +import type { PagedResult } from '../../modules/platform/bitbucket/types'; +import { parseUrl, resolveBaseUrl } from '../url'; +import type { HttpOptions, HttpResponse } from './types'; import { Http } from '.'; let baseUrl = 'https://api.bitbucket.org/'; @@ -7,16 +10,79 @@ export const setBaseUrl = (url: string): void => { baseUrl = url; }; -export class BitbucketHttp extends Http { - constructor(type = 'bitbucket', options?: HttpOptions) { +export interface BitbucketHttpOptions extends HttpOptions { + paginate?: boolean; +} + +export class BitbucketHttp extends Http { + constructor(type = 'bitbucket', options?: BitbucketHttpOptions) { super(type, options); } - protected override request( - url: string | URL, - options?: InternalHttpOptions + protected override async request( + path: string, + options?: BitbucketHttpOptions ): Promise> { const opts = { baseUrl, ...options }; - return super.request(url, opts); + + const result = await super.request(path, opts); + + if (opts.paginate && isPagedResult(result.body)) { + const resultBody = result.body as PagedResult; + + let nextPage = getPageFromURL(resultBody.next); + + while (is.nonEmptyString(nextPage)) { + const nextPath = getNextPagePath(path, nextPage); + + // istanbul ignore if + if (is.nullOrUndefined(nextPath)) { + break; + } + + const nextResult = await super.request>( + nextPath, + options + ); + + resultBody.values.push(...nextResult.body.values); + + nextPage = getPageFromURL(nextResult.body?.next); + } + + // Override other page-related attributes + resultBody.pagelen = resultBody.values.length; + resultBody.size = resultBody.values.length; + resultBody.next = undefined; + } + + 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 isPagedResult(obj: any): obj is PagedResult { + return is.nonEmptyObject(obj) && Array.isArray(obj.values); +}