UNPKG

@prismatic-io/spectral

Version:

Utility library for building Prismatic connectors and code-native integrations

302 lines (301 loc) 14.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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 __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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.inputs = exports.buildRawRequestAction = exports.sendRawRequest = exports.handleErrors = exports.createClient = exports.toAxiosRetryConfig = void 0; const axios_1 = __importDefault(require("axios")); const axios_retry_1 = __importStar(require("axios-retry")); const form_data_1 = __importDefault(require("form-data")); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const object_sizeof_1 = __importDefault(require("object-sizeof")); const __1 = require("../.."); const util_1 = __importDefault(require("../../util")); const inputs_1 = require("./inputs"); Object.defineProperty(exports, "inputs", { enumerable: true, get: function () { return inputs_1.inputs; } }); const toAuthorizationHeaders = (connection) => { var _a, _b, _c, _d; const accessToken = util_1.default.types.toString((_a = connection.token) === null || _a === void 0 ? void 0 : _a.access_token); if (accessToken) { return { Authorization: `Bearer ${accessToken}` }; } const apiKey = util_1.default.types.toString((_b = connection.fields) === null || _b === void 0 ? void 0 : _b.apiKey); if (apiKey) { return { Authorization: `Bearer ${apiKey}` }; } const username = util_1.default.types.toString((_c = connection.fields) === null || _c === void 0 ? void 0 : _c.username); const password = util_1.default.types.toString((_d = connection.fields) === null || _d === void 0 ? void 0 : _d.password); if (username && password) { const encoded = Buffer.from(`${username}:${password}`).toString("base64"); return { Authorization: `Basic ${encoded}` }; } throw new Error(`Failed to guess at authorization parameters for Connection: ${connection.key}`); }; const toFormData = (formData, fileData, fileDataFileNames = {}) => { const form = new form_data_1.default(); (formData || []).map(({ key, value }) => form.append(key, value)); (fileData || []).map(({ key, value }) => form.append(key, util_1.default.types.toBufferDataPayload(value).data, { filename: (fileDataFileNames === null || fileDataFileNames === void 0 ? void 0 : fileDataFileNames[key]) || key, })); return form; }; const computeRetryDelay = (retryDelay, useExponentialBackoff) => { if (useExponentialBackoff) { return typeof retryDelay === "number" ? (retryCount) => Math.pow(2, (retryCount - 1)) * retryDelay : axios_retry_1.exponentialDelay; } // retryDelay is either a number or a function return typeof retryDelay === "number" ? () => retryDelay : retryDelay; }; const toAxiosRetryConfig = (_a) => { var { retryDelay, retryAllErrors, retryCondition, useExponentialBackoff } = _a, rest = __rest(_a, ["retryDelay", "retryAllErrors", "retryCondition", "useExponentialBackoff"]); return (Object.assign(Object.assign({}, rest), { retryDelay: computeRetryDelay(retryDelay, useExponentialBackoff), retryCondition: retryAllErrors ? () => true : typeof retryCondition === "function" ? retryCondition : axios_retry_1.isNetworkOrIdempotentRequestError })); }; exports.toAxiosRetryConfig = toAxiosRetryConfig; /** * Creates a reusable Axios HTTP client pre-configured with a base URL, * headers, timeout, and optional retry logic. This is the recommended * way to make HTTP requests from custom component actions. * * @param props Configuration for the HTTP client. * @returns An Axios instance configured with the provided options. * @see {@link https://prismatic.io/docs/custom-connectors/connections/#using-the-built-in-createclient-http-client | Using the Built-in HTTP Client} * @example * import { createClient } from "@prismatic-io/spectral/dist/clients/http"; * * const client = createClient({ * baseUrl: "https://api.acme.com/v2", * headers: { Authorization: `Bearer ${accessToken}` }, * responseType: "json", * timeout: 30000, * debug: false, * retryConfig: { * retries: 3, * retryDelay: 1000, * useExponentialBackoff: true, * retryAllErrors: false, * }, * }); * * const { data } = await client.get("/items"); */ const createClient = ({ baseUrl, responseType, headers, timeout, params, debug = false, retryConfig, }) => { const client = axios_1.default.create({ baseURL: baseUrl, responseType, headers, timeout, params, maxContentLength: Number.POSITIVE_INFINITY, maxBodyLength: Number.POSITIVE_INFINITY, }); if (debug) { client.interceptors.request.use((request) => { const { baseURL, headers, method, timeout, url, data, params } = request; const dataSize = (0, object_sizeof_1.default)(data); console.log(util_1.default.types.toJSON({ type: "request", baseURL, params, url, headers, method, timeout, data: dataSize > 1024 * 10 || Buffer.isBuffer(data) ? `<data (${dataSize} bytes)>` : data, }, true, true)); return request; }); client.interceptors.response.use((response) => { const { headers, status, statusText, data } = response; const dataSize = (0, object_sizeof_1.default)(data); console.log(util_1.default.types.toJSON({ type: "response", headers, status, statusText, data: dataSize > 1024 * 10 || Buffer.isBuffer(data) ? `<data (${dataSize} bytes)>` : data, }, true, true)); return response; }); } if (retryConfig) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (0, axios_retry_1.default)(client, (0, exports.toAxiosRetryConfig)(retryConfig)); } return client; }; exports.createClient = createClient; /** * A global error handler that examines a thrown error and yields additional * information if the error was produced by Spectral's HTTP client. If the * error is an Axios error, returns a structured object with the response * `data`, `status`, and `headers`. Otherwise, returns the error as-is. * * Commonly used as a component-level `hooks.error` handler. * * @param error A JavaScript error to handle. * @returns An error with data, status and headers if it was an Axios error, or the error otherwise. * @see {@link https://prismatic.io/docs/custom-connectors/error-handling/ | Error Handling} * @example * import { component } from "@prismatic-io/spectral"; * import { handleErrors } from "@prismatic-io/spectral/dist/clients/http"; * * export default component({ * key: "acme", * display: { label: "Acme", description: "Acme connector", iconPath: "icon.png" }, * hooks: { error: handleErrors }, * actions: { ... }, * }); */ const handleErrors = (error) => { if (axios_1.default.isAxiosError(error)) { const { message, response } = error; return { message, data: response === null || response === void 0 ? void 0 : response.data, status: response === null || response === void 0 ? void 0 : response.status, headers: response === null || response === void 0 ? void 0 : response.headers, }; } return error; }; exports.handleErrors = handleErrors; /** * This function sends a raw HTTP request with full control over method, URL, * headers, query parameters, and body. Used internally by `buildRawRequestAction`. * * @param baseUrl The base URL of the API you're integrating with. * @param values An object comprising the HTTP request you'd like to make. * @param authorizationHeaders Auth headers to apply to the request. * @returns The Axios response to the request. * @see {@link https://prismatic.io/docs/integrations/low-code-integration-designer/raw-request-actions/#building-an-http-raw-request-action-in-your-custom-component | Raw Request Actions} * @example * import { sendRawRequest } from "@prismatic-io/spectral/dist/clients/http"; * * const response = await sendRawRequest( * "https://api.acme.com/v2", * { * method: "GET", * url: "/items", * headers: [], * queryParams: [{ key: "limit", value: "10" }], * responseType: "json", * }, * { Authorization: "Bearer my-token" }, * ); */ const sendRawRequest = (baseUrl_1, values_1, ...args_1) => __awaiter(void 0, [baseUrl_1, values_1, ...args_1], void 0, function* (baseUrl, values, authorizationHeaders = {}) { if (values.data && (!(0, isEmpty_1.default)(values.formData) || !(0, isEmpty_1.default)(values.fileData))) { throw new Error("Cannot specify both Data and File/Form Data."); } const payload = !(0, isEmpty_1.default)(values.formData) || !(0, isEmpty_1.default)(values.fileData) ? toFormData(values.formData, values.fileData, values.fileDataFileNames) : values.data; const client = (0, exports.createClient)({ baseUrl, debug: values.debugRequest, responseType: values.responseType, timeout: values.timeout, retryConfig: { retries: values.maxRetries, retryDelay: values.retryDelayMS, retryAllErrors: values.retryAllErrors, useExponentialBackoff: values.useExponentialBackoff, }, }); return yield client.request({ method: values.method, url: values.url, headers: Object.assign(Object.assign(Object.assign({}, util_1.default.types.keyValPairListToObject(values.headers)), (payload instanceof form_data_1.default ? payload.getHeaders() : {})), authorizationHeaders), params: util_1.default.types.keyValPairListToObject(values.queryParams), data: payload || undefined, }); }); exports.sendRawRequest = sendRawRequest; /** * Builds a pre-configured "Raw Request" action for a custom connector. * This action exposes a full HTTP interface (method, URL, headers, query * params, body) so integration builders can make arbitrary API calls. * * @param baseUrl The base URL of the API you're integrating with. * @param label The display label for the action. Defaults to `"Raw Request"`. * @param description The display description for the action. Defaults to `"Issue a raw HTTP request"`. * @returns An action definition for the raw request action. * @see {@link https://prismatic.io/docs/integrations/low-code-integration-designer/raw-request-actions/#building-an-http-raw-request-action-in-your-custom-component | Raw Request Actions} * @example * import { buildRawRequestAction } from "@prismatic-io/spectral/dist/clients/http"; * * // Add a raw request action to your component * const actions = { * listItems: action({ ... }), * rawRequest: buildRawRequestAction("https://api.acme.com/v2"), * }; */ const buildRawRequestAction = (baseUrl, label = "Raw Request", description = "Issue a raw HTTP request") => (0, __1.action)({ display: { label, description }, inputs: Object.assign({ connection: { label: "Connection", type: "connection", required: true } }, inputs_1.inputs), perform: (_context, _a) => __awaiter(void 0, void 0, void 0, function* () { var { connection } = _a, httpInputValues = __rest(_a, ["connection"]); const { data } = yield (0, exports.sendRawRequest)(baseUrl, httpInputValues, toAuthorizationHeaders(connection)); return { data }; }), }); exports.buildRawRequestAction = buildRawRequestAction;