@prismatic-io/spectral
Version:
Utility library for building Prismatic connectors and code-native integrations
375 lines (374 loc) • 21.7 kB
JavaScript
;
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.convertComponent = exports.convertConnection = exports.convertTrigger = exports.convertTemplateInput = exports._isValidTemplateValue = exports.convertInput = exports.validateConcurrentBatchLimit = exports.validateBatchSize = exports.cleanerFor = void 0;
const omit_1 = __importDefault(require("lodash/omit"));
const types_1 = require("../types");
const PollingTriggerDefinition_1 = require("../types/PollingTriggerDefinition");
const perform_1 = require("./perform");
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
/** Auto-generated cleaner for structuredObject/dynamicObject containers.
* Recursively delegates to each child's clean function. Developers do not
* declare a top-level clean on these containers — the conversion always
* supplies one so nested clean functions are applied at runtime. */
const cleanerFor = (input) => {
if (input.type === "structuredObject") {
const childCleaners = Object.entries(input.inputs).reduce((acc, [childKey, childDef]) => (Object.assign(Object.assign({}, acc), { [childKey]: (0, exports.cleanerFor)(childDef) })), {});
const cleanRecord = (value) => isPlainObject(value) ? (0, perform_1.cleanParams)(value, childCleaners) : value;
if (input.collection === "valuelist") {
return (value) => (Array.isArray(value) ? value.map(cleanRecord) : value);
}
if (input.collection === "keyvaluelist") {
// Entries arrive as KeyValuePair envelopes; the object to clean
// lives under `value`.
return (value) => Array.isArray(value)
? value.map((entry) => isPlainObject(entry) && "value" in entry
? Object.assign(Object.assign({}, entry), { value: cleanRecord(entry.value) }) : entry)
: value;
}
return cleanRecord;
}
if (input.type === "dynamicObject") {
const configCleaners = {};
for (const [configKey, configDef] of Object.entries(input.configurations)) {
configCleaners[configKey] = Object.entries(configDef.inputs).reduce((acc, [childKey, childDef]) => (Object.assign(Object.assign({}, acc), { [childKey]: (0, exports.cleanerFor)(childDef) })), {});
}
return (value) => {
if (!isPlainObject(value)) {
return value;
}
const { configuration, values } = value;
if (typeof configuration !== "string") {
return value;
}
const cleaners = configCleaners[configuration];
if (!cleaners) {
return { configuration, values };
}
return {
configuration,
values: isPlainObject(values) ? (0, perform_1.cleanParams)(values, cleaners) : values,
};
};
}
return "clean" in input ? input.clean : undefined;
};
exports.cleanerFor = cleanerFor;
/**
* Throws if `batchSize` isn't a positive integer; otherwise returns it. Shared by the
* component-trigger (`TriggerDefinition.batch.batchSize`) and CNI flow (`flow.batch.batchSize`)
* validation paths.
*/
const validateBatchSize = (ownerLabel, fieldName, batchSize) => {
if (typeof batchSize !== "number" || !Number.isInteger(batchSize) || batchSize < 1) {
throw new Error(`${ownerLabel} has an invalid ${fieldName} batchSize of ${String(batchSize)}. batchSize must be an integer >= 1.`);
}
return batchSize;
};
exports.validateBatchSize = validateBatchSize;
/**
* Throws if `concurrentBatchLimit` is set but isn't a positive integer; returns it
* unchanged (including `undefined`, which the platform treats as unlimited). Shared by the
* component-trigger and CNI flow paths, both sourcing it from the single `batchConfig`.
*/
const validateConcurrentBatchLimit = (ownerLabel, fieldName, concurrentBatchLimit) => {
if (concurrentBatchLimit === undefined) {
return undefined;
}
if (typeof concurrentBatchLimit !== "number" ||
!Number.isInteger(concurrentBatchLimit) ||
concurrentBatchLimit < 1) {
throw new Error(`${ownerLabel} has an invalid ${fieldName} concurrentBatchLimit of ${String(concurrentBatchLimit)}. concurrentBatchLimit must be an integer >= 1.`);
}
return concurrentBatchLimit;
};
exports.validateConcurrentBatchLimit = validateConcurrentBatchLimit;
/**
* Emits the trigger's single default batch size to the one wire field the platform reads
* (`triggerResolverDefaultBatchSize`), shared by both the trigger and on-deploy resolution.
* Emitted when the trigger declares a resolver — `triggerResolverSupport` `"valid"`/`"required"`
* for the normal path, or an `onDeployResolver` for the on-deploy path. Defaults to 1 when no
* `batchConfig` was declared.
*/
const buildBatchDefaultField = (triggerLabel, triggerResolverSupport, hasOnDeployResolver, batchConfig) => {
if (triggerResolverSupport === "invalid" && !hasOnDeployResolver) {
return {};
}
const concurrentBatchLimit = batchConfig
? (0, exports.validateConcurrentBatchLimit)(`Trigger "${triggerLabel}"`, "batchConfig", batchConfig.concurrentBatchLimit)
: undefined;
return Object.assign({ triggerResolverDefaultBatchSize: batchConfig
? (0, exports.validateBatchSize)(`Trigger "${triggerLabel}"`, "batchConfig", batchConfig.batchSize)
: 1 }, (concurrentBatchLimit !== undefined
? { triggerResolverDefaultConcurrentBatchLimit: concurrentBatchLimit }
: {}));
};
const buildTriggerResolverFields = (resolver) => {
if (!resolver) {
return {};
}
const { resolveItems, getNextPaginationState } = resolver;
return Object.assign(Object.assign({}, (resolveItems
? {
resolveTriggerItems: resolveItems,
hasResolveTriggerItems: true,
}
: {})), (getNextPaginationState
? {
getNextPaginationState,
hasGetNextDiscoveryState: true,
}
: {}));
};
const buildOnDeployResolverFields = (resolver) => {
if (!resolver) {
return {};
}
const { resolveItems, getNextPaginationState } = resolver;
return Object.assign(Object.assign({}, (resolveItems
? {
resolveOnDeployItems: resolveItems,
hasResolveOnDeployItems: true,
}
: {})), (getNextPaginationState
? {
getOnDeployNextPaginationState: getNextPaginationState,
hasGetOnDeployNextDiscoveryState: true,
}
: {}));
};
const convertInput = (key, definition) => {
// Cast: the field union is wider than any single member; runtime guards below handle it.
const _a = definition, { default: defaultValue, type, label, collection, inputs: childInputs, configurations } = _a, rest = __rest(_a, ["default", "type", "label", "collection", "inputs", "configurations"]);
const keyLabel = collection === "keyvaluelist" && typeof label === "object" ? label.key : undefined;
const nestedInputs = type === "structuredObject" && childInputs
? Object.entries(childInputs).map(([childKey, childDef]) => (0, exports.convertInput)(childKey, childDef))
: type === "dynamicObject" && configurations
? Object.entries(configurations).map(([configKey, configDef]) => ({
key: configKey,
type: "structuredObject",
label: typeof configDef.label === "string" ? configDef.label : configDef.label.value,
comments: configDef.comments,
inputs: Object.entries(configDef.inputs).map(([childKey, childDef]) => (0, exports.convertInput)(childKey, childDef)),
}))
: undefined;
return Object.assign(Object.assign({}, (0, omit_1.default)(rest, [
"onPremControlled",
"permissionAndVisibilityType",
"visibleToOrgDeployer",
"writeOnly",
])), { key,
type, default: defaultValue !== null && defaultValue !== void 0 ? defaultValue : types_1.InputFieldDefaultMap[type], collection, label: typeof label === "string" ? label : label.value, keyLabel, onPremiseControlled: rest.onPremControlled === true ? true : undefined, inputs: nestedInputs });
};
exports.convertInput = convertInput;
const TEMPLATE_VALUE_REGEX = /{{#(\w+)}}/g;
const TEMPLATE_VALUE_ERRORS = {
NO_SLOTS: "No template slots were found. Declare a template slot with this notation: {{#someInputKey}}",
INVALID_KEYS: "Invalid keys were found in the template string. All referenced keys must be non-template inputs declared in the first argument:",
};
const _isValidTemplateValue = (template, inputs) => {
const matches = [...template.matchAll(TEMPLATE_VALUE_REGEX)];
if (matches.length === 0) {
return {
isValid: false,
error: TEMPLATE_VALUE_ERRORS.NO_SLOTS,
};
}
const invalidKeys = [];
for (const [_substr, key] of matches) {
if (!inputs[key] || inputs[key].type === "template") {
invalidKeys.push(key);
}
}
if (invalidKeys.length > 0) {
return {
isValid: false,
error: `${TEMPLATE_VALUE_ERRORS.INVALID_KEYS} ${invalidKeys}`,
};
}
return {
isValid: true,
};
};
exports._isValidTemplateValue = _isValidTemplateValue;
const convertTemplateInput = (key, _a, inputs) => {
var { templateValue, label } = _a, rest = __rest(_a, ["templateValue", "label"]);
const validation = (0, exports._isValidTemplateValue)(templateValue, inputs);
if (!validation.isValid) {
throw `Template input "${key}": ${validation.error}`;
}
return Object.assign(Object.assign({}, (0, omit_1.default)(rest, ["permissionAndVisibilityType", "visibleToOrgDeployer", "writeOnly"])), { key, type: "template", default: templateValue !== null && templateValue !== void 0 ? templateValue : "", label: typeof label === "string" ? label : label.value, shown: false });
};
exports.convertTemplateInput = convertTemplateInput;
const convertOutputSchema = (outputSchema) => {
if (outputSchema.type === "actionOutput") {
return { type: "actionOutput", schema: JSON.stringify(outputSchema.schema) };
}
return {
type: "branchingOutput",
branchSchemas: Object.entries(outputSchema.branchSchemas).map(([name, schema]) => ({
name,
schema: JSON.stringify(schema),
})),
};
};
const convertAction = (actionKey, _a, hooks) => {
var { inputs = {}, perform, outputSchema } = _a, action = __rest(_a, ["inputs", "perform", "outputSchema"]);
const convertedInputs = Object.entries(inputs).map(([key, value]) => (0, exports.convertInput)(key, value));
const inputCleaners = Object.entries(inputs).reduce((result, [key, value]) => (Object.assign(Object.assign({}, result), { [key]: (0, exports.cleanerFor)(value) })), {});
return Object.assign(Object.assign(Object.assign({}, action), { key: actionKey, inputs: convertedInputs, perform: (0, perform_1.createPerform)(perform, {
inputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
}) }), (outputSchema ? { outputSchema: convertOutputSchema(outputSchema) } : {}));
};
const convertTrigger = (triggerKey,
// `any` is load-bearing: the user-facing TriggerDefinition / PollingTriggerDefinition
// type their event-function fields (onInstanceDeploy, webhookLifecycleHandlers, etc.) over
// TInputs/TConfigVars/TPayload, while the wire-format ServerTrigger drops those generics.
// The `...trigger` spread in the result construction below would surface variance errors
// without these `any`s. The user-typed handlers are immediately replaced with
// createPerform-wrapped versions, so the loose input typing is safe in practice.
trigger, hooks) => {
var _a;
const { onInstanceDeploy, onInstanceDelete } = trigger;
const webhookLifecycleHandlers = "webhookLifecycleHandlers" in trigger ? trigger.webhookLifecycleHandlers : undefined;
const inputs = (_a = trigger.inputs) !== null && _a !== void 0 ? _a : {};
const triggerInputKeys = Object.keys(inputs);
const convertedTriggerInputs = Object.entries(inputs).map(([key, value]) => {
return (0, exports.convertInput)(key, value);
});
const triggerInputCleaners = Object.entries(inputs).reduce((result, [key, value]) => (Object.assign(Object.assign({}, result), { [key]: (0, exports.cleanerFor)(value) })), {});
let scheduleSupport = "scheduleSupport" in trigger ? trigger.scheduleSupport : "invalid";
const batchConfig = "batchConfig" in trigger ? trigger.batchConfig : undefined;
const triggerResolver = "triggerResolver" in trigger ? trigger.triggerResolver : undefined;
const triggerResolverSupport = "triggerResolverSupport" in trigger && trigger.triggerResolverSupport !== undefined
? trigger.triggerResolverSupport
: triggerResolver
? "valid"
: "invalid";
if (triggerResolverSupport === "required" && !triggerResolver) {
throw new Error(`Trigger "${trigger.display.label}" declares triggerResolverSupport "required" but is missing triggerResolver.`);
}
if (triggerResolverSupport === "invalid" && triggerResolver) {
throw new Error(`Trigger "${trigger.display.label}" declares triggerResolver but triggerResolverSupport is "invalid".`);
}
const onDeployPerform = "onDeployPerform" in trigger ? trigger.onDeployPerform : undefined;
const onDeployResolver = "onDeployResolver" in trigger ? trigger.onDeployResolver : undefined;
// On-deploy is presence-driven (no support flag): a trigger that defines an
// `onDeployResolver` must also define the `onDeployPerform` fire it batches.
if ((onDeployResolver === null || onDeployResolver === void 0 ? void 0 : onDeployResolver.resolveItems) && !onDeployPerform) {
throw new Error(`Trigger "${trigger.display.label}" declares onDeployResolver.resolveItems but is missing onDeployPerform.`);
}
let convertedActionInputs = [];
let performToUse;
if ((0, PollingTriggerDefinition_1.isPollingTriggerDefinition)(trigger)) {
const { pollAction: action } = trigger;
let actionInputCleaners = {};
scheduleSupport = "required";
if (action) {
convertedActionInputs = Object.entries(action.inputs).reduce((accum, [key, value]) => {
if (triggerInputKeys.includes(key)) {
throw new Error(`The pollingTrigger "${trigger.display.label}" was defined with an input with the key: ${key}. This key duplicates an input on the associated "${action.display.label}" action. Please assign the trigger input a different key.`);
}
accum.push((0, exports.convertInput)(key, value));
return accum;
}, []);
actionInputCleaners = Object.entries(action.inputs).reduce((result, [key, value]) => (Object.assign(Object.assign({}, result), { [key]: (0, exports.cleanerFor)(value) })), {});
}
const combinedCleaners = Object.assign({}, actionInputCleaners, triggerInputCleaners);
performToUse = (0, perform_1.createPollingPerform)(trigger, {
inputCleaners: combinedCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
}
else {
performToUse = (0, perform_1.createPerform)(trigger.perform, {
inputCleaners: triggerInputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
}
const result = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (0, omit_1.default)(trigger, ["batchConfig", "triggerResolver", "onDeployResolver"])), { key: triggerKey, inputs: convertedTriggerInputs.concat(convertedActionInputs), perform: performToUse, scheduleSupport, synchronousResponseSupport: "synchronousResponseSupport" in trigger
? trigger.synchronousResponseSupport
: scheduleSupport === "invalid"
? "valid"
: "invalid", triggerResolverSupport }), buildBatchDefaultField(trigger.display.label, triggerResolverSupport, !!(onDeployResolver === null || onDeployResolver === void 0 ? void 0 : onDeployResolver.resolveItems), batchConfig)), buildTriggerResolverFields(triggerResolver)), buildOnDeployResolverFields(onDeployResolver)), ((0, PollingTriggerDefinition_1.isPollingTriggerDefinition)(trigger) ? { isPollingTrigger: true } : {}));
if (onInstanceDeploy) {
result.onInstanceDeploy = (0, perform_1.createPerform)(onInstanceDeploy, {
inputCleaners: triggerInputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
result.hasOnInstanceDeploy = true;
}
if (onDeployPerform) {
result.onDeployPerform = (0, perform_1.createPerform)(onDeployPerform, {
inputCleaners: triggerInputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
result.hasOnDeployPerform = true;
}
if (onInstanceDelete) {
result.onInstanceDelete = (0, perform_1.createPerform)(onInstanceDelete, {
inputCleaners: triggerInputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
result.hasOnInstanceDelete = true;
}
if (webhookLifecycleHandlers) {
result.webhookCreate = (0, perform_1.createPerform)(webhookLifecycleHandlers.create, {
inputCleaners: triggerInputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
result.webhookDelete = (0, perform_1.createPerform)(webhookLifecycleHandlers.delete, {
inputCleaners: triggerInputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
});
result.hasWebhookCreateFunction = true;
result.hasWebhookDeleteFunction = true;
}
const { pollAction, triggerType, webhookLifecycleHandlers: _ } = result, resultTrigger = __rest(result, ["pollAction", "triggerType", "webhookLifecycleHandlers"]);
return resultTrigger;
};
exports.convertTrigger = convertTrigger;
const convertDataSource = (dataSourceKey, _a, hooks) => {
var { inputs = {}, perform } = _a, dataSource = __rest(_a, ["inputs", "perform"]);
const convertedInputs = Object.entries(inputs).map(([key, value]) => (0, exports.convertInput)(key, value));
const inputCleaners = Object.entries(inputs).reduce((result, [key, value]) => (Object.assign(Object.assign({}, result), { [key]: (0, exports.cleanerFor)(value) })), {});
return Object.assign(Object.assign({}, dataSource), { key: dataSourceKey, inputs: convertedInputs, perform: (0, perform_1.createPerform)(perform, {
inputCleaners,
errorHandler: hooks === null || hooks === void 0 ? void 0 : hooks.error,
}) });
};
const convertConnection = (_a) => {
var { inputs = {} } = _a, connection = __rest(_a, ["inputs"]);
const { display: { label, icons, description: comments } } = connection, remaining = __rest(connection, ["display"]);
const convertedInputs = Object.entries(inputs).map(([key, value]) => {
if ("templateValue" in value) {
return (0, exports.convertTemplateInput)(key, value, inputs);
}
return (0, exports.convertInput)(key, value);
});
return Object.assign(Object.assign({}, remaining), { label,
comments, iconPath: icons === null || icons === void 0 ? void 0 : icons.oauth2ConnectionIconPath, avatarIconPath: icons === null || icons === void 0 ? void 0 : icons.avatarPath, inputs: convertedInputs });
};
exports.convertConnection = convertConnection;
const convertComponent = (_a) => {
var { connections = [], actions = {}, triggers = {}, dataSources = {}, hooks } = _a, definition = __rest(_a, ["connections", "actions", "triggers", "dataSources", "hooks"]);
const convertedActions = Object.entries(actions).reduce((result, [actionKey, action]) => (Object.assign(Object.assign({}, result), { [actionKey]: convertAction(actionKey, action, hooks) })), {});
const convertedTriggers = Object.entries(triggers).reduce((result, [triggerKey, trigger]) => (Object.assign(Object.assign({}, result), { [triggerKey]: (0, exports.convertTrigger)(triggerKey, trigger, hooks) })), {});
const convertedDataSources = Object.entries(dataSources).reduce((result, [dataSourceKey, dataSource]) => (Object.assign(Object.assign({}, result), { [dataSourceKey]: convertDataSource(dataSourceKey, dataSource, hooks) })), {});
return Object.assign(Object.assign({}, definition), { connections: connections.map(exports.convertConnection), actions: convertedActions, triggers: convertedTriggers, dataSources: convertedDataSources });
};
exports.convertComponent = convertComponent;