"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GithubGraphqlDatasourceHelper = void 0;
const tslib_1 = require("tslib");
const aggregate_error_1 = tslib_1.__importDefault(require("aggregate-error"));
const logger_1 = require("../../../logger");
const external_host_error_1 = require("../../../types/errors/external-host-error");
const memCache = tslib_1.__importStar(require("../../cache/memory"));
const url_1 = require("../url");
/**
 * We know empirically that certain type of GraphQL errors
 * can be fixed by shrinking page size.
 *
 * @see https://github.com/renovatebot/renovate/issues/16343
 */
function isUnknownGraphqlError(err) {
    const { message } = err;
    return message.startsWith('Something went wrong while executing your query.');
}
function canBeSolvedByShrinking(err) {
    const errors = err instanceof aggregate_error_1.default ? [...err] : [err];
    return errors.some((e) => err instanceof external_host_error_1.ExternalHostError || isUnknownGraphqlError(e));
}
class GithubGraphqlDatasourceHelper {
    constructor(packageConfig, http, datasourceAdapter) {
        this.http = http;
        this.datasourceAdapter = datasourceAdapter;
        this.itemsPerQuery = 100;
        this.queryCount = 0;
        this.cursor = null;
        const { packageName, registryUrl } = packageConfig;
        [this.repoOwner, this.repoName] = packageName.split('/');
        this.baseUrl = (0, url_1.getApiBaseUrl)(registryUrl).replace(/\/v3\/$/, '/'); // Replace for GHE
    }
    static prepareQuery(payloadQuery) {
        return `
      query($owner: String!, $name: String!, $cursor: String, $count: Int!) {
        repository(owner: $owner, name: $name) {
          isRepoPrivate: isPrivate
          payload: ${payloadQuery}
        }
      }
    `;
    }
    static async query(config, http, adapter) {
        const instance = new GithubGraphqlDatasourceHelper(config, http, adapter);
        const items = await instance.getItems();
        return items;
    }
    getFingerprint() {
        return [
            this.baseUrl,
            this.repoOwner,
            this.repoName,
            this.datasourceAdapter.key,
        ].join(':');
    }
    getRawQueryOptions() {
        const baseUrl = this.baseUrl;
        const repository = `${this.repoOwner}/${this.repoName}`;
        const query = this.datasourceAdapter.query;
        const variables = {
            owner: this.repoOwner,
            name: this.repoName,
            count: this.itemsPerQuery,
            cursor: this.cursor,
        };
        return {
            baseUrl,
            repository,
            body: { query, variables },
        };
    }
    async doRawQuery() {
        const requestOptions = this.getRawQueryOptions();
        let httpRes;
        try {
            httpRes = await this.http.postJson('/graphql', requestOptions);
        }
        catch (err) {
            return [null, err];
        }
        const { body } = httpRes;
        const { data, errors } = body;
        if (errors?.length) {
            if (errors.length === 1) {
                const { message } = errors[0];
                const err = new Error(message);
                return [null, err];
            }
            else {
                const errorInstances = errors.map(({ message }) => new Error(message));
                const err = new aggregate_error_1.default(errorInstances);
                return [null, err];
            }
        }
        if (!data) {
            const msg = 'GitHub GraphQL datasource: failed to obtain data';
            const err = new Error(msg);
            return [null, err];
        }
        if (!data.repository) {
            const msg = 'GitHub GraphQL datasource: failed to obtain repository data';
            const err = new Error(msg);
            return [null, err];
        }
        if (!data.repository.payload) {
            const msg = 'GitHub GraphQL datasource: failed to obtain repository payload data';
            const err = new Error(msg);
            return [null, err];
        }
        this.queryCount += 1;
        const isRepoPrivate = data.repository.isRepoPrivate;
        const res = { ...data.repository.payload, isRepoPrivate };
        return [res, null];
    }
    shrinkPageSize() {
        if (this.itemsPerQuery === 100) {
            this.itemsPerQuery = 50;
            return true;
        }
        if (this.itemsPerQuery === 50) {
            this.itemsPerQuery = 25;
            return true;
        }
        return false;
    }
    hasReachedQueryLimit() {
        return this.queryCount >= 100;
    }
    async doShrinkableQuery() {
        let res = null;
        let err = null;
        while (!res) {
            [res, err] = await this.doRawQuery();
            if (err) {
                if (!canBeSolvedByShrinking(err)) {
                    throw err;
                }
                const shrinkResult = this.shrinkPageSize();
                if (!shrinkResult) {
                    throw err;
                }
                const { body, ...options } = this.getRawQueryOptions();
                logger_1.logger.debug({ options, newSize: this.itemsPerQuery }, 'Shrinking GitHub GraphQL page size after error');
            }
        }
        return res;
    }
    async doPaginatedQuery() {
        const resultItems = [];
        let hasNextPage = true;
        let cursor;
        while (hasNextPage && !this.hasReachedQueryLimit()) {
            const queryResult = await this.doShrinkableQuery();
            const pageResultItems = queryResult.nodes
                .map((item) => this.datasourceAdapter.transform(item))
                .filter((item) => item !== null);
            resultItems.push(...pageResultItems);
            hasNextPage = queryResult?.pageInfo?.hasNextPage;
            cursor = queryResult?.pageInfo?.endCursor;
            if (hasNextPage && cursor) {
                this.cursor = cursor;
            }
        }
        return resultItems;
    }
    /**
     * This method intentionally was made not async, though it returns `Promise`.
     *
     * It helps us to avoid potential race conditions during concurrent fetching
     * of the same package releases.
     */
    doConcurrentQuery() {
        const packageFingerprint = this.getFingerprint();
        const cacheKey = `github-datasource-promises:${packageFingerprint}`;
        const resultPromise = memCache.get(cacheKey) ?? this.doPaginatedQuery();
        memCache.set(cacheKey, resultPromise);
        return resultPromise;
    }
    async getItems() {
        const res = await this.doConcurrentQuery();
        return res;
    }
}
exports.GithubGraphqlDatasourceHelper = GithubGraphqlDatasourceHelper;
//# sourceMappingURL=datasource-helper.js.map