UNPKG

@gitbeaker/requester-utils

Version:

Utility functions for requester implementatons used in @gitbeaker

236 lines (228 loc) 7.45 kB
'use strict'; 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;