workflow-input.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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. properties: {
  42. calledBy: { type: 'string', description: 'Execution id of the workflow that called this one, empty when run directly' }
  43. }
  44. };
  45. async function execute(config, input, context) {
  46. // The caller's input arrives as the trigger data, the same way a webhook
  47. // body does. Internal keys the engine put there are not part of it.
  48. const given = (input && typeof input === 'object') ? input : {};
  49. const fields = config.fields || [];
  50. const result = {};
  51. const missing = [];
  52. for (let i = 0; i < fields.length; i++) {
  53. const field = fields[i] || {};
  54. const name = String(field.name || '').trim();
  55. if (!name) {
  56. // A row with a description or a default but no name is a filled-in
  57. // row whose key went in the wrong box. Skipping it silently meant
  58. // the caller's value was never accepted and the failure surfaced
  59. // somewhere else entirely - a node further on complaining that
  60. // something it needed was missing.
  61. const described = String(field.description || '').trim();
  62. const fallback = String(field.defaultValue || '').trim();
  63. if (described || fallback || field.required === true) {
  64. throw new Error('Workflow Input: an expected input has no name' +
  65. (described ? ' (described as "' + described + '")' : '') +
  66. (fallback ? ', default "' + fallback + '"' : '') +
  67. '. Put the key the caller passes in the Name box');
  68. }
  69. continue;
  70. }
  71. const has = Object.prototype.hasOwnProperty.call(given, name) &&
  72. given[name] !== undefined && given[name] !== null && given[name] !== '';
  73. if (has) {
  74. result[name] = given[name];
  75. } else if (field.required !== false) {
  76. missing.push(name);
  77. } else if (field.defaultValue !== undefined && field.defaultValue !== '') {
  78. result[name] = field.defaultValue;
  79. }
  80. }
  81. // Refusing here, naming every missing key at once, beats failing three
  82. // nodes later on an undefined that has to be traced back.
  83. if (missing.length > 0) {
  84. throw new Error('Workflow Input: the caller did not pass ' + missing.join(', ') +
  85. '. ' + (missing.length === 1 ? 'It is' : 'They are') + ' marked required on this node');
  86. }
  87. if (config.allowExtra !== false) {
  88. const keys = Object.keys(given);
  89. for (let i = 0; i < keys.length; i++) {
  90. const key = keys[i];
  91. // Engine bookkeeping, not part of what the caller sent.
  92. if (key.charAt(0) === '_') continue;
  93. if (!Object.prototype.hasOwnProperty.call(result, key)) {
  94. result[key] = given[key];
  95. }
  96. }
  97. }
  98. result.calledBy = (given && given._calledBy) || '';
  99. smartbotic.log.info('Workflow Input: accepted ' +
  100. Object.keys(result).filter(function (k) { return k !== 'calledBy'; }).length + ' value(s)');
  101. return result;
  102. }
  103. module.exports = { configSchema, inputSchema, outputSchema, execute };