UNPKG

@datocms/rest-client-utils

Version:
287 lines 15.4 kB
/// <reference lib="dom" /> var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; import { buildNormalizedParams } from './buildNormalizedParams'; import { ApiError, TimeoutError, } from './errors'; import { CanceledPromiseError, makeCancelablePromise, } from './makeCancelablePromise'; import { wait } from './wait'; var isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; var MAX_RETRY_COUNT_ON_TIMEOUT_ERROR = 5; export var LogLevel; (function (LogLevel) { /** No logging */ LogLevel[LogLevel["NONE"] = 0] = "NONE"; /** Logs HTTP requests (method, URL) and responses (status) */ LogLevel[LogLevel["BASIC"] = 1] = "BASIC"; /** Logs HTTP requests (method, URL, body) and responses (status, body) */ LogLevel[LogLevel["BODY"] = 2] = "BODY"; /** Logs HTTP requests (method, URL, headers, body) and responses (status, headers, body) */ LogLevel[LogLevel["BODY_AND_HEADERS"] = 3] = "BODY_AND_HEADERS"; })(LogLevel || (LogLevel = {})); function headersToObject(headers) { var result = {}; headers.forEach(function (value, key) { result[key] = value; }); return result; } function buildApiErrorInitObject(method, url, requestHeaders, requestBody, response, responseBody, preCallStack) { return { request: { url: url, method: method, headers: requestHeaders, body: requestBody, }, response: { status: response.status, statusText: response.statusText, headers: headersToObject(response.headers), body: responseBody, }, preCallStack: preCallStack, }; } function buildTimeoutErrorInitObject(method, url, requestHeaders, requestBody, preCallStack) { return { request: { url: url, method: method, headers: requestHeaders, body: requestBody, }, preCallStack: preCallStack, }; } function buildApiErrorInitObjectFromJobResult(method, url, requestHeaders, requestBody, responseStatus, responseBody, preCallStack) { return { request: { url: url, method: method, headers: requestHeaders, body: requestBody, }, response: { status: responseStatus, statusText: 'N/A', headers: {}, body: responseBody, }, preCallStack: preCallStack, }; } function isErrorWithCode(error) { return typeof error === 'object' && !!error && 'code' in error; } function lowercaseKeys(obj) { return Object.fromEntries(Object.entries(obj).map(function (_a) { var k = _a[0], v = _a[1]; return [k.toLowerCase(), v]; })); } var requestCount = 1; export function getFetchFn(customFetchFn) { var fetchFn = customFetchFn || (typeof fetch === 'undefined' ? undefined : fetch) || (typeof globalThis === 'undefined' ? undefined : globalThis.fetch); if (typeof fetchFn === 'undefined') { throw new Error('fetch() is not available: either polyfill it globally, or provide it as fetchFn option.'); } return fetchFn; } export function request(options) { var _a; return __awaiter(this, void 0, void 0, function () { var requestId, fetchFn, preCallStack, userAgent, retryCount, logLevel, autoRetry, log, headers, baseUrl, body, queryString, url, _i, _b, _c, key, value, requestPromise_1, timeoutId, response, responseContentType, invalidContentType, waitTimeInSecs, _d, _e, key, value, responseBody, _f, jobResult, error, transientErrorCode, error_1; return __generator(this, function (_g) { switch (_g.label) { case 0: requestId = requestCount; requestCount += 1; fetchFn = getFetchFn(options.fetchFn); preCallStack = options.preCallStack; userAgent = options.userAgent || '@datocms/rest-client-utils'; retryCount = options.retryCount || 1; logLevel = options.logLevel || LogLevel.NONE; autoRetry = 'autoRetry' in options ? options.autoRetry : true; log = options.logFn || (function () { return true; }); headers = __assign({ 'content-type': 'application/json', accept: 'application/json', authorization: "Bearer ".concat(options.apiToken), 'user-agent': userAgent }, (options.extraHeaders ? lowercaseKeys(options.extraHeaders) : {})); if (isBrowser) { // user agent cannot be set on browser delete headers['user-agent']; } baseUrl = options.baseUrl.replace(/\/$/, ''); body = options.body ? JSON.stringify(options.body, null, 2) : undefined; queryString = options.queryParams && Object.keys(options.queryParams).length > 0 ? "?".concat(new URLSearchParams(buildNormalizedParams(options.queryParams)).toString()) : ''; url = "".concat(baseUrl).concat(options.url).concat(queryString); if (logLevel >= LogLevel.BASIC) { log("[".concat(requestId, "] ").concat(options.method, " ").concat(url)); if (logLevel >= LogLevel.BODY_AND_HEADERS) { for (_i = 0, _b = Object.entries(headers || {}); _i < _b.length; _i++) { _c = _b[_i], key = _c[0], value = _c[1]; log("[".concat(requestId, "] ").concat(key, ": ").concat(value)); } } if (logLevel >= LogLevel.BODY && body) { log("[".concat(requestId, "] ").concat(body)); } } _g.label = 1; case 1: _g.trys.push([1, 12, , 16]); requestPromise_1 = makeCancelablePromise(fetchFn(url, { method: options.method, headers: headers, body: body, })); timeoutId = setTimeout(function () { requestPromise_1.cancel(); }, options.requestTimeout || 30000); return [4 /*yield*/, requestPromise_1]; case 2: response = _g.sent(); clearTimeout(timeoutId); responseContentType = response.headers.get('Content-Type'); invalidContentType = responseContentType && !responseContentType.includes('application/json'); if (!(response.status === 429 || invalidContentType)) return [3 /*break*/, 4]; if (!autoRetry || (invalidContentType && options.method !== 'GET')) { throw new ApiError(buildApiErrorInitObject(options.method, url, headers, options.body, response, undefined, preCallStack)); } waitTimeInSecs = response.headers.has('X-RateLimit-Reset') ? Number.parseInt(response.headers.get('X-RateLimit-Reset'), 10) : retryCount; if (logLevel >= LogLevel.BASIC) { if (response.status === 429) { log("[".concat(requestId, "] Rate limit exceeded, wait ").concat(waitTimeInSecs, " seconds then retry...")); } else { log("[".concat(requestId, "] Invalid response content type \"").concat(responseContentType, "\" (status ").concat(response.status, "). Wait ").concat(waitTimeInSecs, " seconds then retry...")); } } return [4 /*yield*/, wait(waitTimeInSecs * 1000)]; case 3: _g.sent(); return [2 /*return*/, request(__assign(__assign({}, options), { retryCount: retryCount + 1 }))]; case 4: if (logLevel >= LogLevel.BASIC) { log("[".concat(requestId, "] Status: ").concat(response.status, " (").concat(response.statusText, ")")); if (logLevel >= LogLevel.BODY_AND_HEADERS) { for (_d = 0, _e = [ 'x-api-version', 'x-environment', 'x-queue-time', 'x-ratelimit-remaining', 'x-request-id', 'cf-ray', ]; _d < _e.length; _d++) { key = _e[_d]; value = response.headers.get(key); if (value) { log("[".concat(requestId, "] ").concat(key, ": ").concat(value)); } } } } if (!(response.status === 204)) return [3 /*break*/, 5]; _f = undefined; return [3 /*break*/, 7]; case 5: return [4 /*yield*/, response.json()]; case 6: _f = _g.sent(); _g.label = 7; case 7: responseBody = _f; if (logLevel >= LogLevel.BODY && responseBody) { log("[".concat(requestId, "] ").concat(JSON.stringify(responseBody, null, 2))); } if (!(response.status === 202)) return [3 /*break*/, 9]; return [4 /*yield*/, options.fetchJobResult(responseBody.data.id)]; case 8: jobResult = _g.sent(); if (jobResult.status < 200 || jobResult.status >= 300) { throw new ApiError(buildApiErrorInitObjectFromJobResult(options.method, url, headers, options.body, jobResult.status, jobResult.payload, preCallStack)); } responseBody = jobResult.payload; _g.label = 9; case 9: if (response.status >= 200 && response.status < 300) { return [2 /*return*/, responseBody]; } error = new ApiError(buildApiErrorInitObject(options.method, url, headers, options.body, response, responseBody, preCallStack)); transientErrorCode = (_a = error.errors.find(function (e) { return e.attributes.transient; })) === null || _a === void 0 ? void 0 : _a.attributes.code; if (!(autoRetry && transientErrorCode)) return [3 /*break*/, 11]; if (logLevel >= LogLevel.BASIC) { log("[".concat(requestId, "] ").concat(transientErrorCode, ", wait ").concat(retryCount, " seconds then retry...")); } return [4 /*yield*/, wait(retryCount * 1000)]; case 10: _g.sent(); return [2 /*return*/, request(__assign(__assign({}, options), { retryCount: retryCount + 1 }))]; case 11: throw error; case 12: error_1 = _g.sent(); if (!(error_1 instanceof CanceledPromiseError || (isErrorWithCode(error_1) && error_1.code.includes('ETIMEDOUT')))) return [3 /*break*/, 15]; if (!(autoRetry && retryCount < MAX_RETRY_COUNT_ON_TIMEOUT_ERROR)) return [3 /*break*/, 14]; if (logLevel >= LogLevel.BASIC) { log("[".concat(requestId, "] Timeout error, wait ").concat(retryCount, " seconds then retry...")); } return [4 /*yield*/, wait(retryCount * 1000)]; case 13: _g.sent(); return [2 /*return*/, request(__assign(__assign({}, options), { retryCount: retryCount + 1 }))]; case 14: throw new TimeoutError(buildTimeoutErrorInitObject(options.method, url, headers, options.body, preCallStack)); case 15: throw error_1; case 16: return [2 /*return*/]; } }); }); } //# sourceMappingURL=request.js.map