/** * @node workflow-input * @name Workflow Input * @category triggers * @version 1.0.0 * @description Start a workflow that another workflow calls, and say what it expects to be given * @trigger * @icon log-in */ const configSchema = { type: 'object', // The names below are what this node produces, so the editor can offer // them to later nodes as soon as they are typed - without waiting for a run. outputsFromConfig: { from: 'fields', nameKey: 'name' }, properties: { fields: { type: 'array', title: 'Expected Input', description: 'What the calling workflow must or may pass. A caller that leaves out a required one is refused here rather than failing somewhere deeper', items: { type: 'object', properties: { name: { type: 'string', title: 'Name', description: 'Key the caller passes, such as city' }, required: { type: 'boolean', title: 'Required', description: 'Refuse the call when this is missing', default: true }, description: { type: 'string', title: 'Description', description: 'What the caller should put here' }, defaultValue: { type: 'string', title: 'Default', description: 'Used when an optional key is not passed' } } } }, allowExtra: { type: 'boolean', title: 'Allow Anything Else', description: 'Pass through keys that are not listed above. Turn off to keep the contract exact and catch a caller sending the wrong thing', default: true } } }; const inputSchema = { type: 'object', properties: { data: { type: 'any' } } }; const outputSchema = { type: 'object', properties: { calledBy: { type: 'string', description: 'Execution id of the workflow that called this one, empty when run directly' } } }; async function execute(config, input, context) { // The caller's input arrives as the trigger data, the same way a webhook // body does. Internal keys the engine put there are not part of it. const given = (input && typeof input === 'object') ? input : {}; const fields = config.fields || []; const result = {}; const missing = []; for (let i = 0; i < fields.length; i++) { const field = fields[i] || {}; const name = String(field.name || '').trim(); if (!name) { // A row with a description or a default but no name is a filled-in // row whose key went in the wrong box. Skipping it silently meant // the caller's value was never accepted and the failure surfaced // somewhere else entirely - a node further on complaining that // something it needed was missing. const described = String(field.description || '').trim(); const fallback = String(field.defaultValue || '').trim(); if (described || fallback || field.required === true) { throw new Error('Workflow Input: an expected input has no name' + (described ? ' (described as "' + described + '")' : '') + (fallback ? ', default "' + fallback + '"' : '') + '. Put the key the caller passes in the Name box'); } continue; } const has = Object.prototype.hasOwnProperty.call(given, name) && given[name] !== undefined && given[name] !== null && given[name] !== ''; if (has) { result[name] = given[name]; } else if (field.required !== false) { missing.push(name); } else if (field.defaultValue !== undefined && field.defaultValue !== '') { result[name] = field.defaultValue; } } // Refusing here, naming every missing key at once, beats failing three // nodes later on an undefined that has to be traced back. if (missing.length > 0) { throw new Error('Workflow Input: the caller did not pass ' + missing.join(', ') + '. ' + (missing.length === 1 ? 'It is' : 'They are') + ' marked required on this node'); } if (config.allowExtra !== false) { const keys = Object.keys(given); for (let i = 0; i < keys.length; i++) { const key = keys[i]; // Engine bookkeeping, not part of what the caller sent. if (key.charAt(0) === '_') continue; if (!Object.prototype.hasOwnProperty.call(result, key)) { result[key] = given[key]; } } } result.calledBy = (given && given._calledBy) || ''; smartbotic.log.info('Workflow Input: accepted ' + Object.keys(result).filter(function (k) { return k !== 'calledBy'; }).length + ' value(s)'); return result; } module.exports = { configSchema, inputSchema, outputSchema, execute };