@prismatic-io/spectral
Version:
Utility library for building Prismatic connectors and code-native integrations
860 lines (859 loc) • 43.6 kB
JavaScript
;
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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.invokeTriggerComponentInput = exports.convertConfigVar = exports.convertInputValue = exports.convertFlow = exports.convertQueueConfig = exports.convertConfigPages = exports.convertIntegration = exports.CONCURRENCY_LIMIT_MIN = exports.CONCURRENCY_LIMIT_MAX = void 0;
const node_crypto_1 = require("node:crypto");
const fs_1 = require("fs");
const assign_1 = __importDefault(require("lodash/assign"));
const camelCase_1 = __importDefault(require("lodash/camelCase"));
const merge_1 = __importDefault(require("lodash/merge"));
const pick_1 = __importDefault(require("lodash/pick"));
const path_1 = __importDefault(require("path"));
const yaml_1 = __importDefault(require("yaml"));
const types_1 = require("../types");
const asyncContext_1 = require("./asyncContext");
const context_1 = require("./context");
const convertComponent_1 = require("./convertComponent");
const integration_1 = require("./integration");
const perform_1 = require("./perform");
exports.CONCURRENCY_LIMIT_MAX = 15;
exports.CONCURRENCY_LIMIT_MIN = 2;
const convertIntegration = (definition) => {
var _a, _b, _c;
// Generate a unique reference key that will be used to reference the
// actions, triggers, data sources, and connections that are created
// inline as part of the integration definition.
const referenceKey = (0, node_crypto_1.randomUUID)();
const scopedConfigVars = (_a = definition.scopedConfigVars) !== null && _a !== void 0 ? _a : {};
const configVars = Object.values({
configPages: (_b = definition.configPages) !== null && _b !== void 0 ? _b : {},
userLevelConfigPages: (_c = definition.userLevelConfigPages) !== null && _c !== void 0 ? _c : {},
}).reduce((acc, configPages) => (Object.assign(Object.assign({}, acc), Object.values(configPages).reduce((acc, configPage) => Object.entries(configPage.elements).reduce((acc, [key, element]) => {
// "string" elements are HTML elements and should be ignored.
if (typeof element === "string") {
return acc;
}
if (key in acc || key in scopedConfigVars) {
throw new Error(`Duplicate config var key: "${key}"`);
}
return Object.assign(Object.assign({}, acc), { [key]: element });
}, acc), {}))), {});
let metadata = {};
try {
const metaDataPath = path_1.default.join("..", ".spectral", "metadata.json");
const file = (0, fs_1.readFileSync)(metaDataPath, { encoding: "utf-8" });
metadata = JSON.parse(file);
}
catch (_e) {
// No-op. If there's no metadata file then we move on.
}
const cniComponent = codeNativeIntegrationComponent(definition, referenceKey, configVars);
const cniYaml = codeNativeIntegrationYaml(definition, referenceKey, configVars, metadata);
const publishingMetadata = codeNativeIntegrationPublishingMetadata(definition);
return Object.assign(Object.assign({}, cniComponent), { codeNativeIntegrationYAML: cniYaml, publishingMetadata });
};
exports.convertIntegration = convertIntegration;
const convertConfigPages = (pages, userLevelConfigured) => {
if (!pages || !Object.keys(pages).length) {
return [];
}
return Object.entries(pages).map(([name, { tagline, elements }]) => (Object.assign(Object.assign({ name,
tagline }, (userLevelConfigured ? { userLevelConfigured } : {})), { elements: Object.entries(elements)
.filter(([_key, value]) => !(0, types_1.isConnectionScopedConfigVar)(value))
.map(([key, value]) => {
if (typeof value === "string") {
return {
type: "htmlElement",
value,
};
}
else if (value &&
typeof value === "object" &&
"dataType" in value &&
value.dataType === "htmlElement") {
return {
type: "htmlElement",
value: key,
};
}
return {
type: "configVar",
value: key,
};
}) })));
};
exports.convertConfigPages = convertConfigPages;
const codeNativeIntegrationYaml = ({ name, description, category, documentation, version, labels, endpointType, triggerPreprocessFlowConfig, flows, configPages, userLevelConfigPages, scopedConfigVars, instanceProfile, componentRegistry = {}, }, referenceKey, configVars, metadata) => {
// Find the preprocess flow config on the flow, if one exists.
const preprocessFlows = flows.filter((flow) => flow.preprocessFlowConfig);
// Do some validation of preprocess flow configs.
if (preprocessFlows.length > 1) {
throw new Error("Only one flow may define a Preprocess Flow Config.");
}
if (preprocessFlows.length && triggerPreprocessFlowConfig) {
throw new Error("Integration must not define both a Trigger Preprocess Flow Config and a Preprocess Flow.");
}
const hasPreprocessFlow = preprocessFlows.length > 0;
const preprocessFlowConfig = hasPreprocessFlow
? preprocessFlows[0].preprocessFlowConfig
: triggerPreprocessFlowConfig;
const nonPreprocessFlowTypes = ["instance_specific", "shared_instance"];
if (nonPreprocessFlowTypes.includes(endpointType || "flow_specific") && !preprocessFlowConfig) {
throw new Error("Integration with specified EndpointType must define either a Trigger Preprocess Flow Config or a Preprocess Flow.");
}
const configVarMap = Object.entries(scopedConfigVars !== null && scopedConfigVars !== void 0 ? scopedConfigVars : {}).reduce((acc, [key, value]) => {
if (typeof value === "string") {
return acc;
}
return Object.assign(Object.assign({}, acc), { [key]: value });
}, Object.assign({}, (configVars !== null && configVars !== void 0 ? configVars : {})));
const requiredConfigVars = [];
Object.entries(configVarMap).forEach(([key, configVar]) => {
if (!(0, types_1.isHtmlElementConfigVar)(configVar)) {
requiredConfigVars.push((0, exports.convertConfigVar)(key, configVar, referenceKey, componentRegistry));
}
});
// Transform the IntegrationDefinition into the structure that is appropriate
// for generating YAML, which will then be used by the Prismatic API to import
// the integration as a Code Native Integration.
const result = Object.assign(Object.assign({ definitionVersion: integration_1.DefinitionVersion, isCodeNative: true, name,
description,
category,
documentation,
version,
labels,
requiredConfigVars,
endpointType, preprocessFlowName: hasPreprocessFlow ? preprocessFlows[0].name : undefined, externalCustomerIdField: fieldNameToReferenceInput(hasPreprocessFlow ? "onExecution" : "payload", preprocessFlowConfig === null || preprocessFlowConfig === void 0 ? void 0 : preprocessFlowConfig.externalCustomerIdField), externalCustomerUserIdField: fieldNameToReferenceInput(hasPreprocessFlow ? "onExecution" : "payload", preprocessFlowConfig === null || preprocessFlowConfig === void 0 ? void 0 : preprocessFlowConfig.externalCustomerUserIdField), flowNameField: fieldNameToReferenceInput(hasPreprocessFlow ? "onExecution" : "payload", preprocessFlowConfig === null || preprocessFlowConfig === void 0 ? void 0 : preprocessFlowConfig.flowNameField), flows: flows.map((flow) => (0, exports.convertFlow)(flow, componentRegistry, referenceKey)) }, (instanceProfile && { defaultInstanceProfile: instanceProfile })), { configPages: [
...(0, exports.convertConfigPages)(configPages, false),
...(0, exports.convertConfigPages)(userLevelConfigPages, true),
], importMetadata: metadata });
return yaml_1.default.stringify(result);
};
const permissionAndVisibilityTypeValueMap = {
customer: {
orgOnly: false,
visibleToOrgDeployer: true,
visibleToCustomerDeployer: true,
},
embedded: {
orgOnly: false,
visibleToOrgDeployer: true,
visibleToCustomerDeployer: false,
},
organization: {
orgOnly: true,
visibleToOrgDeployer: true,
visibleToCustomerDeployer: false,
},
};
const getPermissionAndVisibilityValues = ({ permissionAndVisibilityType = "customer", visibleToOrgDeployer = true, }) => {
return Object.assign(Object.assign({}, permissionAndVisibilityTypeValueMap[permissionAndVisibilityType]), (visibleToOrgDeployer !== undefined ? { visibleToOrgDeployer } : {}));
};
/** Converts permission and visibility properties into `meta` properties for inputs. */
const convertInputPermissionAndVisibility = ({ permissionAndVisibilityType, visibleToOrgDeployer, }) => {
const meta = getPermissionAndVisibilityValues({
permissionAndVisibilityType,
visibleToOrgDeployer,
});
return meta;
};
/** Converts permission and visibility properties into `meta` properties for config vars. */
const convertConfigVarPermissionAndVisibility = ({ permissionAndVisibilityType, visibleToOrgDeployer: visibleToOrgDeployerBase, }) => {
const { orgOnly, visibleToCustomerDeployer, visibleToOrgDeployer } = getPermissionAndVisibilityValues({
permissionAndVisibilityType,
visibleToOrgDeployer: visibleToOrgDeployerBase,
});
return {
orgOnly,
meta: {
visibleToCustomerDeployer,
visibleToOrgDeployer,
},
};
};
const convertComponentReference = (componentReference, componentRegistry, referenceType) => {
var _a, _b;
const manifest = componentRegistry[componentReference.component];
if (!manifest) {
throw new Error(`Component with key "${componentReference.component}" not found in component registry.`);
}
const manifestEntry = manifest[referenceType][componentReference.key];
if (!manifestEntry) {
throw new Error(`Component with key "${componentReference.component}" does not have an entry with key "${componentReference.key}" in the component registry.`);
}
const ref = {
component: {
key: manifest.key,
signature: (_a = manifest.signature) !== null && _a !== void 0 ? _a : "",
isPublic: manifest.public,
},
// older versions of the manifest did not contain a key so we fall back to the componentReference key
key: (_b = manifestEntry.key) !== null && _b !== void 0 ? _b : componentReference.key,
};
const inputs = Object.entries(manifestEntry.inputs).reduce((result, [key, manifestEntryInput]) => {
var _a, _b, _c;
const isCollection = Boolean(manifestEntryInput.collection);
// Retrieve the input value or default to the manifest's default value
const value = (_b = (_a = componentReference.values) === null || _a === void 0 ? void 0 : _a[key]) !== null && _b !== void 0 ? _b : {
value: isCollection
? manifestEntryInput.default === ""
? []
: manifestEntryInput.default
: ((_c = manifestEntryInput.default) !== null && _c !== void 0 ? _c : ""),
};
const type = isCollection ? "complex" : "value" in value ? "value" : "configVar";
if ("value" in value) {
const valueExpr = manifestEntryInput.collection === "keyvaluelist" && value.value instanceof Object
? Object.entries(value.value).map(([k, v]) => ({
name: { type: "value", value: k },
type: "value",
value: JSON.stringify(v),
}))
: manifestEntryInput.collection === "valuelist" && Array.isArray(value.value)
? value.value.map((v) => ({ type: "value", value: v }))
: value.value;
const formattedValue = type === "complex" || typeof valueExpr === "string"
? valueExpr
: JSON.stringify(valueExpr);
const meta = convertInputPermissionAndVisibility((0, pick_1.default)(value, ["permissionAndVisibilityType", "visibleToOrgDeployer"]));
const { writeOnly } = (0, pick_1.default)(value, ["writeOnly"]);
if (writeOnly) {
meta.writeOnly = writeOnly;
}
return Object.assign(Object.assign({}, result), { [key]: { type: type, value: formattedValue, meta } });
}
if ("configVar" in value) {
return Object.assign(Object.assign({}, result), { [key]: { type: "configVar", value: value.configVar } });
}
if ("template" in value) {
return Object.assign(Object.assign({}, result), { [key]: { type: "template", value: value.template } });
}
return result;
}, {});
return {
ref,
inputs,
};
};
const convertComponentRegistry = (componentRegistry, publicSupplementalComponent) => {
const convertedRegistry = Object.values(componentRegistry).map(({ key, public: isPublic, signature }) => (Object.assign({ key,
isPublic }, (signature ? { signature } : { version: "LATEST" }))));
if (publicSupplementalComponent) {
convertedRegistry.push({
key: `${publicSupplementalComponent}-triggers`,
isPublic: true,
version: "LATEST",
});
}
return convertedRegistry;
};
/**
* Create a reference to the private component built as part of this CNI.
*
* References to this component always use `version: "LATEST", isPublic: false`
* because they automatically publish alongside the corresponding CNI yml.
* */
const codeNativeIntegrationComponentReference = (referenceKey) => ({
key: referenceKey,
version: "LATEST",
isPublic: false,
});
/* A flow's trigger gets wrapped in a custom component if there's a defined
* onTrigger function, or if any custom onInstance or webhook lifecycle behavior is defined.
* */
const flowUsesWrapperTrigger = (flow) => {
return (typeof flow.onTrigger === "function" ||
flow.onInstanceDelete ||
flow.onInstanceDeploy ||
flow.webhookLifecycleHandlers);
};
/** Converts typed QueueConfig to legacy format with usesFifoQueue and concurrencyLimit. */
const convertQueueConfig = (queueConfig) => {
if (!("type" in queueConfig)) {
return queueConfig;
}
switch (queueConfig.type) {
case "parallel":
return { usesFifoQueue: false };
case "throttled":
return {
usesFifoQueue: true,
concurrencyLimit: queueConfig.concurrencyLimit,
dedupeIdField: queueConfig.dedupeIdField,
};
case "sequential":
return {
usesFifoQueue: true,
dedupeIdField: queueConfig.dedupeIdField,
};
default:
return queueConfig;
}
};
exports.convertQueueConfig = convertQueueConfig;
const convertFlowSchemas = (flowKey, schemas) => {
return Object.entries(schemas).reduce((acc, [key, value]) => {
var _a;
acc[key] = Object.assign({ title: value.title || `${flowKey}-${key}`, type: "object", $comment: value.$comment, properties: value.properties, $schema: value.$schema || types_1.DEFAULT_JSON_SCHEMA_VERSION }, (((_a = value.required) === null || _a === void 0 ? void 0 : _a.length) ? { required: value.required } : {}));
return acc;
}, {});
};
/** Converts a Flow into the structure necessary for YAML generation. */
const convertFlow = (flow, componentRegistry, referenceKey) => {
var _a;
const result = Object.assign({}, flow);
result.onTrigger = undefined;
result.trigger = undefined;
result.onInstanceDeploy = undefined;
result.onInstanceDelete = undefined;
result.webhookLifecycleHandlers = undefined;
result.onExecution = undefined;
result.preprocessFlowConfig = undefined;
result.errorConfig = undefined;
result.testApiKeys = undefined;
result.triggerType = undefined;
let publicSupplementalComponent;
const triggerStep = {
name: "On Trigger",
stableKey: `${flow.stableKey}-onTrigger`,
description: "The function that will be executed by the flow to return an HTTP response.",
isTrigger: true,
errorConfig: "errorConfig" in flow ? Object.assign({}, flow.errorConfig) : undefined,
};
const useWrapperTrigger = flowUsesWrapperTrigger(flow);
if ((0, types_1.isComponentReference)(flow.onTrigger) && !useWrapperTrigger) {
const { ref, inputs } = convertComponentReference(flow.onTrigger, componentRegistry, "triggers");
triggerStep.action = ref;
triggerStep.inputs = inputs;
}
else if (useWrapperTrigger) {
if (!flow.onTrigger) {
publicSupplementalComponent = flow.schedule ? "schedule" : "webhook";
}
triggerStep.action = {
key: flowFunctionKey(flow.name, "onTrigger"),
component: codeNativeIntegrationComponentReference(referenceKey),
};
}
else {
const hasSchedule = "schedule" in flow && typeof flow.schedule === "object";
const key = hasSchedule ? "schedule" : "webhook";
triggerStep.action = {
key,
component: {
key: `${key}-triggers`,
/**
* TODO: Add support for specific versions of platform triggers
*/
version: "LATEST",
isPublic: true,
},
};
}
let hasSchedule = false;
if ("schedule" in flow && typeof flow.schedule === "object") {
const { schedule } = flow;
triggerStep.schedule = {
type: "configVar" in schedule ? "configVar" : "value",
value: "configVar" in schedule ? schedule.configVar : schedule.value,
meta: {
scheduleType: "custom",
timeZone: (_a = schedule.timezone) !== null && _a !== void 0 ? _a : "",
},
};
result.schedule = undefined;
hasSchedule = true;
}
if (flow.triggerType === "polling" && !hasSchedule) {
throw new Error(`${flow.name} is marked as a polling trigger but has no schedule. Polling triggers require a schedule.`);
}
if ("queueConfig" in flow && typeof flow.queueConfig === "object") {
const queueConfig = (0, exports.convertQueueConfig)(flow.queueConfig);
if (hasSchedule && queueConfig.usesFifoQueue) {
throw new Error(`${flow.name} has a schedule & usesFifoQueue set to true. FIFO queues cannot be used with scheduled flows.`);
}
else if (!hasSchedule && queueConfig.singletonExecutions) {
throw new Error(`${flow.name} is configured for singletonExecutions but has no schedule. Unscheduled flows cannot be configured for singleton executions.`);
}
else if (queueConfig.usesFifoQueue && queueConfig.singletonExecutions) {
throw new Error(`${flow.name} is configured for both FIFO queues and singleton executions, but these options are mutually exclusive. Please choose one.`);
}
if (queueConfig.concurrencyLimit !== undefined &&
(queueConfig.concurrencyLimit < exports.CONCURRENCY_LIMIT_MIN ||
queueConfig.concurrencyLimit > exports.CONCURRENCY_LIMIT_MAX)) {
throw new Error(`${flow.name} has an invalid concurrencyLimit of ${queueConfig.concurrencyLimit}. concurrencyLimit must be between ${exports.CONCURRENCY_LIMIT_MIN} and ${exports.CONCURRENCY_LIMIT_MAX}.`);
}
result.queueConfig = Object.assign(Object.assign({ usesFifoQueue: false }, queueConfig), (queueConfig.dedupeIdField
? {
dedupeIdField: {
type: "reference",
value: `${triggerStep.name ? (0, camelCase_1.default)(triggerStep.name) : "onTrigger"}.results.${queueConfig.dedupeIdField}`,
},
}
: {}));
}
const actionStep = {
action: {
key: flowFunctionKey(flow.name, "onExecution"),
component: codeNativeIntegrationComponentReference(referenceKey),
},
name: "On Execution",
stableKey: `${flow.stableKey}-onExecution`,
description: "The function that will be executed by the flow.",
errorConfig: "errorConfig" in flow ? Object.assign({}, flow.errorConfig) : undefined,
};
result.steps = [triggerStep, actionStep];
result.supplementalComponents = convertComponentRegistry(componentRegistry, publicSupplementalComponent);
result.schemas = flow.schemas ? convertFlowSchemas(flow.stableKey, flow.schemas) : undefined;
return result;
};
exports.convertFlow = convertFlow;
/** Converts an input value to the expected server type by its collection type. */
const convertInputValue = (value, collectionType) => {
if (collectionType !== "keyvaluelist") {
return value;
}
if (Array.isArray(value)) {
return value;
}
return Object.entries(value).map(([key, value]) => ({
key,
value: typeof value === "string" ? value : JSON.stringify(value),
}));
};
exports.convertInputValue = convertInputValue;
const validateOnPremConnectionConfig = (connection) => {
if ((0, types_1.isConnectionDefinitionConfigVar)(connection)) {
const hasOnPremControlledInputs = Object.values(connection.inputs).some((value) => {
return "onPremControlled" in value && value.onPremControlled;
});
const { onPremConnectionConfig: config } = connection;
if (hasOnPremControlledInputs && !config) {
throw new Error(`Connection ${connection.stableKey} has onPremControlled inputs but no onPremConnectionConfig value set. Please set an onPremConnectionConfig value for the connection.`);
}
if (!hasOnPremControlledInputs && config && config !== "disallowed") {
throw new Error(`Connection ${connection.stableKey} has onPremConnectionConfig set but no onPremControlled inputs. The connection will not be valid without onPremControlled inputs (host, port).`);
}
return hasOnPremControlledInputs && config ? config : "disallowed";
}
return "disallowed";
};
/** Converts a Config Var into the structure necessary for YAML generation. */
const convertConfigVar = (key, configVar, referenceKey, componentRegistry) => {
var _a, _b, _c;
if ((0, types_1.isConnectionScopedConfigVar)(configVar)) {
const { stableKey } = (0, pick_1.default)(configVar, ["stableKey"]);
return {
key,
stableKey,
dataType: "connection",
useScopedConfigVar: stableKey,
};
}
const { orgOnly, meta } = convertConfigVarPermissionAndVisibility((0, pick_1.default)(configVar, ["permissionAndVisibilityType", "visibleToOrgDeployer"]));
if ((0, types_1.isConnectionDefinitionConfigVar)(configVar)) {
const { stableKey, description } = (0, pick_1.default)(configVar, ["stableKey", "description"]);
return {
stableKey,
description,
key,
dataType: "connection",
onPremiseConnectionConfig: validateOnPremConnectionConfig(configVar),
connection: {
key: (0, camelCase_1.default)(key),
component: codeNativeIntegrationComponentReference(referenceKey),
},
inputs: Object.entries(configVar.inputs).reduce((result, [key, input]) => {
// Connection template inputs are never shown in the resulting YAML.
if (input.shown === false || "templateValue" in input) {
return result;
}
const meta = convertInputPermissionAndVisibility((0, pick_1.default)(input, ["permissionAndVisibilityType", "visibleToOrgDeployer"]));
if (input.writeOnly) {
meta.writeOnly = input.writeOnly;
}
const defaultValue = input.collection
? (Array.isArray(input.default) ? input.default : []).map((defaultValue) => {
if (typeof defaultValue === "string") {
return {
type: "value",
value: defaultValue,
};
}
return {
name: defaultValue.key,
type: "value",
value: defaultValue.value,
};
})
: input.default || "";
return Object.assign(Object.assign({}, result), { [key]: {
type: input.collection ? "complex" : "value",
value: defaultValue,
meta,
} });
}, {}),
orgOnly,
meta: Object.assign(Object.assign({}, meta), ("oauth2Config" in configVar ? ((_a = configVar.oauth2Config) !== null && _a !== void 0 ? _a : {}) : {})),
};
}
if ((0, types_1.isConnectionReferenceConfigVar)(configVar)) {
const { ref, inputs } = convertComponentReference(configVar.connection, componentRegistry, "connections");
const { stableKey = "", description, connection: { template, onPremiseConnectionConfig }, } = (0, pick_1.default)(configVar, ["stableKey", "description", "connection"]);
return {
stableKey,
description,
key,
dataType: "connection",
onPremiseConnectionConfig,
connection: Object.assign(Object.assign({}, ref), { template }),
inputs,
orgOnly,
meta: Object.assign(Object.assign({}, meta), ("oauth2Config" in configVar ? ((_b = configVar.oauth2Config) !== null && _b !== void 0 ? _b : {}) : {})),
};
}
const rawDefaultValue = "defaultValue" in configVar
? (0, exports.convertInputValue)(configVar.defaultValue, configVar.collectionType)
: undefined;
const defaultValue = typeof rawDefaultValue !== "undefined"
? typeof rawDefaultValue === "string"
? rawDefaultValue
: JSON.stringify(rawDefaultValue)
: undefined;
const result = (0, assign_1.default)({ orgOnly, meta, key, defaultValue }, (0, pick_1.default)(configVar, [
"stableKey",
"description",
"dataType",
"pickList",
"timeZone",
"codeLanguage",
"collectionType",
]));
if ((0, types_1.isScheduleConfigVar)(configVar)) {
// Mirror the low-code options: callers may supply `scheduleType`
// explicitly ("none" / "minute" / "hour" / "day" / "week" / "custom").
// Otherwise infer from defaultValue: a non-empty string is treated as a
// custom CRON expression; missing/empty means "never".
if (configVar.scheduleType) {
result.scheduleType = configVar.scheduleType;
}
else if (typeof defaultValue === "string" && defaultValue.length > 0) {
result.scheduleType = "custom";
}
else {
result.scheduleType = "none";
}
}
if ((0, types_1.isJsonFormConfigVar)(configVar) || (0, types_1.isJsonFormDataSourceConfigVar)(configVar)) {
result.meta = Object.assign(Object.assign({}, result.meta), { validationMode: (_c = configVar === null || configVar === void 0 ? void 0 : configVar.validationMode) !== null && _c !== void 0 ? _c : "ValidateAndShow" });
}
if ((0, types_1.isJsonFormDataSourceConfigVar)(configVar) && configVar.dataSourceReset) {
result.meta = Object.assign(Object.assign({}, result.meta), { dataSourceReset: configVar.dataSourceReset.mode });
// Create placeholder inputs for each config variable dependency, so that
// the config wizard can detect if any changed and reset the data source.
result.inputs = (configVar.dataSourceReset.dependencies || []).reduce((acc, dep, idx) => (Object.assign(Object.assign({}, acc), { [`input${idx}`]: {
type: "configVar",
value: dep,
} })), {});
}
if ((0, types_1.isDataSourceDefinitionConfigVar)(configVar)) {
result.dataType = configVar.dataSourceType;
result.dataSource = {
key: (0, camelCase_1.default)(key),
component: codeNativeIntegrationComponentReference(referenceKey),
};
}
if ((0, types_1.isDataSourceReferenceConfigVar)(configVar)) {
const { ref, inputs } = convertComponentReference(configVar.dataSource, componentRegistry, "dataSources");
result.dataType =
componentRegistry[configVar.dataSource.component].dataSources[ref.key].dataSourceType;
result.dataSource = ref;
result.inputs = inputs;
if (configVar.validationMode) {
result.meta = Object.assign(Object.assign({}, result.meta), { validationMode: configVar.validationMode });
}
if (configVar.dataSourceReset) {
result.meta = Object.assign(Object.assign({}, result.meta), { dataSourceReset: configVar.dataSourceReset.mode });
}
}
return result;
};
exports.convertConfigVar = convertConfigVar;
/** Maps the step name field to a fully qualified input. */
const fieldNameToReferenceInput = (stepName, fieldName) => fieldName ? { type: "reference", value: `${stepName}.results.${fieldName}` } : undefined;
/** Actions and Triggers will be scoped to their flow by combining the flow
* name and the function name. This is to ensure that the keys are unique
* on the resulting object, which will be turned into a Component. */
const flowFunctionKey = (flowName, functionName) => {
const flowKey = flowName
.replace(/[^0-9a-zA-Z]+/g, " ")
.trim()
.split(" ")
.map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join("");
return `${flowKey}_${functionName}`;
};
/* Generates component argument for invokeTrigger calls. */
const invokeTriggerComponentInput = (componentRef, onTrigger, eventName) => {
const { component } = componentRef;
const inputComponent = "signature" in componentRef.component
? {
key: component.key,
signature: "signature" in component &&
component.signature !== null &&
component.signature !== void 0
? component.signature
: "",
isPublic: component.isPublic,
}
: component;
return {
component: inputComponent,
key: onTrigger ? onTrigger.key : componentRef.key,
triggerEventFunctionName: eventName,
};
};
exports.invokeTriggerComponentInput = invokeTriggerComponentInput;
/** Type guard to narrow trigger perform functions based on triggerType.
* Since TriggerPerformFunction and CodeNativePollingTriggerPerformFunction are
* structurally identical, TypeScript cannot distinguish them. This guard uses
* triggerType to narrow the function type. */
const isStandardTriggerPerform = (fn, triggerType) => triggerType !== "polling";
// Force incoming config into a discriminated union type to simplify downstream handling
function validateTriggerPerformConfig(params) {
const { componentRef, onTrigger, componentRegistry, triggerType } = params;
if (componentRef && onTrigger && typeof onTrigger !== "function") {
return {
componentRef,
onTrigger,
triggerType: "component-ref",
componentRegistry,
};
}
else if (triggerType === "polling" && typeof onTrigger === "function") {
return {
componentRef: undefined,
onTrigger,
triggerType,
componentRegistry,
};
}
else if (typeof onTrigger === "function" && isStandardTriggerPerform(onTrigger, triggerType)) {
return {
componentRef: undefined,
onTrigger,
triggerType: "standard",
componentRegistry,
};
}
else {
throw new Error(`Invalid trigger configuration detected: ${JSON.stringify(params, null, 2)}`);
}
}
/* Generates a wrapper function that calls an existing component trigger's perform. */
function generateTriggerPerformFn(params) {
const { componentRef, onTrigger, componentRegistry, triggerType } = validateTriggerPerformConfig(params);
switch (triggerType) {
case "polling":
return (0, perform_1.createCNIPollingPerform)({ onTrigger, componentRegistry });
case "standard":
return (0, perform_1.createCNIPerform)({ componentRegistry, onTrigger });
case "component-ref":
return (0, perform_1.createCNIComponentRefPerform)({ componentRegistry, componentRef, onTrigger });
default:
throw new Error(`Invalid trigger configuration detected: ${JSON.stringify(params, null, 2)}`);
}
}
/** Generates a wrapper function that calls an existing component's trigger event function
* (onInstanceDeploy, onInstanceDelete, webhookCreate, or webhookDelete), then calls
* the flow-defined version if it exists.
* Returns the deep-merged results of the two, prioritizing the custom response
* if there's a conflict. */
const generateTriggerEventWrapperFn = (componentRef, onTrigger, eventName, componentRegistry, customFn) => {
const usesComponentRef = componentRef && typeof onTrigger !== "function";
if (usesComponentRef) {
return (context, params) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
// @ts-expect-error: _components isn't part of the public API
const _components = (_a = context._components) !== null && _a !== void 0 ? _a : {
invokeTrigger: () => { },
};
const invokeTrigger = _components.invokeTrigger;
const cniContext = (0, context_1.createCNIContext)(context, componentRegistry);
// Using runWithContext allows for component action invocation via manifest.
return yield (0, asyncContext_1.runWithContext)(cniContext, () => __awaiter(void 0, void 0, void 0, function* () {
const invokeResponse = (yield invokeTrigger((0, exports.invokeTriggerComponentInput)(componentRef, onTrigger, eventName), cniContext, null, params)) || {};
let customResponse = {};
if (customFn) {
customResponse = (yield customFn(cniContext, params)) || {};
}
return (0, merge_1.default)(invokeResponse, customResponse);
}));
});
}
else if (customFn) {
return (context, params) => __awaiter(void 0, void 0, void 0, function* () {
const cniContext = (0, context_1.createCNIContext)(context, componentRegistry);
// Using runWithContext allows for component action invocation via manifest.
return yield (0, asyncContext_1.runWithContext)(cniContext, () => __awaiter(void 0, void 0, void 0, function* () {
return yield customFn(cniContext, params);
}));
});
}
else {
return;
}
};
const convertOnExecution = (onExecution, componentRegistry) => (context, params) => __awaiter(void 0, void 0, void 0, function* () {
const actionContext = (0, context_1.createCNIContext)(context, componentRegistry);
// Using runWithContext allows for component action invocation via manifest.
const result = yield (0, asyncContext_1.runWithContext)(actionContext, () => __awaiter(void 0, void 0, void 0, function* () {
return yield onExecution(actionContext, params);
}));
(0, context_1.logDebugResults)(actionContext);
return result;
});
/** Creates the structure necessary to import a Component as part of a
* Code Native integration. */
const codeNativeIntegrationComponent = ({ name, iconPath, description, flows = [], componentRegistry = {}, }, referenceKey, configVars) => {
const convertedActions = flows.reduce((result, { name, onExecution }) => {
const key = flowFunctionKey(name, "onExecution");
return Object.assign(Object.assign({}, result), { [key]: {
key,
display: {
label: `${name} - onExecution`,
description: "The function that will be executed by the flow.",
},
perform: convertOnExecution(onExecution, componentRegistry),
inputs: [],
} });
}, {});
const convertedTriggers = flows.reduce((result, { name, onTrigger, onInstanceDeploy, onInstanceDelete, webhookLifecycleHandlers, schedule, triggerType, }) => {
if (!flowUsesWrapperTrigger({
onTrigger,
onInstanceDelete,
onInstanceDeploy,
webhookLifecycleHandlers,
})) {
// In this scenario, the user has defined an existing component trigger
// without any custom behavior, so we don't need to wrap anything.
return result;
}
const key = flowFunctionKey(name, "onTrigger");
const defaultComponentKey = schedule && typeof schedule === "object" ? "schedule" : "webhook";
const defaultComponentRef = {
component: {
key: `${defaultComponentKey}-triggers`,
version: "LATEST",
isPublic: true,
},
key: defaultComponentKey,
};
// The component ref here is undefined if onTrigger is a function.
const { ref } = (0, types_1.isComponentReference)(onTrigger)
? convertComponentReference(onTrigger, componentRegistry, "triggers")
: { ref: onTrigger ? undefined : defaultComponentRef };
const performFn = generateTriggerPerformFn({
componentRef: ref,
onTrigger,
componentRegistry,
triggerType,
});
const deleteFn = generateTriggerEventWrapperFn(ref, onTrigger, "onInstanceDelete", componentRegistry, onInstanceDelete);
const deployFn = generateTriggerEventWrapperFn(ref, onTrigger, "onInstanceDeploy", componentRegistry, onInstanceDeploy);
const webhookCreateFn = generateTriggerEventWrapperFn(ref, onTrigger, "webhookCreate", componentRegistry, webhookLifecycleHandlers === null || webhookLifecycleHandlers === void 0 ? void 0 : webhookLifecycleHandlers.create);
const webhookDeleteFn = generateTriggerEventWrapperFn(ref, onTrigger, "webhookDelete", componentRegistry, webhookLifecycleHandlers === null || webhookLifecycleHandlers === void 0 ? void 0 : webhookLifecycleHandlers.delete);
return Object.assign(Object.assign({}, result), { [key]: {
key,
display: {
label: `${name} - onTrigger`,
description: "The function that will be executed by the flow to return an HTTP response.",
},
perform: performFn,
onInstanceDeploy: deployFn,
hasOnInstanceDeploy: !!deployFn,
onInstanceDelete: deleteFn,
hasOnInstanceDelete: !!deleteFn,
webhookCreate: webhookCreateFn,
hasWebhookCreateFunction: !!webhookCreateFn,
webhookDelete: webhookDeleteFn,
hasWebhookDeleteFunction: !!webhookDeleteFn,
inputs: [],
scheduleSupport: triggerType === "polling" ? "required" : "valid",
synchronousResponseSupport: "valid",
isPollingTrigger: triggerType === "polling",
} });
}, {});
const convertedDataSources = Object.entries(configVars).reduce((result, [key, configVar]) => {
if (!(0, types_1.isDataSourceDefinitionConfigVar)(configVar)) {
return result;
}
const camelKey = (0, camelCase_1.default)(key);
const dataSource = (0, pick_1.default)(configVar, ["perform", "dataSourceType"]);
return Object.assign(Object.assign({}, result), { [camelKey]: Object.assign(Object.assign({}, dataSource), { key: camelKey, display: {
label: key,
description: key,
}, inputs:
// Create placeholder inputs for each config variable dependency,so that
// the config wizard can detect if any changed and reset the data source.
(0, types_1.isJsonFormDataSourceConfigVar)(configVar) && configVar.dataSourceReset
? (configVar.dataSourceReset.dependencies || []).map((dep, idx) => ({
key: `input${idx}`,
label: dep,
type: "string",
}))
: [] }) });
}, {});
const convertedConnections = Object.entries(configVars).reduce((result, [key, configVar]) => {
var _a;
if (!(0, types_1.isConnectionDefinitionConfigVar)(configVar)) {
return result;
}
const convertedInputs = Object.entries(configVar.inputs).map(([key, value]) => {
if ("templateValue" in value) {
return (0, convertComponent_1.convertTemplateInput)(key, value, configVar.inputs);
}
return (0, convertComponent_1.convertInput)(key, value);
});
const connection = (0, pick_1.default)(configVar, ["oauth2Type", "oauth2PkceMethod"]);
const { avatarPath: avatarIconPath, oauth2ConnectionIconPath: iconPath } = (_a = configVar.icons) !== null && _a !== void 0 ? _a : {};
return [
...result,
Object.assign(Object.assign({}, connection), { iconPath,
avatarIconPath, inputs: convertedInputs, key: (0, camelCase_1.default)(key), label: key }),
];
}, []);
return {
key: referenceKey,
display: {
label: referenceKey,
iconPath,
description: description || name,
},
connections: convertedConnections,
actions: convertedActions,
triggers: convertedTriggers,
dataSources: convertedDataSources,
};
};
const codeNativeIntegrationPublishingMetadata = (definition) => {
const customerRequiredSecurityEndpoints = definition.flows
.filter((flow) => flow.endpointSecurityType === "customer_required")
.map(({ name, testApiKeys }) => {
return { name, testApiKeys };
});
return {
flowsWithCustomerRequiredAPIKeys: customerRequiredSecurityEndpoints,
};
};