workflow-input.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. /**
  2. * @node workflow-input
  3. * @name Workflow Input
  4. * @category triggers
  5. * @version 1.0.0
  6. * @description Start a workflow that another workflow calls, and say what it expects to be given
  7. * @trigger
  8. * @icon log-in
  9. */
  10. const configSchema = {
  11. type: 'object',
  12. // The names below are what this node produces, so the editor can offer
  13. // them to later nodes as soon as they are typed - without waiting for a run.
  14. outputsFromConfig: { from: 'fields', nameKey: 'name' },
  15. properties: {
  16. fields: {
  17. type: 'array',
  18. title: 'Expected Input',
  19. 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',
  20. items: {
  21. type: 'object',
  22. properties: {
  23. name: { type: 'string', title: 'Name', description: 'Key the caller passes, such as city' },
  24. required: { type: 'boolean', title: 'Required', description: 'Refuse the call when this is missing', default: true },
  25. description: { type: 'string', title: 'Description', description: 'What the caller should put here' },
  26. defaultValue: { type: 'string', title: 'Default', description: 'Used when an optional key is not passed' }
  27. }
  28. }
  29. },
  30. allowExtra: {
  31. type: 'boolean',
  32. title: 'Allow Anything Else',
  33. description: 'Pass through keys that are not listed above. Turn off to keep the contract exact and catch a caller sending the wrong thing',
  34. default: true
  35. }
  36. }
  37. };
  38. const inputSchema = { type: 'object', properties: { data: { type: 'any' } } };
  39. const outputSchema = {
  40. type: 'object',
  41. // The real shape is dynamic: one key per row configured under Expected
  42. // Input, named whatever the caller was told to pass (see configSchema's
  43. // outputsFromConfig, which is what feeds the editor the real names as
  44. // they are typed). Plus, when Allow Anything Else is on, any other key
  45. // the caller passed that was not listed. calledBy is the only key
  46. // guaranteed to exist regardless of configuration.
  47. properties: {
  48. calledBy: { type: 'string', description: 'Execution id of the workflow that called this one, empty when run directly' }
  49. },
  50. additionalProperties: true
  51. };
  52. async function execute(config, input, context) {
  53. // The caller's input arrives as the trigger data, the same way a webhook
  54. // body does. Internal keys the engine put there are not part of it.
  55. const given = (input && typeof input === 'object') ? input : {};
  56. const fields = config.fields || [];
  57. const result = {};
  58. const missing = [];
  59. for (let i = 0; i < fields.length; i++) {
  60. const field = fields[i] || {};
  61. const name = String(field.name || '').trim();
  62. if (!name) {
  63. // A row with a description or a default but no name is a filled-in
  64. // row whose key went in the wrong box. Skipping it silently meant
  65. // the caller's value was never accepted and the failure surfaced
  66. // somewhere else entirely - a node further on complaining that
  67. // something it needed was missing.
  68. const described = String(field.description || '').trim();
  69. const fallback = String(field.defaultValue || '').trim();
  70. if (described || fallback || field.required === true) {
  71. throw new Error('Workflow Input: an expected input has no name' +
  72. (described ? ' (described as "' + described + '")' : '') +
  73. (fallback ? ', default "' + fallback + '"' : '') +
  74. '. Put the key the caller passes in the Name box');
  75. }
  76. continue;
  77. }
  78. const has = Object.prototype.hasOwnProperty.call(given, name) &&
  79. given[name] !== undefined && given[name] !== null && given[name] !== '';
  80. if (has) {
  81. result[name] = given[name];
  82. } else if (field.required !== false) {
  83. missing.push(name);
  84. } else if (field.defaultValue !== undefined && field.defaultValue !== '') {
  85. result[name] = field.defaultValue;
  86. }
  87. }
  88. // Refusing here, naming every missing key at once, beats failing three
  89. // nodes later on an undefined that has to be traced back.
  90. if (missing.length > 0) {
  91. throw new Error('Workflow Input: the caller did not pass ' + missing.join(', ') +
  92. '. ' + (missing.length === 1 ? 'It is' : 'They are') + ' marked required on this node');
  93. }
  94. if (config.allowExtra !== false) {
  95. const keys = Object.keys(given);
  96. for (let i = 0; i < keys.length; i++) {
  97. const key = keys[i];
  98. // Engine bookkeeping, not part of what the caller sent.
  99. if (key.charAt(0) === '_') continue;
  100. if (!Object.prototype.hasOwnProperty.call(result, key)) {
  101. result[key] = given[key];
  102. }
  103. }
  104. }
  105. result.calledBy = (given && given._calledBy) || '';
  106. smartbotic.log.info('Workflow Input: accepted ' +
  107. Object.keys(result).filter(function (k) { return k !== 'calledBy'; }).length + ' value(s)');
  108. return result;
  109. }
  110. module.exports = { configSchema, inputSchema, outputSchema, execute };