@prismatic-io/spectral
Version:
Utility library for building Prismatic connectors and code-native integrations
302 lines (301 loc) • 14.3 kB
JavaScript
;
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;