mercadopago
Version:
Mercadopago SDK for Node.js
171 lines (170 loc) • 9.55 kB
JavaScript
;
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RestClient = void 0;
const crypto_1 = require("crypto");
const config_1 = require("../config");
const errors_1 = require("../errors");
const crypto_2 = require("crypto");
/** HTTP 204 — the server processed the request but returns no body. */
const NO_CONTENT = 204;
function headersToRecord(headers) {
const out = {};
headers.forEach((value, key) => {
if (!out[key])
out[key] = [];
out[key].push(value);
});
return out;
}
class RestClient {
/** Generates a UUID v4 idempotency key for write operations. */
static generateIdempotencyKey() {
return (0, crypto_2.randomUUID)();
}
/**
* Appends query-string parameters to a URL.
*
* Skips `undefined` values so callers don't need to pre-filter.
* Handles URLs that already contain a `?` by appending with `&`.
*
* @param url - Base URL (may already include a query string).
* @param queryParams - Key-value pairs to append.
* @returns The URL with the encoded query string.
*/
static appendQueryParamsToUrl(url, queryParams) {
if (!queryParams)
return url;
const searchParams = new URLSearchParams();
for (const key in queryParams) {
if (Object.prototype.hasOwnProperty.call(queryParams, key) && typeof queryParams[key] !== 'undefined') {
searchParams.append(key, queryParams[key].toString());
}
}
return url.includes('?') ? `${url}&${searchParams.toString()}` : `${url}?${searchParams.toString()}`;
}
static computeDelay(attempt, initialDelay, maxDelay, useJitter) {
const exponential = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
if (!useJitter || exponential <= 0)
return exponential;
// Use crypto.randomInt — NEVER Math.random()
return (0, crypto_1.randomInt)(0, exponential + 1);
}
static parseRetryAfterMs(response) {
const header = response.headers.get('Retry-After');
if (!header)
return null;
const seconds = parseInt(header, 10);
return isNaN(seconds) ? null : seconds * 1000;
}
static async retryWithExponentialBackoff(fn, retries, retryOn, initialDelay, maxDelay, useJitter, onRetry) {
let lastError = new Error('Retry loop exited without result');
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fn();
if (response.ok)
return response;
if (response.status === 204)
return response;
// Non-ok response — should we retry?
if (attempt < retries && retryOn.includes(response.status)) {
const retryAfterMs = response.status === 429
? RestClient.parseRetryAfterMs(response)
: null;
const delay = retryAfterMs !== null
? Math.min(retryAfterMs, maxDelay)
: RestClient.computeDelay(attempt, initialDelay, maxDelay, useJitter);
// Build a typed error for the onRetry callback
const errorBody = await response.json().catch(() => ({}));
const err = (0, errors_1.buildError)(response.status, errorBody);
lastError = err;
onRetry === null || onRetry === void 0 ? void 0 : onRetry(attempt + 1, err);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
}
catch (networkError) {
if (attempt >= retries) {
throw new errors_1.MPConnectionError(networkError);
}
const err = new errors_1.MPConnectionError(networkError);
lastError = err;
onRetry === null || onRetry === void 0 ? void 0 : onRetry(attempt + 1, err);
const delay = RestClient.computeDelay(attempt, initialDelay, maxDelay, useJitter);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Performs an HTTP request against the MercadoPago API.
*
* This is the single exit point for all network I/O in the SDK.
* It merges SDK defaults with caller-provided overrides, injects
* required headers, and normalises the JSON response.
*
* - **204 No Content** → returns `{ api_response }` with no body.
* - **2xx with body** → returns the parsed JSON with `api_response` appended.
* - **Non-2xx** → throws a typed {@link MercadoPagoError} subtype.
*
* @typeParam T - Expected shape of the parsed JSON response.
* @param endpoint - API path relative to the base URL (e.g. `/v1/payments`).
* @param config - Merged request settings (headers, body, method, options, etc.).
* @returns Parsed API response with an `api_response` envelope.
*/
static async fetch(endpoint, config) {
const _a = config || {}, { timeout = config_1.AppConfig.DEFAULT_TIMEOUT, idempotencyKey = RestClient.generateIdempotencyKey(), queryParams, method = 'GET', retries = config_1.AppConfig.DEFAULT_RETRIES, maxRetries, retryOn = config_1.AppConfig.DEFAULT_RETRY_ON, initialDelay = config_1.AppConfig.BASE_DELAY_MS, maxDelay = config_1.AppConfig.DEFAULT_MAX_DELAY_MS, jitter = false, onRetry, corporationId, integratorId, platformId, meliSessionId, expandResponseNodes, cardValidation, testToken } = _a, customConfig = __rest(_a, ["timeout", "idempotencyKey", "queryParams", "method", "retries", "maxRetries", "retryOn", "initialDelay", "maxDelay", "jitter", "onRetry", "corporationId", "integratorId", "platformId", "meliSessionId", "expandResponseNodes", "cardValidation", "testToken"]);
// maxRetries (new field) takes precedence over retries (legacy field)
const effectiveRetries = maxRetries !== undefined ? maxRetries : retries;
const url = RestClient.appendQueryParamsToUrl(`${config_1.AppConfig.BASE_URL}${endpoint}`, queryParams);
customConfig.headers = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, customConfig.headers), { [config_1.AppConfig.Headers.CONTENT_TYPE]: 'application/json', [config_1.AppConfig.Headers.PRODUCT_ID]: config_1.AppConfig.PRODUCT_ID, [config_1.AppConfig.Headers.TRACKING_ID]: config_1.AppConfig.getTrackingId(), [config_1.AppConfig.Headers.USER_AGENT]: config_1.AppConfig.getUserAgent() }), (corporationId ? { [config_1.AppConfig.Headers.CORPORATION_ID]: corporationId } : {})), (integratorId ? { [config_1.AppConfig.Headers.INTEGRATOR_ID]: integratorId } : {})), (platformId ? { [config_1.AppConfig.Headers.PLATFORM_ID]: platformId } : {})), (meliSessionId ? { [config_1.AppConfig.Headers.MELI_SESSION_ID]: meliSessionId } : {})), (expandResponseNodes ? { [config_1.AppConfig.Headers.EXPAND_RESPONDE_NODES]: expandResponseNodes } : {})), (cardValidation ? { [config_1.AppConfig.Headers.CARD_VALIDATION]: cardValidation } : {})), (testToken ? { [config_1.AppConfig.Headers.TEST_TOKEN]: testToken.toString() } : {}));
if (method && method !== 'GET') {
customConfig.headers = Object.assign(Object.assign({}, customConfig.headers), { [config_1.AppConfig.Headers.IDEMPOTENCY_KEY]: idempotencyKey });
}
const fetchFn = async () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, Object.assign(Object.assign({}, customConfig), { method, signal: controller.signal }));
}
finally {
clearTimeout(timeoutId);
}
};
const response = await RestClient.retryWithExponentialBackoff(fetchFn, effectiveRetries, retryOn, initialDelay, maxDelay, jitter, onRetry);
if (response.status === NO_CONTENT) {
return {
api_response: {
status: response.status,
headers: headersToRecord(response.headers),
}
};
}
const body = await response.json().catch(() => ({}));
if (!response.ok) {
const retryAfter = response.status === 429
? RestClient.parseRetryAfterMs(response)
: null;
const seconds = retryAfter !== null ? retryAfter / 1000 : null;
throw (0, errors_1.buildError)(response.status, body, seconds);
}
const api_response = {
status: response.status,
headers: headersToRecord(response.headers),
};
body.api_response = api_response;
return body;
}
}
exports.RestClient = RestClient;