UNPKG

@prismatic-io/spectral

Version:

Utility library for building Prismatic connectors and code-native integrations

860 lines (859 loc) 43.6 kB
"use strict"; 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, }; };