UNPKG

@prismatic-io/spectral

Version:

Utility library for building Prismatic connectors and code-native integrations

375 lines (374 loc) 21.7 kB
"use strict"; 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;