@prismatic-io/spectral
Version:
Utility library for building Prismatic connectors and code-native integrations
918 lines (917 loc) • 54.4 kB
JavaScript
"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 __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.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;
/**
* Default `resolveItems`: a batched `trigger`'s fires return their records under
* `payload.body.data` (the wrapper {@link normalizeBatchedFlow} builds writes them there),
* so extraction is just reading that array back. Authors never write this themselves.
*/
const defaultResolveItems = (_context, result) => result.payload.body.data;
/**
* Default `getNextPaginationState`: a batched fire returns the next page's cursor as
* `paginationState`, which the wrapper {@link normalizeBatchedFlow} builds stamps onto
* `payload.paginationState`. Reading it back (defaulting to `null`) is the whole loop: a
* non-null value re-invokes the fire, `null` ends it. Authors never write this.
*/
const defaultGetNextPaginationState = (_context, result) => { var _a; return (_a = result.payload.paginationState) !== null && _a !== void 0 ? _a : null; };
/**
* Expands a flow's batched `trigger` (built with `batchFlowTrigger`) into the flat
* `onTrigger`/`onDeployTrigger`/`triggerResolver`/`onDeployResolver` shape the rest of the
* conversion pipeline already understands. The trigger fires return `{ items, paginationState? }`;
* here we wrap each into a `TriggerPerformFunction` that emits `{ payload: { …payload, body: {
* data: items }, paginationState } }`, then synthesize the default `resolveItems` (reads the
* items back) and `getNextPaginationState` (reads the cursor back). Flows without a `trigger`
* pass through unchanged.
*
* Returns the same `Flow` type it received; the synthesized `triggerResolver`/`onDeployResolver`
* are wire-only fields (not on the author-facing `Flow`), read downstream via `"x" in flow` checks.
*/
const normalizeBatchedFlow = (flow) => {
const trigger = "trigger" in flow ? flow.trigger : undefined;
if (!trigger) {
return flow;
}
const { onTrigger, onDeploy } = trigger;
// Wrap a batched fire (returns `{ items, paginationState?, response? }`) into a
// TriggerPerformFunction that emits the wire payload shape: items at `body.data` and the
// next-page cursor at `paginationState` (defaulting `null` to terminate the loop). The
// incoming payload's `paginationState` was already consumed by the fire, so overwriting it
// with the returned cursor is safe — `getNextPaginationState` reads it straight back.
const wrapFire = (fire) => (context, payload) => __awaiter(void 0, void 0, void 0, function* () {
const { items, paginationState, response } = yield fire(context, payload);
return Object.assign({ payload: Object.assign(Object.assign({}, payload), { body: { data: items, contentType: "application/json" }, paginationState: paginationState !== null && paginationState !== void 0 ? paginationState : null }) }, (response ? { response } : {}));
});
const _a = flow, { trigger: _omitTrigger } = _a, rest = __rest(_a, ["trigger"]);
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, rest), { onTrigger: wrapFire(onTrigger) }), (onDeploy ? { onDeployTrigger: wrapFire(onDeploy) } : {})), { triggerResolver: {
resolveItems: defaultResolveItems,
getNextPaginationState: defaultGetNextPaginationState,
} }), (onDeploy
? {
onDeployResolver: {
resolveItems: defaultResolveItems,
getNextPaginationState: defaultGetNextPaginationState,
},
}
: {}));
};
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: rawFlows, configPages, userLevelConfigPages, scopedConfigVars, instanceProfile, componentRegistry = {}, }, referenceKey, configVars, metadata) => {
// Expand any batched `trigger` (built with `batchFlowTrigger`) into the flat
// onTrigger/onDeployTrigger/triggerResolver/onDeployResolver shape the rest of this
// pipeline (convertFlow + the trigger reducer) already handles.
const flows = rawFlows.map((flow) => normalizeBatchedFlow(flow));
// 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 = (rawFlow, componentRegistry, referenceKey) => {
var _a;
// Expand a batched `trigger` into the flat shape this function serializes. Idempotent: a flow
// that already lacks `trigger` (including one pre-normalized by `convertIntegration`) is returned as-is.
const flow = normalizeBatchedFlow(rawFlow);
const result = Object.assign({}, flow);
result.onTrigger = undefined;
result.trigger = undefined;
result.onInstanceDeploy = undefined;
result.onInstanceDelete = undefined;
result.webhookLifecycleHandlers = undefined;
result.onExecution = undefined;
result.onDeployTrigger = 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";
}
// The step action points at the CNI-generated wrapper trigger rather than
// the referenced component's trigger. When the flow references a component
// trigger, still carry that reference's configured input values onto the
// step so the wrapper trigger receives them as params (and can forward them
// to the referenced trigger via invokeTrigger).
if ((0, types_1.isComponentReference)(flow.onTrigger)) {
const { inputs } = convertComponentReference(flow.onTrigger, componentRegistry, "triggers");
triggerStep.inputs = inputs;
}
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 triggerResolver = "triggerResolver" in flow ? flow.triggerResolver : undefined;
const onDeployResolver = "onDeployResolver" in flow ? flow.onDeployResolver : undefined;
const batchConfig = "batchConfig" in flow ? flow.batchConfig : undefined;
// Resolver behaviors (resolveItems/getNextPaginationState) are serialized onto the
// synthesized trigger below. On the flow wire we emit only `triggerResolver`, the single
// config the platform reads (`trigger_resolver_batch_size` / `trigger_resolver_enabled`)
// and shares between the normal and on-deploy fires. `batchConfig`/`onDeployResolver` are
// author-side only — clear them out of the `{ ...flow }` spread.
result.triggerResolver = undefined;
result.onDeployResolver = undefined;
result.batchConfig = undefined;
if (triggerResolver || onDeployResolver) {
if (!batchConfig) {
throw new Error(`${flow.name} defines a triggerResolver/onDeployResolver but no batchConfig. Add \`batchConfig: { batchSize }\` to the flow.`);
}
if (onDeployResolver &&
(!("onDeployTrigger" in flow) || typeof flow.onDeployTrigger !== "function")) {
throw new Error(`${flow.name} declares onDeployResolver without onDeployTrigger. Set onDeployTrigger to handle the initial-deploy fire that the resolver fans out.`);
}
// `enabled: true` is required: for a "valid"-support trigger (which CNI synthesized
// triggers are) the platform only batches when the flow's resolver is enabled.
const concurrentBatchLimit = (0, convertComponent_1.validateConcurrentBatchLimit)(flow.name, "batchConfig", batchConfig.concurrentBatchLimit);
result.triggerResolver = Object.assign({ batchSize: (0, convertComponent_1.validateBatchSize)(flow.name, "batchConfig", batchConfig.batchSize), enabled: true }, (concurrentBatchLimit !== undefined ? { concurrentBatchLimit } : {}));
}
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;
/* When a flow references a component trigger but also defines on* lifecycle
* behavior, the trigger is wrapped in a generated CNI component trigger. That
* wrapper must declare the referenced trigger's inputs so the platform passes
* the step's configured values through as params (which the wrapper then
* forwards to the referenced trigger via invokeTrigger). */
const wrapperTriggerInputsFromReference = (onTrigger, componentRegistry) => {
var _a, _b, _c;
if (!(0, types_1.isComponentReference)(onTrigger)) {
return [];
}
const manifestInputs = (_c = (_b = (_a = componentRegistry[onTrigger.component]) === null || _a === void 0 ? void 0 : _a.triggers) === null || _b === void 0 ? void 0 : _b[onTrigger.key]) === null || _c === void 0 ? void 0 : _c.inputs;
if (!manifestInputs) {
return [];
}
return Object.entries(manifestInputs).map(([key, input]) => (Object.assign(Object.assign(Object.assign({ key, label: key, type: input.inputType }, (input.collection ? { collection: input.collection } : {})), (input.default !== undefined ? { default: input.default } : {})), (input.required !== undefined ? { required: input.required } : {}))));
};
/** 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: rawFlows = [], componentRegistry = {}, }, referenceKey, configVars) => {
// Expand any batched `trigger` so the action/trigger reducers below see the flat shape.
const flows = rawFlows.map((flow) => normalizeBatchedFlow(flow));
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, flow) => {
var _a;
const { name, onTrigger, onInstanceDeploy, onInstanceDelete, webhookLifecycleHandlers, schedule, triggerType, onDeployTrigger, } = flow;
// `batchConfig`/`triggerResolver`/`onDeployResolver` are wire-only fields synthesized by
// `normalizeBatchedFlow`; they aren't on the author-facing `Flow` type, so read via cast.
const { batchConfig, triggerResolver, onDeployResolver } = flow;
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]: Object.assign(Object.assign(Object.assign(Object.assign({ 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: wrapperTriggerInputsFromReference(onTrigger, componentRegistry), scheduleSupport: triggerType === "polling" ? "required" : "valid", synchronousResponseSupport: "valid", isPollingTrigger: triggerType === "polling", triggerResolverSupport: triggerResolver ? "valid" : "invalid" }, (triggerResolver || onDeployResolver
? Object.assign({ triggerResolverDefaultBatchSize: (_a = batchConfig === null || batchConfig === void 0 ? void 0 : batchConfig.batchSize) !== null && _a !== void 0 ? _a : 1 }, ((batchConfig === null || batchConfig === void 0 ? void 0 : batchConfig.concurrentBatchLimit) !== undefined
? {
triggerResolverDefaultConcurrentBatchLimit: batchConfig.concurrentBatchLimit,
}
: {})) : {})), (triggerResolver