Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export type { EnterIfNode, ExitIfNode } from './nodes/branching_nodes';
export type {
EnterIfNode,
ExitIfNode,
EnterConditionBranchNode,
EnterConditionBranchNodeSchema,
ExitConditionBranchNode,
ExitConditionBranchNodeSchema,
} from './nodes/branching_nodes';
export type { EnterForeachNode, ExitForeachNode } from './nodes/loop_nodes';
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,28 @@ import { IfStepSchema } from '../../../spec/schema';
export const EnterIfNodeSchema = z.object({
id: z.string(),
type: z.literal('enter-if'),
trueNodeIds: z.array(z.string()),
falseNodeIds: z.array(z.string()),
exitNodeId: z.string(),
configuration: IfStepSchema.omit({
steps: true,
else: true,
}),
});

export type EnterIfNode = z.infer<typeof EnterIfNodeSchema>;

export const EnterConditionBranchNodeSchema = z.object({
id: z.string(),
type: z.literal('enter-condition-branch'),
condition: z.union([z.string(), z.undefined()]),
});
export type EnterConditionBranchNode = z.infer<typeof EnterConditionBranchNodeSchema>;

export const ExitConditionBranchNodeSchema = z.object({
id: z.string(),
type: z.literal('exit-condition-branch'),
startNodeId: z.string(),
});
export type ExitConditionBranchNode = z.infer<typeof ExitConditionBranchNodeSchema>;

export const ExitIfNodeSchema = z.object({
id: z.string(),
type: z.literal('exit-if'),
Expand Down
11 changes: 10 additions & 1 deletion src/platform/packages/shared/kbn-workflows/types/latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ export {
CreateWorkflowCommandSchema,
} from './v1';

export type { EnterIfNode, ExitIfNode, EnterForeachNode, ExitForeachNode } from './execution';
export type {
EnterIfNode,
ExitIfNode,
EnterConditionBranchNode,
EnterConditionBranchNodeSchema,
ExitConditionBranchNode,
ExitConditionBranchNodeSchema,
EnterForeachNode,
ExitForeachNode,
} from './execution';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { StepImplementation } from '../step_base';
import { WorkflowExecutionRuntimeManager } from '../../workflow_context_manager/workflow_execution_runtime_manager';

export class EnterConditionBranchNodeImpl implements StepImplementation {
constructor(private wfExecutionRuntimeManager: WorkflowExecutionRuntimeManager) {}

public async run(): Promise<void> {
this.wfExecutionRuntimeManager.goToNextStep();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,51 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { EnterIfNode } from '@kbn/workflows';
import { EnterIfNode, EnterConditionBranchNode } from '@kbn/workflows';
import { StepImplementation } from '../step_base';
import { WorkflowExecutionRuntimeManager } from '../../workflow_context_manager/workflow_execution_runtime_manager';

export class EnterIfNodeImpl implements StepImplementation {
constructor(private step: EnterIfNode, private workflowState: WorkflowExecutionRuntimeManager) {}
constructor(
private step: EnterIfNode,
private wfExecutionRuntimeManager: WorkflowExecutionRuntimeManager
) {}

public async run(): Promise<void> {
await this.workflowState.startStep(this.step.id);
const evaluatedConditionResult = this.step.configuration.condition; // must be real condition from step definition
await this.wfExecutionRuntimeManager.startStep(this.step.id);
const successors: any[] = this.wfExecutionRuntimeManager.getNodeSuccessors(this.step.id);

let runningBranch: string[];
let notRunningBranch: string[];
if (successors.some((node) => node.type !== 'enter-condition-branch')) {
throw new Error(
`EnterIfNode with id ${
this.step.id
} must have only 'enter-condition-branch' successors, but found: ${successors
.map((node) => node.type)
.join(', ')}.`
);
}

const thenNode = successors?.find((node) =>
Object.hasOwn(node, 'condition')
) as EnterConditionBranchNode;
// multiple else-if could be implemented similarly to thenNode
const elseNode = successors?.find(
(node) => !Object.hasOwn(node, 'condition')
) as EnterConditionBranchNode;

const evaluatedConditionResult =
typeof thenNode.condition === 'boolean'
? thenNode.condition
: thenNode.condition?.toLowerCase() === 'true'; // must be real condition from step definition)

if (evaluatedConditionResult) {
runningBranch = this.step.trueNodeIds;
notRunningBranch = this.step.falseNodeIds;
this.wfExecutionRuntimeManager.goToStep(thenNode.id);
} else if (elseNode) {
this.wfExecutionRuntimeManager.goToStep(elseNode.id);
} else {
runningBranch = this.step.falseNodeIds;
notRunningBranch = this.step.trueNodeIds;
// in the case when the condition evaluates to false and no else branch is defined
// we go straight to the exit node skipping "then" branch
this.wfExecutionRuntimeManager.goToStep(this.step.exitNodeId);
}

await this.workflowState.skipSteps(notRunningBranch);
this.workflowState.goToStep(runningBranch[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { ExitConditionBranchNode } from '@kbn/workflows';
import { StepImplementation } from '../step_base';
import { WorkflowExecutionRuntimeManager } from '../../workflow_context_manager/workflow_execution_runtime_manager';

export class ExitConditionBranchNodeImpl implements StepImplementation {
constructor(
private step: ExitConditionBranchNode,
private wfExecutionRuntimeManager: WorkflowExecutionRuntimeManager
) {}

public async run(): Promise<void> {
const successors = this.wfExecutionRuntimeManager.getNodeSuccessors(this.step.id);

if (successors.length !== 1) {
throw new Error(
`ExitConditionBranchNode with id ${this.step.id} must have exactly one successor, but found ${successors.length}.`
);
}

if (successors[0].type !== 'exit-if') {
throw new Error(
`ExitConditionBranchNode with id ${this.step.id} must have an exit-if successor, but found ${successors[0].type} with id ${successors[0].id}.`
);
}

// After the branch finishes, we go to the end of If condition
const exitIfNode = successors[0];
this.wfExecutionRuntimeManager.goToStep(exitIfNode.id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import { StepImplementation } from '../step_base';
import { WorkflowExecutionRuntimeManager } from '../../workflow_context_manager/workflow_execution_runtime_manager';

export class ExitIfNodeImpl implements StepImplementation {
constructor(private step: ExitIfNode, private workflowState: WorkflowExecutionRuntimeManager) {}
constructor(
private step: ExitIfNode,
private wfExecutionRuntimeManager: WorkflowExecutionRuntimeManager
) {}

public async run(): Promise<void> {
await this.workflowState.finishStep(this.step.startNodeId);
this.workflowState.goToNextStep();
await this.wfExecutionRuntimeManager.finishStep(this.step.startNodeId);
this.wfExecutionRuntimeManager.goToNextStep();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
*/

export { EnterIfNodeImpl } from './enter_if_node_impl';
export { EnterConditionBranchNodeImpl } from './enter_condition_branch_node_impl';
export { ExitConditionBranchNodeImpl } from './exit_condition_branch_node_impl';
export { ExitIfNodeImpl } from './exit_if_node_impl';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { EnterConditionBranchNodeImpl } from '../enter_condition_branch_node_impl';
import { WorkflowExecutionRuntimeManager } from '../../../workflow_context_manager/workflow_execution_runtime_manager';

describe('EnterConditionBranchNodeImpl', () => {
let wfExecutionRuntimeManagerMock: WorkflowExecutionRuntimeManager;
let impl: EnterConditionBranchNodeImpl;

beforeEach(() => {
wfExecutionRuntimeManagerMock = {
goToNextStep: jest.fn(),
} as any;
impl = new EnterConditionBranchNodeImpl(wfExecutionRuntimeManagerMock);
});

it('should go to next step', async () => {
await impl.run();
expect(wfExecutionRuntimeManagerMock.goToNextStep).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { EnterIfNodeImpl } from '../enter_if_node_impl';
import { WorkflowExecutionRuntimeManager } from '../../../workflow_context_manager/workflow_execution_runtime_manager';
import { EnterConditionBranchNode, EnterIfNode } from '@kbn/workflows';

describe('EnterIfNodeImpl', () => {
let step: EnterIfNode;
let wfExecutionRuntimeManagerMock: WorkflowExecutionRuntimeManager;
let impl: EnterIfNodeImpl;
let startStep: jest.Mock<any, any, any>;
let goToStep: jest.Mock<any, any, any>;
let getNodeSuccessors: jest.Mock<any, any, any>;

beforeEach(() => {
startStep = jest.fn();
goToStep = jest.fn();
getNodeSuccessors = jest.fn();
step = { id: 'testStep', type: 'enter-if', exitNodeId: 'exitIfNode', configuration: {} as any };
wfExecutionRuntimeManagerMock = {
startStep,
goToStep,
getNodeSuccessors,
} as any;
impl = new EnterIfNodeImpl(step, wfExecutionRuntimeManagerMock);

getNodeSuccessors.mockReturnValue([
{
id: 'thenNode',
type: 'enter-condition-branch',
condition: 'true',
} as EnterConditionBranchNode,
{
id: 'elseNode',
type: 'enter-condition-branch',
} as EnterConditionBranchNode,
]);
});

it('should start the step and go to the next step', async () => {
await impl.run();
expect(wfExecutionRuntimeManagerMock.startStep).toHaveBeenCalledWith(step.id);
});

it('should evaluate condition and go to thenNode if condition is true', async () => {
getNodeSuccessors.mockReturnValueOnce([
{
id: 'thenNode',
type: 'enter-condition-branch',
condition: 'true',
} as EnterConditionBranchNode,
{
id: 'elseNode',
type: 'enter-condition-branch',
condition: 'false',
} as EnterConditionBranchNode,
]);
await impl.run();
expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledTimes(1);
expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledWith('thenNode');
});

it('should evaluate condition and go to elseNode if condition is false', async () => {
getNodeSuccessors.mockReturnValueOnce([
{
id: 'thenNode',
type: 'enter-condition-branch',
condition: 'false',
} as EnterConditionBranchNode,
{
id: 'elseNode',
type: 'enter-condition-branch',
} as EnterConditionBranchNode,
]);
await impl.run();
expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledTimes(1);
expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledWith('elseNode');
});

it('should evaluate condition and go to exit node if no else branch is defined', async () => {
getNodeSuccessors.mockReturnValueOnce([
{
id: 'thenNode',
type: 'enter-condition-branch',
condition: 'false',
} as EnterConditionBranchNode,
]);
await impl.run();
expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledTimes(1);
expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledWith('exitIfNode');
});

it('should throw an error if successors are not enter-condition-branch', async () => {
getNodeSuccessors.mockReturnValueOnce([{ id: 'someOtherNode', type: 'some-other-type' }]);
await expect(impl.run()).rejects.toThrow(
`EnterIfNode with id ${step.id} must have only 'enter-condition-branch' successors, but found: some-other-type.`
);
});
});
Loading