Turn filter action into conditions (#13005)
Previous logic was using the previous step output and filtering items that were passing filters. What we actually want is: - send filters, right operand being always a step output key, left operand being either a key, either a value - resolve those filter variables - apply the filters to decide whether the condition is passed or not
This commit is contained in:
@ -585,7 +585,8 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
settings: {
|
settings: {
|
||||||
...BASE_STEP_DEFINITION,
|
...BASE_STEP_DEFINITION,
|
||||||
input: {
|
input: {
|
||||||
filter: {},
|
filterGroups: [],
|
||||||
|
filters: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,10 +8,9 @@ import {
|
|||||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||||
import { WorkflowExecutorInput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
import { WorkflowExecutorInput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||||
import { WorkflowExecutorOutput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-output.type';
|
import { WorkflowExecutorOutput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-output.type';
|
||||||
|
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||||
import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard';
|
import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard';
|
||||||
import { applyFilter } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/apply-filter.util';
|
import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
|
||||||
|
|
||||||
import { getPreviousStepOutput } from './utils/get-previous-step-output.util';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FilterWorkflowAction implements WorkflowExecutor {
|
export class FilterWorkflowAction implements WorkflowExecutor {
|
||||||
@ -34,37 +33,30 @@ export class FilterWorkflowAction implements WorkflowExecutor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filter } = step.settings.input;
|
const { filterGroups, filters } = step.settings.input;
|
||||||
|
|
||||||
if (!filter) {
|
if (!filterGroups || !filters) {
|
||||||
throw new WorkflowStepExecutorException(
|
throw new WorkflowStepExecutorException(
|
||||||
'Filter is not defined',
|
'Filter is not defined',
|
||||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_SETTINGS,
|
WorkflowStepExecutorExceptionCode.INVALID_STEP_SETTINGS,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousStepOutput = getPreviousStepOutput(
|
const resolvedFilters = filters.map((filter) => ({
|
||||||
steps,
|
...filter,
|
||||||
currentStepId,
|
rightOperand: resolveInput(filter.value, context),
|
||||||
context,
|
leftOperand: resolveInput(filter.stepOutputKey, context),
|
||||||
);
|
}));
|
||||||
|
|
||||||
const isPreviousStepOutputArray = Array.isArray(previousStepOutput);
|
const matchesFilter = evaluateFilterConditions({
|
||||||
|
filterGroups,
|
||||||
const previousStepOutputArray = isPreviousStepOutputArray
|
filters: resolvedFilters,
|
||||||
? previousStepOutput
|
});
|
||||||
: [previousStepOutput];
|
|
||||||
|
|
||||||
const filteredOutput = applyFilter(previousStepOutputArray, filter);
|
|
||||||
|
|
||||||
if (filteredOutput.length === 0) {
|
|
||||||
return {
|
|
||||||
result: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: isPreviousStepOutputArray ? filteredOutput : filteredOutput[0],
|
result: {
|
||||||
|
matchesFilter,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,44 @@
|
|||||||
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
|
||||||
|
|
||||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||||
|
|
||||||
|
export enum LogicalOperator {
|
||||||
|
AND = 'AND',
|
||||||
|
OR = 'OR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Operand {
|
||||||
|
EQ = 'eq',
|
||||||
|
NE = 'ne',
|
||||||
|
GT = 'gt',
|
||||||
|
GTE = 'gte',
|
||||||
|
LT = 'lt',
|
||||||
|
LTE = 'lte',
|
||||||
|
LIKE = 'like',
|
||||||
|
ILIKE = 'ilike',
|
||||||
|
IN = 'in',
|
||||||
|
IS = 'is',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterGroup = {
|
||||||
|
id: string;
|
||||||
|
logicalOperator: LogicalOperator;
|
||||||
|
parentRecordFilterGroupId?: string;
|
||||||
|
positionInRecordFilterGroup?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Filter = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
operand: Operand;
|
||||||
|
displayValue: string;
|
||||||
|
recordFilterGroupId: string;
|
||||||
|
stepOutputKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowFilterActionSettings = BaseWorkflowActionSettings & {
|
export type WorkflowFilterActionSettings = BaseWorkflowActionSettings & {
|
||||||
input: {
|
input: {
|
||||||
filter: Partial<ObjectRecordFilter>;
|
filterGroups?: FilterGroup[];
|
||||||
|
filters?: Filter[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,324 +0,0 @@
|
|||||||
import { applyFilter } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/apply-filter.util';
|
|
||||||
|
|
||||||
describe('applyFilter', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
name: { firstName: 'John', lastName: 'Doe' },
|
|
||||||
companyId: '20202020-171e-4bcc-9cf7-43448d6fb278',
|
|
||||||
emails: { primaryEmail: 'john.doe@example.com' },
|
|
||||||
city: 'New York',
|
|
||||||
age: 30,
|
|
||||||
tags: ['developer', 'senior'],
|
|
||||||
metadata: { lastLogin: '2024-03-15', isActive: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: { firstName: 'Jane', lastName: 'Smith' },
|
|
||||||
companyId: '30303030-171e-4bcc-9cf7-43448d6fb278',
|
|
||||||
emails: { primaryEmail: 'jane.smith@test.com' },
|
|
||||||
city: null,
|
|
||||||
age: 25,
|
|
||||||
tags: ['designer', 'junior'],
|
|
||||||
metadata: { lastLogin: '2024-03-10', isActive: false },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: { firstName: 'Tom', lastName: 'Cruise' },
|
|
||||||
companyId: '20202020-171e-4bcc-9cf7-43448d6fb278',
|
|
||||||
emails: { primaryEmail: 'tom.cruise@example.com' },
|
|
||||||
city: '',
|
|
||||||
age: 40,
|
|
||||||
tags: ['manager', 'senior'],
|
|
||||||
metadata: { lastLogin: '2024-03-20', isActive: true },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('Logical Operators', () => {
|
|
||||||
it('should handle AND conditions', () => {
|
|
||||||
const filter = {
|
|
||||||
and: [{ age: { gt: 25 } }, { metadata: { isActive: { eq: true } } }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
testData[0], // John Doe
|
|
||||||
testData[2], // Tom Cruise
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle OR conditions', () => {
|
|
||||||
const filter = {
|
|
||||||
or: [{ age: { lt: 30 } }, { city: { is: 'NULL' } }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
testData[1], // Jane Smith
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle NOT conditions', () => {
|
|
||||||
const filter = {
|
|
||||||
not: {
|
|
||||||
metadata: { isActive: { eq: true } },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
testData[1], // Jane Smith
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle complex nested logical operators', () => {
|
|
||||||
const filter = {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
or: [{ age: { gt: 35 } }, { city: { is: 'NULL' } }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
not: {
|
|
||||||
metadata: { isActive: { eq: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
testData[2], // Tom Cruise
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Comparison Operators', () => {
|
|
||||||
it('should handle eq operator', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { eq: 30 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle ne operator', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { ne: 30 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[1], testData[2]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle gt operator', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { gt: 35 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[2]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle gte operator', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { gte: 30 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[2]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle lt operator', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { lt: 30 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[1]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle lte operator', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { lte: 30 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[1]]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('String Operators', () => {
|
|
||||||
it('should handle like operator', () => {
|
|
||||||
const filter = {
|
|
||||||
name: {
|
|
||||||
firstName: { like: 'J%' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[1]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle ilike operator', () => {
|
|
||||||
const filter = {
|
|
||||||
name: {
|
|
||||||
firstName: { ilike: 'j%' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[1]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle like with multiple wildcards', () => {
|
|
||||||
const filter = {
|
|
||||||
emails: {
|
|
||||||
primaryEmail: { like: '%@example.com' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[2]]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Array and Null Operators', () => {
|
|
||||||
it('should handle in operator', () => {
|
|
||||||
const filter = {
|
|
||||||
companyId: {
|
|
||||||
in: ['20202020-171e-4bcc-9cf7-43448d6fb278'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[2]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle is NULL operator', () => {
|
|
||||||
const filter = {
|
|
||||||
city: { is: 'NULL' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[1]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle array contains', () => {
|
|
||||||
const filter = {
|
|
||||||
tags: {
|
|
||||||
in: ['senior'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0], testData[2]]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Nested Object Conditions', () => {
|
|
||||||
it('should handle deeply nested conditions', () => {
|
|
||||||
const filter = {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
firstName: { like: 'J%' },
|
|
||||||
lastName: { like: 'D%' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metadata: {
|
|
||||||
isActive: { eq: true },
|
|
||||||
lastLogin: { like: '2024-03-15' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[0]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle multiple levels of nesting', () => {
|
|
||||||
const filter = {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
firstName: { like: 'T%' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metadata: {
|
|
||||||
isActive: { eq: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
emails: {
|
|
||||||
primaryEmail: { like: '%@example.com' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([testData[2]]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
|
||||||
it('should handle empty filter', () => {
|
|
||||||
const filter = {};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual(testData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty array input', () => {
|
|
||||||
const filter = {
|
|
||||||
age: { gt: 30 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter([], filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle non-existent fields', () => {
|
|
||||||
const filter = {
|
|
||||||
nonExistentField: { eq: 'value' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle null values in nested objects', () => {
|
|
||||||
const filter = {
|
|
||||||
metadata: {
|
|
||||||
nonExistentField: { is: 'NULL' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyFilter(testData, filter);
|
|
||||||
|
|
||||||
expect(result).toEqual(testData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -0,0 +1,805 @@
|
|||||||
|
import {
|
||||||
|
FilterGroup,
|
||||||
|
LogicalOperator,
|
||||||
|
Operand,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||||
|
import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
|
||||||
|
|
||||||
|
type ResolvedFilter = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
rightOperand: unknown;
|
||||||
|
operand: Operand;
|
||||||
|
displayValue: string;
|
||||||
|
fieldMetadataId: string;
|
||||||
|
recordFilterGroupId: string;
|
||||||
|
leftOperand: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('evaluateFilterConditions', () => {
|
||||||
|
describe('empty inputs', () => {
|
||||||
|
it('should return true when no filters or groups are provided', () => {
|
||||||
|
const result = evaluateFilterConditions({
|
||||||
|
filterGroups: [],
|
||||||
|
filters: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when inputs are undefined', () => {
|
||||||
|
const result = evaluateFilterConditions({});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('single filter operands', () => {
|
||||||
|
const createFilter = (
|
||||||
|
operand: Operand,
|
||||||
|
leftOperand: unknown,
|
||||||
|
rightOperand: unknown,
|
||||||
|
): ResolvedFilter => ({
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Test Filter',
|
||||||
|
rightOperand,
|
||||||
|
operand,
|
||||||
|
displayValue: String(rightOperand),
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('eq operand', () => {
|
||||||
|
it('should return true when values are equal', () => {
|
||||||
|
const filter = createFilter(Operand.EQ, 'John', 'John');
|
||||||
|
const result = evaluateFilterConditions({ filters: [filter] });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when values are loosely equal', () => {
|
||||||
|
const filter = createFilter(Operand.EQ, '123', 123);
|
||||||
|
const result = evaluateFilterConditions({ filters: [filter] });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when values are not equal', () => {
|
||||||
|
const filter = createFilter(Operand.EQ, 'John', 'Jane');
|
||||||
|
const result = evaluateFilterConditions({ filters: [filter] });
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ne operand', () => {
|
||||||
|
it('should return false when values are equal', () => {
|
||||||
|
const filter = createFilter(Operand.NE, 'John', 'John');
|
||||||
|
const result = evaluateFilterConditions({ filters: [filter] });
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when values are not equal', () => {
|
||||||
|
const filter = createFilter(Operand.NE, 'John', 'Jane');
|
||||||
|
const result = evaluateFilterConditions({ filters: [filter] });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('numeric operands', () => {
|
||||||
|
it('should handle gt operand correctly', () => {
|
||||||
|
const filter1 = createFilter(Operand.GT, 30, 25);
|
||||||
|
const filter2 = createFilter(Operand.GT, 20, 25);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle gte operand correctly', () => {
|
||||||
|
const filter1 = createFilter(Operand.GTE, 25, 25);
|
||||||
|
const filter2 = createFilter(Operand.GTE, 30, 25);
|
||||||
|
const filter3 = createFilter(Operand.GTE, 20, 25);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle lt operand correctly', () => {
|
||||||
|
const filter1 = createFilter(Operand.LT, 20, 25);
|
||||||
|
const filter2 = createFilter(Operand.LT, 30, 25);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle lte operand correctly', () => {
|
||||||
|
const filter1 = createFilter(Operand.LTE, 25, 25);
|
||||||
|
const filter2 = createFilter(Operand.LTE, 20, 25);
|
||||||
|
const filter3 = createFilter(Operand.LTE, 30, 25);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert string numbers for numeric comparisons', () => {
|
||||||
|
const filter1 = createFilter(Operand.GT, '30', '25');
|
||||||
|
const filter2 = createFilter(Operand.LT, '20', '25');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('string operands', () => {
|
||||||
|
it('should handle like operand correctly', () => {
|
||||||
|
const filter1 = createFilter(Operand.LIKE, 'Hello World', 'World');
|
||||||
|
const filter2 = createFilter(Operand.LIKE, 'Hello', 'World');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ilike operand correctly (case insensitive)', () => {
|
||||||
|
const filter1 = createFilter(Operand.ILIKE, 'Hello World', 'WORLD');
|
||||||
|
const filter2 = createFilter(Operand.ILIKE, 'Hello World', 'world');
|
||||||
|
const filter3 = createFilter(Operand.ILIKE, 'Hello', 'WORLD');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('in operand', () => {
|
||||||
|
it('should handle JSON array values', () => {
|
||||||
|
const filter1 = createFilter(
|
||||||
|
Operand.IN,
|
||||||
|
'apple',
|
||||||
|
'["apple", "banana", "cherry"]',
|
||||||
|
);
|
||||||
|
const filter2 = createFilter(
|
||||||
|
Operand.IN,
|
||||||
|
'grape',
|
||||||
|
'["apple", "banana", "cherry"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle comma-separated string values when JSON parsing fails', () => {
|
||||||
|
const filter1 = createFilter(
|
||||||
|
Operand.IN,
|
||||||
|
'apple',
|
||||||
|
'apple, banana, cherry',
|
||||||
|
);
|
||||||
|
const filter2 = createFilter(
|
||||||
|
Operand.IN,
|
||||||
|
'grape',
|
||||||
|
'apple, banana, cherry',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle comma-separated values with whitespace', () => {
|
||||||
|
const filter = createFilter(
|
||||||
|
Operand.IN,
|
||||||
|
'apple',
|
||||||
|
' apple , banana , cherry ',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-array JSON values', () => {
|
||||||
|
const filter = createFilter(Operand.IN, 'apple', '{"key": "value"}');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter] })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('is operand', () => {
|
||||||
|
it('should handle null checks', () => {
|
||||||
|
const filter1 = createFilter(Operand.IS, null, 'null');
|
||||||
|
const filter2 = createFilter(Operand.IS, undefined, 'NULL');
|
||||||
|
const filter3 = createFilter(Operand.IS, 'value', 'null');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle not null checks', () => {
|
||||||
|
const filter1 = createFilter(Operand.IS, 'value', 'not null');
|
||||||
|
const filter2 = createFilter(Operand.IS, 'value', 'NOT NULL');
|
||||||
|
const filter3 = createFilter(Operand.IS, null, 'not null');
|
||||||
|
const filter4 = createFilter(Operand.IS, undefined, 'not null');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter4] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle exact value comparisons for non-null/not-null cases', () => {
|
||||||
|
const filter1 = createFilter(Operand.IS, 'exact', 'exact');
|
||||||
|
const filter2 = createFilter(Operand.IS, 'value', 'different');
|
||||||
|
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||||
|
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error cases', () => {
|
||||||
|
it('should throw error for unknown operand', () => {
|
||||||
|
const filter = createFilter('unknown' as Operand, 'value', 'value');
|
||||||
|
|
||||||
|
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
|
||||||
|
'Unknown operand: unknown',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple filters without groups', () => {
|
||||||
|
it('should apply AND logic by default for multiple filters', () => {
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 30,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filters });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when one filter fails in AND logic', () => {
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 20, // This will fail
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filters });
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter groups', () => {
|
||||||
|
describe('single group with AND logic', () => {
|
||||||
|
it('should return true when all filters pass', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 30,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when one filter fails', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'Jane', // This will fail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 30,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('single group with OR logic', () => {
|
||||||
|
it('should return true when at least one filter passes', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.OR,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'Jane', // This will fail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 30, // This will pass
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all filters fail', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.OR,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'Jane', // This will fail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 20, // This will fail
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty groups', () => {
|
||||||
|
it('should return true for empty group', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters: [] });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nested groups', () => {
|
||||||
|
it('should handle nested groups correctly', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group2',
|
||||||
|
logicalOperator: LogicalOperator.OR,
|
||||||
|
parentRecordFilterGroupId: 'group1',
|
||||||
|
positionInRecordFilterGroup: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
// Filter in parent group
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John', // This will pass
|
||||||
|
},
|
||||||
|
// Filters in child group with OR logic
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 25,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '25',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 20, // This will fail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter3',
|
||||||
|
type: 'text',
|
||||||
|
label: 'City Filter',
|
||||||
|
rightOperand: 'NYC',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'NYC',
|
||||||
|
fieldMetadataId: 'field3',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 'NYC', // This will pass
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parent group uses AND: filter1 (pass) AND group2 (pass because filter3 passes)
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple child groups with correct positioning', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group2',
|
||||||
|
logicalOperator: LogicalOperator.OR,
|
||||||
|
parentRecordFilterGroupId: 'group1',
|
||||||
|
positionInRecordFilterGroup: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group3',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
parentRecordFilterGroupId: 'group1',
|
||||||
|
positionInRecordFilterGroup: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
// Group2 filters (OR logic)
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 'Jane', // This will fail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Status Filter',
|
||||||
|
rightOperand: 'active',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'active',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 'active', // This will pass
|
||||||
|
},
|
||||||
|
// Group3 filters (AND logic)
|
||||||
|
{
|
||||||
|
id: 'filter3',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 18,
|
||||||
|
operand: Operand.GTE,
|
||||||
|
displayValue: '18',
|
||||||
|
fieldMetadataId: 'field3',
|
||||||
|
recordFilterGroupId: 'group3',
|
||||||
|
leftOperand: 25, // This will pass
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter4',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Score Filter',
|
||||||
|
rightOperand: 80,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '80',
|
||||||
|
fieldMetadataId: 'field4',
|
||||||
|
recordFilterGroupId: 'group3',
|
||||||
|
leftOperand: 85, // This will pass
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Group1 uses AND: group2 (pass) AND group3 (pass)
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple root groups', () => {
|
||||||
|
it('should combine multiple root groups with AND logic', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.OR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group2',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
// Group1 filters (OR logic)
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John', // This will pass
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Status Filter',
|
||||||
|
rightOperand: 'inactive',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'inactive',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'active', // This will fail
|
||||||
|
},
|
||||||
|
// Group2 filters (AND logic)
|
||||||
|
{
|
||||||
|
id: 'filter3',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 18,
|
||||||
|
operand: Operand.GTE,
|
||||||
|
displayValue: '18',
|
||||||
|
fieldMetadataId: 'field3',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 25, // This will pass
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter4',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Score Filter',
|
||||||
|
rightOperand: 80,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '80',
|
||||||
|
fieldMetadataId: 'field4',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 85, // This will pass
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Root groups combined with AND: group1 (pass) AND group2 (pass)
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when one root group fails', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.OR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group2',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
// Group1 filters (OR logic) - will pass
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John', // This will pass
|
||||||
|
},
|
||||||
|
// Group2 filters (AND logic) - will fail
|
||||||
|
{
|
||||||
|
id: 'filter2',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Age Filter',
|
||||||
|
rightOperand: 30,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '30',
|
||||||
|
fieldMetadataId: 'field2',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 25, // This will fail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter3',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Score Filter',
|
||||||
|
rightOperand: 80,
|
||||||
|
operand: Operand.GT,
|
||||||
|
displayValue: '80',
|
||||||
|
fieldMetadataId: 'field3',
|
||||||
|
recordFilterGroupId: 'group2',
|
||||||
|
leftOperand: 85, // This will pass
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Root groups combined with AND: group1 (pass) AND group2 (fail)
|
||||||
|
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error cases', () => {
|
||||||
|
it('should throw error when filter group is not found', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'nonexistent-group',
|
||||||
|
leftOperand: 'John',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
evaluateFilterConditions({ filterGroups, filters }),
|
||||||
|
).toThrow('Filter group with id nonexistent-group not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for unknown logical operator', () => {
|
||||||
|
const filterGroups: FilterGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
logicalOperator: 'UNKNOWN' as LogicalOperator,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters: ResolvedFilter[] = [
|
||||||
|
{
|
||||||
|
id: 'filter1',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Name Filter',
|
||||||
|
rightOperand: 'John',
|
||||||
|
operand: Operand.EQ,
|
||||||
|
displayValue: 'John',
|
||||||
|
fieldMetadataId: 'field1',
|
||||||
|
recordFilterGroupId: 'group1',
|
||||||
|
leftOperand: 'John',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
evaluateFilterConditions({ filterGroups, filters }),
|
||||||
|
).toThrow('Unknown logical operator: UNKNOWN');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import { WorkflowStepExecutorException } from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
|
||||||
import { getPreviousStepOutput } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/get-previous-step-output.util';
|
|
||||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
|
||||||
|
|
||||||
describe('getPreviousStepOutput', () => {
|
|
||||||
const mockSteps: WorkflowAction[] = [
|
|
||||||
{
|
|
||||||
id: 'step1',
|
|
||||||
nextStepIds: ['step2'],
|
|
||||||
} as WorkflowAction,
|
|
||||||
{
|
|
||||||
id: 'step2',
|
|
||||||
nextStepIds: ['step3'],
|
|
||||||
} as WorkflowAction,
|
|
||||||
{
|
|
||||||
id: 'step3',
|
|
||||||
nextStepIds: undefined,
|
|
||||||
} as WorkflowAction,
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockContext = {
|
|
||||||
step1: { data: 'step1 output' },
|
|
||||||
step2: { data: 'step2 output' },
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should return the previous step output when valid', () => {
|
|
||||||
const result = getPreviousStepOutput(mockSteps, 'step2', mockContext);
|
|
||||||
|
|
||||||
expect(result).toEqual({ data: 'step1 output' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when there is no previous step', () => {
|
|
||||||
expect(() =>
|
|
||||||
getPreviousStepOutput(mockSteps, 'step1', mockContext),
|
|
||||||
).toThrow(WorkflowStepExecutorException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when there are multiple previous steps', () => {
|
|
||||||
const stepsWithMultiplePrevious: WorkflowAction[] = [
|
|
||||||
{
|
|
||||||
id: 'step1',
|
|
||||||
nextStepIds: ['step3'],
|
|
||||||
} as WorkflowAction,
|
|
||||||
{
|
|
||||||
id: 'step2',
|
|
||||||
nextStepIds: ['step3'],
|
|
||||||
} as WorkflowAction,
|
|
||||||
{
|
|
||||||
id: 'step3',
|
|
||||||
nextStepIds: undefined,
|
|
||||||
} as WorkflowAction,
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
getPreviousStepOutput(stepsWithMultiplePrevious, 'step3', mockContext),
|
|
||||||
).toThrow(WorkflowStepExecutorException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when previous step output is not found', () => {
|
|
||||||
const emptyContext = {};
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
getPreviousStepOutput(mockSteps, 'step2', emptyContext),
|
|
||||||
).toThrow(WorkflowStepExecutorException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,179 +0,0 @@
|
|||||||
import { FilterCondition } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/filter-condition.type';
|
|
||||||
import { FilterOperator } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/filter-operator.type';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const applyFilter = <T extends Record<string, any>>(
|
|
||||||
array: T[],
|
|
||||||
filter: FilterCondition,
|
|
||||||
): T[] => {
|
|
||||||
return array.filter((item) => evaluateFilter(item, filter));
|
|
||||||
};
|
|
||||||
|
|
||||||
const evaluateFilter = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
item: Record<string, any>,
|
|
||||||
filter: FilterCondition,
|
|
||||||
): boolean => {
|
|
||||||
if ('and' in filter) {
|
|
||||||
return (
|
|
||||||
filter.and?.every((subFilter) => evaluateFilter(item, subFilter)) ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('or' in filter) {
|
|
||||||
return (
|
|
||||||
filter.or?.some((subFilter) => evaluateFilter(item, subFilter)) ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('not' in filter && filter.not) {
|
|
||||||
return !evaluateFilter(item, filter.not);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(filter).every(([field, conditions]) => {
|
|
||||||
const value = item[field];
|
|
||||||
|
|
||||||
if (isOperator(conditions)) {
|
|
||||||
return evaluateCondition(value, conditions as FilterOperator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === undefined && !hasNullCheck(conditions as FilterCondition)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return evaluateNestedConditions(value, conditions as FilterCondition);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasNullCheck = (conditions: FilterCondition): boolean => {
|
|
||||||
const operator = conditions as FilterOperator;
|
|
||||||
|
|
||||||
if ('is' in operator && operator.is === 'NULL') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(conditions).some(
|
|
||||||
(value) =>
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null &&
|
|
||||||
hasNullCheck(value as FilterCondition),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const evaluateNestedConditions = (
|
|
||||||
value: unknown,
|
|
||||||
conditions: FilterCondition,
|
|
||||||
): boolean => {
|
|
||||||
const operator = conditions as FilterOperator;
|
|
||||||
|
|
||||||
if ('is' in operator && operator.is === 'NULL') {
|
|
||||||
return value === null || value === undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(conditions).every(([field, nestedConditions]) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const nestedValue = (value as Record<string, any>)[field];
|
|
||||||
|
|
||||||
if (isOperator(nestedConditions)) {
|
|
||||||
return evaluateCondition(nestedValue, nestedConditions as FilterOperator);
|
|
||||||
}
|
|
||||||
|
|
||||||
return evaluateNestedConditions(
|
|
||||||
nestedValue,
|
|
||||||
nestedConditions as FilterCondition,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const evaluateCondition = (value: any, condition: FilterOperator): boolean => {
|
|
||||||
const [[operator, targetValue]] = Object.entries(condition);
|
|
||||||
|
|
||||||
switch (operator) {
|
|
||||||
case 'eq':
|
|
||||||
return value === targetValue;
|
|
||||||
|
|
||||||
case 'ne':
|
|
||||||
return value !== targetValue;
|
|
||||||
|
|
||||||
case 'gt':
|
|
||||||
return value > targetValue;
|
|
||||||
|
|
||||||
case 'gte':
|
|
||||||
return value >= targetValue;
|
|
||||||
|
|
||||||
case 'lt':
|
|
||||||
return value < targetValue;
|
|
||||||
|
|
||||||
case 'lte':
|
|
||||||
return value <= targetValue;
|
|
||||||
|
|
||||||
case 'like':
|
|
||||||
return matchesLike(value, targetValue as string);
|
|
||||||
|
|
||||||
case 'ilike':
|
|
||||||
return matchesILike(value, targetValue as string);
|
|
||||||
|
|
||||||
case 'in':
|
|
||||||
if (!Array.isArray(targetValue)) return false;
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.some((v) => targetValue.includes(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetValue.includes(value);
|
|
||||||
|
|
||||||
case 'is':
|
|
||||||
return targetValue === 'NULL'
|
|
||||||
? value === null || value === undefined
|
|
||||||
: value === targetValue;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const matchesLike = (value: any, pattern: string): boolean => {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regexPattern = pattern.replace(/%/g, '.*').replace(/_/g, '.');
|
|
||||||
|
|
||||||
return new RegExp(`^${regexPattern}$`).test(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const matchesILike = (value: any, pattern: string): boolean => {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regexPattern = pattern.replace(/%/g, '.*').replace(/_/g, '.');
|
|
||||||
|
|
||||||
return new RegExp(`^${regexPattern}$`, 'i').test(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const isOperator = (obj: any): boolean => {
|
|
||||||
if (typeof obj !== 'object' || obj === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'eq',
|
|
||||||
'ne',
|
|
||||||
'gt',
|
|
||||||
'gte',
|
|
||||||
'lt',
|
|
||||||
'lte',
|
|
||||||
'like',
|
|
||||||
'ilike',
|
|
||||||
'in',
|
|
||||||
'is',
|
|
||||||
].some((op) => op in obj);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
Filter,
|
||||||
|
FilterGroup,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||||
|
|
||||||
|
type ResolvedFilter = Omit<Filter, 'value' | 'stepOutputKey'> & {
|
||||||
|
rightOperand: unknown;
|
||||||
|
leftOperand: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function evaluateFilter(filter: ResolvedFilter): boolean {
|
||||||
|
const leftValue = filter.leftOperand;
|
||||||
|
const rightValue = filter.rightOperand;
|
||||||
|
|
||||||
|
switch (filter.operand) {
|
||||||
|
case 'eq':
|
||||||
|
return leftValue == rightValue;
|
||||||
|
|
||||||
|
case 'ne':
|
||||||
|
return leftValue != rightValue;
|
||||||
|
|
||||||
|
case 'gt':
|
||||||
|
return Number(leftValue) > Number(rightValue);
|
||||||
|
|
||||||
|
case 'gte':
|
||||||
|
return Number(leftValue) >= Number(rightValue);
|
||||||
|
|
||||||
|
case 'lt':
|
||||||
|
return Number(leftValue) < Number(rightValue);
|
||||||
|
|
||||||
|
case 'lte':
|
||||||
|
return Number(leftValue) <= Number(rightValue);
|
||||||
|
|
||||||
|
case 'like':
|
||||||
|
return String(leftValue).includes(String(rightValue));
|
||||||
|
|
||||||
|
case 'ilike':
|
||||||
|
return String(leftValue)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(String(rightValue).toLowerCase());
|
||||||
|
|
||||||
|
case 'in':
|
||||||
|
try {
|
||||||
|
const values = JSON.parse(String(rightValue));
|
||||||
|
|
||||||
|
return Array.isArray(values) && values.includes(leftValue);
|
||||||
|
} catch {
|
||||||
|
const values = String(rightValue)
|
||||||
|
.split(',')
|
||||||
|
.map((v) => v.trim());
|
||||||
|
|
||||||
|
return values.includes(String(leftValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'is':
|
||||||
|
if (String(rightValue).toLowerCase() === 'null') {
|
||||||
|
return leftValue === null || leftValue === undefined;
|
||||||
|
}
|
||||||
|
if (String(rightValue).toLowerCase() === 'not null') {
|
||||||
|
return leftValue !== null && leftValue !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftValue === rightValue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown operand: ${filter.operand}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively evaluates a filter group and its children
|
||||||
|
*/
|
||||||
|
function evaluateFilterGroup(
|
||||||
|
groupId: string,
|
||||||
|
filterGroups: FilterGroup[],
|
||||||
|
filters: ResolvedFilter[],
|
||||||
|
): boolean {
|
||||||
|
const group = filterGroups.find((g) => g.id === groupId);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new Error(`Filter group with id ${groupId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all direct child groups
|
||||||
|
const childGroups = filterGroups
|
||||||
|
.filter((g) => g.parentRecordFilterGroupId === groupId)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a.positionInRecordFilterGroup || 0) -
|
||||||
|
(b.positionInRecordFilterGroup || 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupFilters = filters.filter((f) => f.recordFilterGroupId === groupId);
|
||||||
|
|
||||||
|
const filterResults = groupFilters.map((filter) => evaluateFilter(filter));
|
||||||
|
|
||||||
|
const childGroupResults = childGroups.map((childGroup) =>
|
||||||
|
evaluateFilterGroup(childGroup.id, filterGroups, filters),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allResults = [...filterResults, ...childGroupResults];
|
||||||
|
|
||||||
|
if (allResults.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (group.logicalOperator) {
|
||||||
|
case 'AND':
|
||||||
|
return allResults.every((result) => result);
|
||||||
|
|
||||||
|
case 'OR':
|
||||||
|
return allResults.some((result) => result);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown logical operator: ${group.logicalOperator}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateFilterConditions({
|
||||||
|
filterGroups = [],
|
||||||
|
filters = [],
|
||||||
|
}: {
|
||||||
|
filterGroups?: FilterGroup[];
|
||||||
|
filters?: ResolvedFilter[];
|
||||||
|
}): boolean {
|
||||||
|
if (filterGroups.length === 0 && filters.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterGroups.length > 0) {
|
||||||
|
const groupIds = new Set(filterGroups.map((g) => g.id));
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (!groupIds.has(filter.recordFilterGroupId)) {
|
||||||
|
throw new Error(
|
||||||
|
`Filter group with id ${filter.recordFilterGroupId} not found`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootGroups = filterGroups.filter((g) => !g.parentRecordFilterGroupId);
|
||||||
|
|
||||||
|
if (rootGroups.length === 0 && filters.length > 0) {
|
||||||
|
const filterResults = filters.map((filter) => evaluateFilter(filter));
|
||||||
|
|
||||||
|
return filterResults.every((result) => result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootResults = rootGroups.map((rootGroup) =>
|
||||||
|
evaluateFilterGroup(rootGroup.id, filterGroups, filters),
|
||||||
|
);
|
||||||
|
|
||||||
|
return rootResults.every((result) => result);
|
||||||
|
}
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
WorkflowStepExecutorException,
|
|
||||||
WorkflowStepExecutorExceptionCode,
|
|
||||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
|
||||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
|
||||||
|
|
||||||
export const getPreviousStepOutput = (
|
|
||||||
steps: WorkflowAction[],
|
|
||||||
currentStepId: string,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
context: Record<string, any>,
|
|
||||||
) => {
|
|
||||||
const previousSteps = steps.filter((step) =>
|
|
||||||
step?.nextStepIds?.includes(currentStepId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (previousSteps.length === 0) {
|
|
||||||
throw new WorkflowStepExecutorException(
|
|
||||||
'Filter action must have a previous step',
|
|
||||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousSteps.length > 1) {
|
|
||||||
throw new WorkflowStepExecutorException(
|
|
||||||
'Filter action must have only one previous step',
|
|
||||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStep = previousSteps[0];
|
|
||||||
const previousStepOutput = context[previousStep.id];
|
|
||||||
|
|
||||||
if (!previousStepOutput) {
|
|
||||||
throw new WorkflowStepExecutorException(
|
|
||||||
'Previous step output not found',
|
|
||||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return previousStepOutput;
|
|
||||||
};
|
|
||||||
@ -69,8 +69,16 @@ describe('deleteManyObjectRecordsPermissions', () => {
|
|||||||
expect(response.body.data).toBeDefined();
|
expect(response.body.data).toBeDefined();
|
||||||
expect(response.body.data.deletePeople).toBeDefined();
|
expect(response.body.data.deletePeople).toBeDefined();
|
||||||
expect(response.body.data.deletePeople).toHaveLength(2);
|
expect(response.body.data.deletePeople).toHaveLength(2);
|
||||||
expect(response.body.data.deletePeople[0].id).toBe(personId1);
|
expect(
|
||||||
expect(response.body.data.deletePeople[1].id).toBe(personId2);
|
response.body.data.deletePeople.some(
|
||||||
|
(person: { id: string }) => person.id === personId1,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
response.body.data.deletePeople.some(
|
||||||
|
(person: { id: string }) => person.id === personId2,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete multiple object records when executed by api key', async () => {
|
it('should delete multiple object records when executed by api key', async () => {
|
||||||
|
|||||||
@ -68,7 +68,15 @@ describe('destroyManyObjectRecordsPermissions', () => {
|
|||||||
expect(response.body.data).toBeDefined();
|
expect(response.body.data).toBeDefined();
|
||||||
expect(response.body.data.destroyPeople).toBeDefined();
|
expect(response.body.data.destroyPeople).toBeDefined();
|
||||||
expect(response.body.data.destroyPeople).toHaveLength(2);
|
expect(response.body.data.destroyPeople).toHaveLength(2);
|
||||||
expect(response.body.data.destroyPeople[0].id).toBe(personId1);
|
expect(
|
||||||
expect(response.body.data.destroyPeople[1].id).toBe(personId2);
|
response.body.data.destroyPeople.some(
|
||||||
|
(person: { id: string }) => person.id === personId1,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
response.body.data.destroyPeople.some(
|
||||||
|
(person: { id: string }) => person.id === personId2,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user