@gitbeaker/requester-utils
Version:
Utility functions for requester implementatons used in @gitbeaker
236 lines (228 loc) • 7.45 kB
JavaScript
var qs = require('qs');
var xcase = require('xcase');
var rateLimiterFlexible = require('rate-limiter-flexible');
var Picomatch = require('picomatch-browser');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var Picomatch__default = /*#__PURE__*/_interopDefault(Picomatch);
// src/RequesterUtils.ts
var { isMatch: isGlobMatch } = Picomatch__default.default;
function generateRateLimiterFn(limit, interval) {
const limiter = new rateLimiterFlexible.RateLimiterQueue(
new rateLimiterFlexible.RateLimiterMemory({ points: limit, duration: interval })
);
return () => limiter.removeTokens(1);
}
function formatQuery(params = {}) {
const decamelized = xcase.decamelizeKeys(params);
return qs.stringify(decamelized, { arrayFormat: "brackets" });
}
async function defaultOptionsHandler(resourceOptions, {
body,
searchParams,
sudo,
signal,
asStream = false,
method = "GET"
} = {}) {
const { headers: preconfiguredHeaders, authHeaders, url } = resourceOptions;
const defaultOptions = {
method,
asStream,
signal,
prefixUrl: url
};
defaultOptions.headers = { ...preconfiguredHeaders };
if (sudo) defaultOptions.headers.sudo = `${sudo}`;
if (body) {
if (body instanceof FormData) {
defaultOptions.body = body;
} else {
defaultOptions.body = JSON.stringify(xcase.decamelizeKeys(body));
defaultOptions.headers["content-type"] = "application/json";
}
}
if (Object.keys(authHeaders).length > 0) {
const [authHeaderKey, authHeaderFn] = Object.entries(authHeaders)[0];
defaultOptions.headers[authHeaderKey] = await authHeaderFn();
}
const q = formatQuery(searchParams);
if (q) defaultOptions.searchParams = q;
return Promise.resolve(defaultOptions);
}
function createRateLimiters(rateLimitOptions = {}, rateLimitDuration = 60) {
const rateLimiters = {};
Object.entries(rateLimitOptions).forEach(([key, config]) => {
if (typeof config === "number")
rateLimiters[key] = generateRateLimiterFn(config, rateLimitDuration);
else
rateLimiters[key] = {
method: config.method.toUpperCase(),
limit: generateRateLimiterFn(config.limit, rateLimitDuration)
};
});
return rateLimiters;
}
function createRequesterFn(optionsHandler, requestHandler) {
const methods = ["get", "post", "put", "patch", "delete"];
return (serviceOptions) => {
const requester = {};
const rateLimiters = createRateLimiters(
serviceOptions.rateLimits,
serviceOptions.rateLimitDuration
);
methods.forEach((m) => {
requester[m] = async (endpoint, options) => {
const defaultRequestOptions = await defaultOptionsHandler(serviceOptions, {
...options,
method: m.toUpperCase()
});
const requestOptions = await optionsHandler(serviceOptions, defaultRequestOptions);
return requestHandler(endpoint, { ...requestOptions, rateLimiters });
};
});
return requester;
};
}
function extendClass(Base, customConfig) {
return class extends Base {
constructor(...options) {
const [config, ...opts] = options;
super({ ...customConfig, ...config }, ...opts);
}
};
}
function presetResourceArguments(resources, customConfig = {}) {
const updated = {};
Object.entries(resources).filter(([, s]) => typeof s === "function").forEach(([k, r]) => {
updated[k] = extendClass(r, customConfig);
});
return updated;
}
function getMatchingRateLimiter(endpoint, rateLimiters = {}, method = "GET") {
const sortedEndpoints = Object.keys(rateLimiters).sort().reverse();
const match = sortedEndpoints.find((ep) => isGlobMatch(endpoint, ep));
const rateLimitConfig = match && rateLimiters[match];
if (typeof rateLimitConfig === "function") return rateLimitConfig;
if (rateLimitConfig && rateLimitConfig?.method?.toUpperCase() === method.toUpperCase()) {
return rateLimitConfig.limit;
}
return generateRateLimiterFn(3e3, 60);
}
// src/BaseResource.ts
function getDynamicToken(tokenArgument) {
return tokenArgument instanceof Function ? tokenArgument() : Promise.resolve(tokenArgument);
}
var DEFAULT_RATE_LIMITS = Object.freeze({
// Default rate limit
"**": 3e3,
// Import/Export
"projects/import": 6,
"projects/*/export": 6,
"projects/*/download": 1,
"groups/import": 6,
"groups/*/export": 6,
"groups/*/download": 1,
// Note creation
"projects/*/issues/*/notes": {
method: "post",
limit: 300
},
"projects/*/snippets/*/notes": {
method: "post",
limit: 300
},
"projects/*/merge_requests/*/notes": {
method: "post",
limit: 300
},
"groups/*/epics/*/notes": {
method: "post",
limit: 300
},
// Repositories - get file archive
"projects/*/repository/archive*": 5,
// Project Jobs
"projects/*/jobs": 600,
// Member deletion
"projects/*/members": 60,
"groups/*/members": 60
});
var BaseResource = class {
url;
requester;
queryTimeout;
headers;
authHeaders;
camelize;
rejectUnauthorized;
constructor({
sudo,
profileToken,
camelize,
requesterFn,
profileMode = "execution",
host = "https://gitlab.com",
prefixUrl = "",
rejectUnauthorized = true,
queryTimeout = 3e5,
rateLimitDuration = 60,
rateLimits = DEFAULT_RATE_LIMITS,
...tokens
}) {
if (!requesterFn) throw new ReferenceError("requesterFn must be passed");
this.url = [host, "api", "v4", prefixUrl].join("/");
this.headers = {};
this.authHeaders = {};
this.rejectUnauthorized = rejectUnauthorized;
this.camelize = camelize;
this.queryTimeout = queryTimeout;
if ("oauthToken" in tokens)
this.authHeaders.authorization = async () => {
const token = await getDynamicToken(tokens.oauthToken);
return `Bearer ${token}`;
};
else if ("jobToken" in tokens)
this.authHeaders["job-token"] = async () => getDynamicToken(tokens.jobToken);
else if ("token" in tokens)
this.authHeaders["private-token"] = async () => getDynamicToken(tokens.token);
if (profileToken) {
this.headers["X-Profile-Token"] = profileToken;
this.headers["X-Profile-Mode"] = profileMode;
}
if (sudo) this.headers.Sudo = `${sudo}`;
this.requester = requesterFn({ ...this, rateLimits, rateLimitDuration });
}
};
// src/GitbeakerError.ts
var GitbeakerRequestError = class extends Error {
cause;
constructor(message, options) {
super(message, options);
this.cause = options?.cause;
this.name = "GitbeakerRequestError";
}
};
var GitbeakerTimeoutError = class extends Error {
constructor(message, options) {
super(message, options);
this.name = "GitbeakerTimeoutError";
}
};
var GitbeakerRetryError = class extends Error {
constructor(message, options) {
super(message, options);
this.name = "GitbeakerRetryError";
}
};
exports.BaseResource = BaseResource;
exports.GitbeakerRequestError = GitbeakerRequestError;
exports.GitbeakerRetryError = GitbeakerRetryError;
exports.GitbeakerTimeoutError = GitbeakerTimeoutError;
exports.createRateLimiters = createRateLimiters;
exports.createRequesterFn = createRequesterFn;
exports.defaultOptionsHandler = defaultOptionsHandler;
exports.formatQuery = formatQuery;
exports.generateRateLimiterFn = generateRateLimiterFn;
exports.getMatchingRateLimiter = getMatchingRateLimiter;
exports.presetResourceArguments = presetResourceArguments;
;