|
|
@@ -0,0 +1,163 @@
|
|
|
+/**
|
|
|
+ * @node filter
|
|
|
+ * @name Filter
|
|
|
+ * @category flow-control
|
|
|
+ * @version 1.0.0
|
|
|
+ * @description Split an array into the items that match and the ones that do not
|
|
|
+ * @icon filter
|
|
|
+ */
|
|
|
+
|
|
|
+const outputs = [
|
|
|
+ { name: 'kept', displayName: 'Kept', type: 'any', color: '#22c55e' },
|
|
|
+ { name: 'discarded', displayName: 'Discarded', type: 'any', color: '#ef4444' }
|
|
|
+];
|
|
|
+
|
|
|
+const configSchema = {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ inputField: {
|
|
|
+ type: 'string',
|
|
|
+ title: 'Array Field',
|
|
|
+ description: 'Path to the array to filter, such as data.result.items',
|
|
|
+ default: 'data'
|
|
|
+ },
|
|
|
+ conditions: {
|
|
|
+ type: 'array',
|
|
|
+ title: 'Conditions',
|
|
|
+ description: 'Field paths are relative to each item',
|
|
|
+ items: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ field: {
|
|
|
+ type: 'string',
|
|
|
+ title: 'Field'
|
|
|
+ },
|
|
|
+ operator: {
|
|
|
+ type: 'string',
|
|
|
+ title: 'Operator',
|
|
|
+ enum: ['equals', 'not_equals', 'contains', 'not_contains', 'starts_with', 'ends_with', 'greater_than', 'less_than', 'is_empty', 'is_not_empty', 'regex'],
|
|
|
+ default: 'equals'
|
|
|
+ },
|
|
|
+ value: {
|
|
|
+ type: 'string',
|
|
|
+ title: 'Value'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ combineWith: {
|
|
|
+ type: 'string',
|
|
|
+ title: 'Combine Conditions',
|
|
|
+ enum: ['and', 'or'],
|
|
|
+ default: 'and'
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const inputSchema = {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ data: { type: 'any' }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const outputSchema = {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ keptCount: { type: 'number' },
|
|
|
+ discardedCount: { type: 'number' }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+function isEmpty(value) {
|
|
|
+ return value === undefined || value === null || value === '' ||
|
|
|
+ (Array.isArray(value) && value.length === 0);
|
|
|
+}
|
|
|
+
|
|
|
+function conditionHolds(item, condition) {
|
|
|
+ const fieldValue = smartbotic.utils.getFieldValue(item, condition.field);
|
|
|
+ const compare = condition.value;
|
|
|
+
|
|
|
+ switch (condition.operator) {
|
|
|
+ case 'equals':
|
|
|
+ return String(fieldValue) === String(compare);
|
|
|
+ case 'not_equals':
|
|
|
+ return String(fieldValue) !== String(compare);
|
|
|
+ case 'contains':
|
|
|
+ return String(fieldValue).indexOf(String(compare)) !== -1;
|
|
|
+ case 'not_contains':
|
|
|
+ return String(fieldValue).indexOf(String(compare)) === -1;
|
|
|
+ case 'starts_with':
|
|
|
+ return String(fieldValue).indexOf(String(compare)) === 0;
|
|
|
+ case 'ends_with': {
|
|
|
+ const haystack = String(fieldValue);
|
|
|
+ const needle = String(compare);
|
|
|
+ return haystack.length >= needle.length &&
|
|
|
+ haystack.lastIndexOf(needle) === haystack.length - needle.length;
|
|
|
+ }
|
|
|
+ case 'greater_than':
|
|
|
+ return Number(fieldValue) > Number(compare);
|
|
|
+ case 'less_than':
|
|
|
+ return Number(fieldValue) < Number(compare);
|
|
|
+ case 'is_empty':
|
|
|
+ return isEmpty(fieldValue);
|
|
|
+ case 'is_not_empty':
|
|
|
+ return !isEmpty(fieldValue);
|
|
|
+ case 'regex':
|
|
|
+ try {
|
|
|
+ return new RegExp(String(compare)).test(String(fieldValue));
|
|
|
+ } catch (e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function execute(config, input, context) {
|
|
|
+ const conditions = Array.isArray(config.conditions) ? config.conditions : [];
|
|
|
+ const combineWith = config.combineWith || 'and';
|
|
|
+ const inputField = config.inputField;
|
|
|
+
|
|
|
+ let items;
|
|
|
+ if (Array.isArray(inputField)) {
|
|
|
+ items = inputField;
|
|
|
+ } else {
|
|
|
+ items = smartbotic.utils.getFieldValue(input, inputField || 'data');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Array.isArray(items)) {
|
|
|
+ throw new Error('Filter: the value at "' + inputField + '" is not an array');
|
|
|
+ }
|
|
|
+
|
|
|
+ const kept = [];
|
|
|
+ const discarded = [];
|
|
|
+
|
|
|
+ for (const item of items) {
|
|
|
+ let passes;
|
|
|
+ if (conditions.length === 0) {
|
|
|
+ passes = true;
|
|
|
+ } else if (combineWith === 'or') {
|
|
|
+ passes = conditions.some(function (c) { return conditionHolds(item, c); });
|
|
|
+ } else {
|
|
|
+ passes = conditions.every(function (c) { return conditionHolds(item, c); });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (passes) {
|
|
|
+ kept.push(item);
|
|
|
+ } else {
|
|
|
+ discarded.push(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ smartbotic.log.info('Filter: kept ' + kept.length + ' of ' + items.length);
|
|
|
+
|
|
|
+ return {
|
|
|
+ keptCount: kept.length,
|
|
|
+ discardedCount: discarded.length,
|
|
|
+ kept: kept,
|
|
|
+ discarded: discarded
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { configSchema, inputSchema, outputSchema, outputs, execute };
|