|
@@ -2,7 +2,7 @@
|
|
|
* @node if-condition
|
|
* @node if-condition
|
|
|
* @name IF Condition
|
|
* @name IF Condition
|
|
|
* @category flow-control
|
|
* @category flow-control
|
|
|
- * @version 2.2.0
|
|
|
|
|
|
|
+ * @version 2.3.0
|
|
|
* @description Conditional branching with TRUE/FALSE output paths
|
|
* @description Conditional branching with TRUE/FALSE output paths
|
|
|
* @icon git-branch
|
|
* @icon git-branch
|
|
|
*/
|
|
*/
|
|
@@ -36,6 +36,12 @@ const configSchema = {
|
|
|
type: 'string',
|
|
type: 'string',
|
|
|
title: 'Value',
|
|
title: 'Value',
|
|
|
description: 'Value to compare against'
|
|
description: 'Value to compare against'
|
|
|
|
|
+ },
|
|
|
|
|
+ optional: {
|
|
|
|
|
+ type: 'boolean',
|
|
|
|
|
+ title: 'Field Is Optional',
|
|
|
|
|
+ description: 'If the field path does not resolve, treat it as missing (matching is_null/exists/is_empty as usual) instead of failing the node. Only turn this on when the field genuinely may not be there - it silences the same check this node exists to make.',
|
|
|
|
|
+ default: false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -66,18 +72,46 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
|
|
+// Describes what IS at a spot in the data, for use in an error message when a
|
|
|
|
|
+// path expected something else there.
|
|
|
|
|
+function describeAvailable(value) {
|
|
|
|
|
+ if (value === null || value === undefined) return 'nothing';
|
|
|
|
|
+ if (Array.isArray(value)) return 'an array (length ' + value.length + ')';
|
|
|
|
|
+ if (typeof value === 'object') {
|
|
|
|
|
+ const keys = Object.keys(value);
|
|
|
|
|
+ return keys.length ? keys.join(', ') : 'an empty object';
|
|
|
|
|
+ }
|
|
|
|
|
+ return typeof value;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// `diagnostics`, when passed, is filled in when the path does not resolve:
|
|
|
|
|
+// which segment broke, the path consumed up to that point, and what was
|
|
|
|
|
+// actually there instead - the detail needed for a message that names the
|
|
|
|
|
+// mistake instead of just reporting "false".
|
|
|
|
|
+function getFieldValue(data, path, diagnostics) {
|
|
|
if (!path) return data;
|
|
if (!path) return data;
|
|
|
|
|
|
|
|
const keys = path.split('.');
|
|
const keys = path.split('.');
|
|
|
let value = data;
|
|
let value = data;
|
|
|
|
|
+ const consumed = [];
|
|
|
|
|
+
|
|
|
|
|
+ const fail = (missingKey, availableIn) => {
|
|
|
|
|
+ if (diagnostics) {
|
|
|
|
|
+ diagnostics.failed = true;
|
|
|
|
|
+ diagnostics.missingKey = missingKey;
|
|
|
|
|
+ diagnostics.failedAt = consumed.length ? consumed.join('.') : null;
|
|
|
|
|
+ diagnostics.availableDesc = describeAvailable(availableIn);
|
|
|
|
|
+ }
|
|
|
|
|
+ return undefined;
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
for (const key of keys) {
|
|
for (const key of keys) {
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
|
|
+ if (value === null || value === undefined) return fail(key, value);
|
|
|
|
|
|
|
|
// Handle array .length property
|
|
// Handle array .length property
|
|
|
if (key === 'length' && Array.isArray(value)) {
|
|
if (key === 'length' && Array.isArray(value)) {
|
|
|
value = value.length;
|
|
value = value.length;
|
|
|
|
|
+ consumed.push(key);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -90,30 +124,46 @@ function getFieldValue(data, path) {
|
|
|
}
|
|
}
|
|
|
if (Array.isArray(value)) {
|
|
if (Array.isArray(value)) {
|
|
|
value = value[parseInt(index, 10)];
|
|
value = value[parseInt(index, 10)];
|
|
|
|
|
+ consumed.push(key);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- return undefined;
|
|
|
|
|
|
|
+ return fail(key, value);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Handle pure numeric index for arrays
|
|
// Handle pure numeric index for arrays
|
|
|
if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
value = value[parseInt(key, 10)];
|
|
value = value[parseInt(key, 10)];
|
|
|
|
|
+ consumed.push(key);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (typeof value === 'object' && key in value) {
|
|
if (typeof value === 'object' && key in value) {
|
|
|
value = value[key];
|
|
value = value[key];
|
|
|
|
|
+ consumed.push(key);
|
|
|
} else {
|
|
} else {
|
|
|
- return undefined;
|
|
|
|
|
|
|
+ return fail(key, value);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
return value;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Operators that exist specifically to ask "is this field here at all".
|
|
|
|
|
+// A missing field is a legitimate, meaningful answer for these - failing
|
|
|
|
|
+// the node would defeat the point of having them. is_empty/is_not_empty are
|
|
|
|
|
+// included because they are the standard way to check an optional field,
|
|
|
|
|
+// and a missing field reads the same as an explicitly empty one for that
|
|
|
|
|
+// purpose. Everything else - including is_true/is_false and not_contains,
|
|
|
|
|
+// which can each silently read "missing" as a valid match - requires the
|
|
|
|
|
+// path to actually resolve.
|
|
|
|
|
+const PRESENCE_OPERATORS = new Set([
|
|
|
|
|
+ 'exists', 'is_null', 'is_not_null', 'is_empty', 'is_not_empty'
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
function evaluateCondition(data, condition) {
|
|
function evaluateCondition(data, condition) {
|
|
|
let fieldValue;
|
|
let fieldValue;
|
|
|
const field = condition.field;
|
|
const field = condition.field;
|
|
|
|
|
+ let pathDiagnostics = null;
|
|
|
|
|
|
|
|
// If field is already a non-string value (boolean, number, etc. from expression evaluation),
|
|
// If field is already a non-string value (boolean, number, etc. from expression evaluation),
|
|
|
// use it directly instead of trying to resolve it as a path
|
|
// use it directly instead of trying to resolve it as a path
|
|
@@ -126,7 +176,8 @@ function evaluateCondition(data, condition) {
|
|
|
if (/^\d+$/.test(field) || /^\d+\.\d+$/.test(field)) {
|
|
if (/^\d+$/.test(field) || /^\d+\.\d+$/.test(field)) {
|
|
|
fieldValue = parseFloat(field);
|
|
fieldValue = parseFloat(field);
|
|
|
} else {
|
|
} else {
|
|
|
- fieldValue = getFieldValue(data, field);
|
|
|
|
|
|
|
+ pathDiagnostics = {};
|
|
|
|
|
+ fieldValue = getFieldValue(data, field, pathDiagnostics);
|
|
|
}
|
|
}
|
|
|
} else if (field === null || field === undefined) {
|
|
} else if (field === null || field === undefined) {
|
|
|
fieldValue = field;
|
|
fieldValue = field;
|
|
@@ -135,6 +186,25 @@ function evaluateCondition(data, condition) {
|
|
|
fieldValue = field;
|
|
fieldValue = field;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // A field path that never resolved is a configuration mistake, not a
|
|
|
|
|
+ // false answer - "this field is false" and "this field does not exist"
|
|
|
|
|
+ // are different questions, and blurring them is what let a workflow run
|
|
|
|
|
+ // green while silently skipping a step. Presence operators are exempt
|
|
|
|
|
+ // (see PRESENCE_OPERATORS above); an author who genuinely means "treat
|
|
|
|
|
+ // absent as false" can also set `optional: true` on the condition.
|
|
|
|
|
+ if (pathDiagnostics && pathDiagnostics.failed &&
|
|
|
|
|
+ !PRESENCE_OPERATORS.has(condition.operator) && !condition.optional) {
|
|
|
|
|
+ const at = pathDiagnostics.failedAt
|
|
|
|
|
+ ? 'under "' + pathDiagnostics.failedAt + '"'
|
|
|
|
|
+ : 'at the top level';
|
|
|
|
|
+ throw new Error(
|
|
|
|
|
+ 'IF condition: nothing found at "' + field + '". Field "' + pathDiagnostics.missingKey +
|
|
|
|
|
+ '" does not exist ' + at + '. Available there: ' + pathDiagnostics.availableDesc + '. ' +
|
|
|
|
|
+ 'If this field is genuinely optional, set "Field Is Optional" on the condition, ' +
|
|
|
|
|
+ 'or use exists/is_null/is_empty to test for it directly.'
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const compareValue = condition.value;
|
|
const compareValue = condition.value;
|
|
|
|
|
|
|
|
switch (condition.operator) {
|
|
switch (condition.operator) {
|