Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93581b13a1 | ||
|
|
0c1a7c24e6 | ||
|
|
1fc7091657 | ||
|
|
566793ff00 | ||
|
|
d119fd8a90 | ||
|
|
6db3d2aa0f | ||
|
|
6846aff6dc | ||
|
|
4f115e53c6 | ||
|
|
20f0d38dd1 | ||
|
|
09eca84ecb | ||
|
|
0950a439e4 | ||
|
|
7e67a581e0 | ||
|
|
6c3de53631 | ||
|
|
41c6cba913 | ||
|
|
51e4a14778 | ||
|
|
aa804e2fc5 | ||
|
|
07f72dae49 | ||
|
|
910d5c9858 | ||
|
|
6ef1727eb8 | ||
|
|
fce13986b4 | ||
|
|
c8f3bb4312 |
No files matched your search
+21
-10
@@ -1,34 +1,45 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.3.7
|
||||
qqq-orb: kingsrook/qqq-orb@0.3.12
|
||||
|
||||
workflows:
|
||||
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
|
||||
tags:
|
||||
only: []
|
||||
only: []
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
|
||||
publish_feature:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: feature
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- feature/.*
|
||||
tags:
|
||||
only: [/publish.*/]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
@@ -38,18 +49,18 @@ workflows:
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [ /v.*/ ]
|
||||
only: [/v.*/]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: hotfix
|
||||
filters:
|
||||
branches:
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
|
||||
<properties>
|
||||
<revision>0.3.0-SNAPSHOT</revision>
|
||||
<revision>0.114.0-SNAPSHOT</revision>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
|
||||
@@ -28,7 +28,7 @@ package com.kingsrook.qbits.workflows.definition;
|
||||
** ONE is a regular statement, that just flows to its next.
|
||||
** CONTAINER is itself a no-op, but it wraps (for UI reasons) a sequence
|
||||
** TWO is for an if/else
|
||||
** Unclear exactly how we'd do variable number of outbounds (e.g., a switch)
|
||||
** VARIABLE is for a variable number - 1 or more
|
||||
***************************************************************************/
|
||||
public enum OutboundLinkMode
|
||||
{
|
||||
@@ -36,4 +36,5 @@ public enum OutboundLinkMode
|
||||
ONE,
|
||||
CONTAINER,
|
||||
TWO,
|
||||
VARIABLE
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepExecutorInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
@@ -56,8 +57,10 @@ public class WorkflowStepType implements Serializable
|
||||
private OutboundLinkMode outboundLinkMode;
|
||||
private List<OutboundLinkOption> outboundLinkOptions;
|
||||
|
||||
private ArrayList<QFieldMetaData> inputFields;
|
||||
private ArrayList<String> inputWidgetNames;
|
||||
private Set<String> allowedDescendantStepTypes;
|
||||
|
||||
private ArrayList<QFieldMetaData> inputFields = new ArrayList<>();
|
||||
private ArrayList<String> inputWidgetNames = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +86,17 @@ public class WorkflowStepType implements Serializable
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* for a variable-output type step, it must dynamically define its outbound
|
||||
* links
|
||||
***************************************************************************/
|
||||
public List<OutboundLinkOption> getDynamicOutboundLinkOptions(Integer workflowId, Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@@ -409,6 +423,7 @@ public class WorkflowStepType implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Getter for validator
|
||||
* @see #withValidator(QCodeReference)
|
||||
@@ -445,4 +460,42 @@ public class WorkflowStepType implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Getter for allowedDescendantStepTypes
|
||||
* @see #withAllowedDescendantStepTypes(Set)
|
||||
*******************************************************************************/
|
||||
public Set<String> getAllowedDescendantStepTypes()
|
||||
{
|
||||
return (this.allowedDescendantStepTypes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Setter for allowedDescendantStepTypes
|
||||
* @see #withAllowedDescendantStepTypes(Set)
|
||||
*******************************************************************************/
|
||||
public void setAllowedDescendantStepTypes(Set<String> allowedDescendantStepTypes)
|
||||
{
|
||||
this.allowedDescendantStepTypes = allowedDescendantStepTypes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Fluent setter for allowedDescendantStepTypes
|
||||
*
|
||||
* @param allowedDescendantStepTypes
|
||||
* to limit what step types can be placed inside this step type within a graph.
|
||||
* for example,
|
||||
* @return this
|
||||
*******************************************************************************/
|
||||
public WorkflowStepType withAllowedDescendantStepTypes(Set<String> allowedDescendantStepTypes)
|
||||
{
|
||||
this.allowedDescendantStepTypes = allowedDescendantStepTypes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -25,16 +25,19 @@ package com.kingsrook.qbits.workflows.definition;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeExecutorInterface;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeTesterInterface;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeValidatorInterface;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
@@ -50,9 +53,11 @@ public class WorkflowType implements Serializable
|
||||
private String label;
|
||||
private String description;
|
||||
private QCodeReference executor;
|
||||
private QCodeReference validator;
|
||||
private QCodeReference tester;
|
||||
|
||||
private ArrayList<WorkflowStepTypeCategory> stepTypeCategories;
|
||||
protected Map<String, List<QHelpContent>> helpContent;
|
||||
private ArrayList<WorkflowStepTypeCategory> stepTypeCategories;
|
||||
|
||||
|
||||
|
||||
@@ -81,6 +86,11 @@ public class WorkflowType implements Serializable
|
||||
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: executor ", executor, WorkflowTypeExecutorInterface.class);
|
||||
}
|
||||
|
||||
if(validator != null)
|
||||
{
|
||||
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: validator ", validator, WorkflowTypeValidatorInterface.class);
|
||||
}
|
||||
|
||||
if(tester != null)
|
||||
{
|
||||
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: tester ", tester, WorkflowTypeTesterInterface.class);
|
||||
@@ -266,6 +276,8 @@ public class WorkflowType implements Serializable
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Getter for tester
|
||||
* @see #withTester(QCodeReference)
|
||||
@@ -303,4 +315,92 @@ public class WorkflowType implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for helpContent
|
||||
*******************************************************************************/
|
||||
public Map<String, List<QHelpContent>> getHelpContent()
|
||||
{
|
||||
return (this.helpContent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for helpContent
|
||||
*******************************************************************************/
|
||||
public void setHelpContent(Map<String, List<QHelpContent>> helpContent)
|
||||
{
|
||||
this.helpContent = helpContent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for helpContent
|
||||
*******************************************************************************/
|
||||
public WorkflowType withHelpContent(Map<String, List<QHelpContent>> helpContent)
|
||||
{
|
||||
this.helpContent = helpContent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for helpContent
|
||||
*******************************************************************************/
|
||||
public WorkflowType withHelpContent(String key, QHelpContent helpContent)
|
||||
{
|
||||
if(this.helpContent == null)
|
||||
{
|
||||
this.helpContent = new HashMap<>();
|
||||
}
|
||||
if(!this.helpContent.containsKey(key))
|
||||
{
|
||||
this.helpContent.put(key, new ArrayList<>());
|
||||
}
|
||||
|
||||
this.helpContent.get(key).add(helpContent);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Getter for validator
|
||||
* @see #withValidator(QCodeReference)
|
||||
*******************************************************************************/
|
||||
public QCodeReference getValidator()
|
||||
{
|
||||
return (this.validator);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Setter for validator
|
||||
* @see #withValidator(QCodeReference)
|
||||
*******************************************************************************/
|
||||
public void setValidator(QCodeReference validator)
|
||||
{
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Fluent setter for validator
|
||||
*
|
||||
* @param validator reference to implementation of
|
||||
* {@link com.kingsrook.qbits.workflows.execution.WorkflowTypeValidatorInterface}
|
||||
*
|
||||
* @return this
|
||||
*******************************************************************************/
|
||||
public WorkflowType withValidator(QCodeReference validator)
|
||||
{
|
||||
this.validator = validator;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -203,6 +203,8 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
||||
/***************************************************************************
|
||||
* plug in to the QQQ Help Content system, to allow help content to set
|
||||
* descriptions and labels for workflow steps.
|
||||
*
|
||||
* Note: helpContent can come in here as null, if it has been, e.g., deleted!
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void acceptHelpContent(QInstance qInstance, QHelpContent helpContent, Map<String, String> nameValuePairs)
|
||||
@@ -215,11 +217,18 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
||||
{
|
||||
if("description".equals(nameValuePairs.get("slot")))
|
||||
{
|
||||
workflowStepType.setDescription(helpContent.getContentAsHtml());
|
||||
workflowStepType.setDescription(helpContent == null ? null : helpContent.getContentAsHtml());
|
||||
}
|
||||
else if("label".equals(nameValuePairs.get("slot")))
|
||||
{
|
||||
workflowStepType.setLabel(helpContent.getContent());
|
||||
if(helpContent != null)
|
||||
{
|
||||
workflowStepType.setLabel(helpContent.getContent());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Not removing workflow step type label after deleted help content (original label is not known...", logPair("name", workflowStepTypeName));
|
||||
}
|
||||
}
|
||||
else if(nameValuePairs.containsKey("field"))
|
||||
{
|
||||
@@ -229,9 +238,10 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
||||
.withType(QFieldType.STRING)
|
||||
.withLabel("Step Description")
|
||||
.withName("qswdDescription")
|
||||
.withHelpContent(helpContent)
|
||||
.withGridColumns(12);
|
||||
|
||||
newField.setHelpContents(helpContent == null ? null : ListBuilder.of(helpContent));
|
||||
|
||||
List<QFieldMetaData> existingFields = workflowStepType.getInputFields();
|
||||
existingFields.add(newField);
|
||||
workflowStepType.setInputFields(existingFields);
|
||||
@@ -242,13 +252,24 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
||||
{
|
||||
if(nameValuePairs.get("field").equals(fieldMetaData.getName()))
|
||||
{
|
||||
fieldMetaData.setHelpContents(ListBuilder.of(helpContent));
|
||||
fieldMetaData.setHelpContents(helpContent == null ? null : ListBuilder.of(helpContent));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(nameValuePairs.containsKey("workflow") && nameValuePairs.containsKey("field"))
|
||||
{
|
||||
if(workflowTypes != null)
|
||||
{
|
||||
for(String workflowTypeName : workflowTypes.keySet())
|
||||
{
|
||||
String key = "workflow:" + nameValuePairs.get("workflow") + ";field:" + nameValuePairs.get("field");
|
||||
workflowTypes.get(workflowTypeName).withHelpContent(key, helpContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRunLogStep;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* state data carried throughout the {@link WorkflowExecutor} - which is
|
||||
* useful to "leak out" in a managed way, e.g., for step types that do their
|
||||
* own more sophisticated kind of execution, e.g., {@link WorkflowMultiForkingStepExecutorInterface}
|
||||
***************************************************************************/
|
||||
record ExecutionPayload(Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linkMap, WorkflowTypeExecutorInterface workflowTypeExecutor, AtomicInteger seqNo, List<WorkflowRunLogStep> logStepList)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution;
|
||||
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* given a graph, find the first "join point" where multiple forks come together.
|
||||
* Credit: https://chatgpt.com/share/68000e8f-879c-8011-bb33-0564eb77a1f2
|
||||
*******************************************************************************/
|
||||
public class FindFirstJoinPoint
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static Integer executeForFromStepNo(Integer fromStepNo, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksMapByFromStepNo)
|
||||
{
|
||||
List<Integer> startLinks = CollectionUtils.nonNullList(linksMapByFromStepNo.get(fromStepNo)).stream().map(ws -> ws.getToStepNo()).toList();
|
||||
return execute(startLinks, stepMap, linksMapByFromStepNo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static Integer executeForLinks(Collection<WorkflowLink> startLinks, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksMapByFromStepNo)
|
||||
{
|
||||
return execute(startLinks.stream().map(ws -> ws.getToStepNo()).toList(), stepMap, linksMapByFromStepNo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@SuppressWarnings("Java8MapApi")
|
||||
public static Integer execute(Collection<Integer> startStepNos, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksMapByFromStepNo)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// visited: map each node to a set of source step numbers that have reached it. //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<Integer, Integer> visited = new ListingHash<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Initialize the multi-source BFS queue. Each entry carries the current node id and its originating source. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Deque<BFSQueueNode> queue = new ArrayDeque<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Seed the queue with each starting node, marking each as visited by itself. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
for(Integer node : startStepNos)
|
||||
{
|
||||
visited.add(node, node);
|
||||
queue.add(new BFSQueueNode(node, node));
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// Perform the multi-source BFS. //
|
||||
///////////////////////////////////
|
||||
while(!queue.isEmpty())
|
||||
{
|
||||
BFSQueueNode firstNode = queue.removeFirst();
|
||||
Integer current = firstNode.current;
|
||||
Integer source = firstNode.source;
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Get the list of outgoing links/neighbors for the current node. //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
List<WorkflowLink> neighbors = Objects.requireNonNullElse(linksMapByFromStepNo.get(current), Collections.emptyList());
|
||||
for(WorkflowLink neighbor : neighbors)
|
||||
{
|
||||
Integer neighborId = neighbor.getToStepNo();
|
||||
if(neighborId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Retrieve or initialize the set of sources that visited this neighbor. //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
List<Integer> sourcesSet = visited.get(neighborId);
|
||||
if(sourcesSet == null)
|
||||
{
|
||||
sourcesSet = new ArrayList<>();
|
||||
visited.put(neighborId, sourcesSet);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// If the current source has not visited the neighbor yet, add it. //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
if(!sourcesSet.contains(source))
|
||||
{
|
||||
sourcesSet.add(source);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Check if the neighbor was reached from multiple sources. //
|
||||
// todo: should this be it has to equal all of them? //
|
||||
// if(sourcesSet.size() == startStepNos.size()) //
|
||||
//////////////////////////////////////////////////////////////
|
||||
if(sourcesSet.size() > 1)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// This is the first join point encountered – return the corresponding WorkflowStep. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
return (stepMap.get(neighborId).getStepNo());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// Continue the BFS from this neighbor for the current source. //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
queue.push(new BFSQueueNode(neighborId, source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// If no join point was found across all paths, return null. //
|
||||
///////////////////////////////////////////////////////////////
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private record BFSQueueNode(Integer current, Integer source)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,6 +42,8 @@ public class WorkflowExecutionContext
|
||||
private QBackendTransaction transaction;
|
||||
private WorkflowRunLog workflowRunLog;
|
||||
|
||||
private WorkflowExecutor executor;
|
||||
|
||||
private boolean isTestRun = false;
|
||||
|
||||
|
||||
@@ -235,4 +237,26 @@ public class WorkflowExecutionContext
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Getter for executor
|
||||
* @see #setExecutor(WorkflowExecutor)
|
||||
*******************************************************************************/
|
||||
WorkflowExecutor getExecutor()
|
||||
{
|
||||
return (this.executor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Setter for executor
|
||||
* @param executor the object executing the workflow.
|
||||
*******************************************************************************/
|
||||
void setExecutor(WorkflowExecutor executor)
|
||||
{
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||
@@ -71,6 +72,7 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
private WorkflowRunLog inputWorkflowRunLog;
|
||||
|
||||
private Stack<Integer> containerStack = new Stack<>();
|
||||
private ExecutionPayload payload;
|
||||
|
||||
|
||||
|
||||
@@ -167,31 +169,10 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
///////////////
|
||||
// step loop //
|
||||
///////////////
|
||||
Integer stepNo = workflowRevision.getStartStepNo();
|
||||
int seqNo = 1;
|
||||
while(stepNo != null)
|
||||
{
|
||||
WorkflowStep step = stepMap.get(stepNo);
|
||||
if(step == null)
|
||||
{
|
||||
throw new QException("Step not found by stepNo: " + stepNo);
|
||||
}
|
||||
|
||||
WorkflowRunLogStep workflowRunLogStep = new WorkflowRunLogStep();
|
||||
workflowRunLogStep.setWorkflowStepId(step.getId());
|
||||
workflowRunLogStep.setSeqNo(seqNo);
|
||||
workflowRunLogStep.setStartTimestamp(Instant.now());
|
||||
logStepList.add(workflowRunLogStep);
|
||||
|
||||
WorkflowStepOutput workflowStepOutput = executeStep(step, workflowTypeExecutor, context);
|
||||
workflowRunLogStep.setOutputData(ValueUtils.getValueAsString(workflowStepOutput.outputData()));
|
||||
workflowRunLogStep.setMessage(workflowStepOutput.message());
|
||||
|
||||
stepNo = getNextStepNo(workflowStepOutput.outputData(), step, linkMap, stepMap, false);
|
||||
|
||||
workflowRunLogStep.setEndTimestamp(Instant.now());
|
||||
seqNo++;
|
||||
}
|
||||
ExecutionPayload payload = new ExecutionPayload(stepMap, linkMap, workflowTypeExecutor, new AtomicInteger(1), logStepList);
|
||||
this.payload = payload;
|
||||
context.setExecutor(this);
|
||||
runStepLoop(workflowRevision.getStartStepNo(), context, null);
|
||||
|
||||
workflowTypeExecutor.postRun(context);
|
||||
|
||||
@@ -239,6 +220,51 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* @return true if the workflow should immediately stop
|
||||
***************************************************************************/
|
||||
boolean runStepLoop(Integer stepNo, WorkflowExecutionContext context, Integer stopStepNo) throws QException
|
||||
{
|
||||
while(stepNo != null)
|
||||
{
|
||||
WorkflowStep step = payload.stepMap().get(stepNo);
|
||||
if(step == null)
|
||||
{
|
||||
throw new QException("Step not found by stepNo: " + stepNo);
|
||||
}
|
||||
|
||||
WorkflowRunLogStep workflowRunLogStep = new WorkflowRunLogStep();
|
||||
workflowRunLogStep.setWorkflowStepId(step.getId());
|
||||
workflowRunLogStep.setStartTimestamp(Instant.now());
|
||||
|
||||
WorkflowStepOutput workflowStepOutput = executeStep(step, payload, context);
|
||||
|
||||
stepNo = getNextStepNo(workflowStepOutput.outputData(), step, payload.linkMap(), payload.stepMap(), false);
|
||||
|
||||
workflowRunLogStep.setEndTimestamp(Instant.now());
|
||||
workflowRunLogStep.setOutputData(ValueUtils.getValueAsString(workflowStepOutput.outputData()));
|
||||
workflowRunLogStep.setMessage(workflowStepOutput.message());
|
||||
workflowRunLogStep.setSeqNo(payload.seqNo().getAndIncrement());
|
||||
payload.logStepList().add(workflowRunLogStep);
|
||||
|
||||
if(stopStepNo != null && Objects.equals(stepNo, stopStepNo))
|
||||
{
|
||||
LOG.debug("Stopping at requested step", logPair("stopStepNo", stepNo));
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(workflowStepOutput.shouldWorkflowImmediatelyStop())
|
||||
{
|
||||
LOG.info("Stopping step loop due to step output indicating workflow should immediately stop", logPair("stepNo", stepNo));
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@@ -332,13 +358,23 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
}
|
||||
}
|
||||
|
||||
Integer fromStepNo = fromStep.getStepNo();
|
||||
List<WorkflowLink> links = linkMap.get(fromStepNo);
|
||||
Class<? extends Serializable> stepOutputClass = stepOutput == null ? null : stepOutput.getClass();
|
||||
Integer fromStepNo = fromStep.getStepNo();
|
||||
List<WorkflowLink> links = linkMap.get(fromStepNo);
|
||||
|
||||
if(OutboundLinkMode.VARIABLE.equals(fromWorkflowStepType.getOutboundLinkMode()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// after a VARIABLE step (where its branches were all executed in //
|
||||
// processMultiForkingStep), find the first join point, and step to there. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
Integer joinStepNo = FindFirstJoinPoint.execute(links.stream().map(wl -> wl.getToStepNo()).toList(), stepMap, linkMap);
|
||||
return (joinStepNo);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// look for a link between the fromStepNo matching the stepOutput / condition value //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
List<String> evaluatedConditionValues = new ArrayList<>();
|
||||
for(WorkflowLink link : CollectionUtils.nonNullList(links))
|
||||
{
|
||||
if(link.getConditionValue() == null)
|
||||
@@ -350,8 +386,10 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
}
|
||||
else
|
||||
{
|
||||
evaluatedConditionValues.add(link.getConditionValue());
|
||||
try
|
||||
{
|
||||
Class<? extends Serializable> stepOutputClass = stepOutput == null ? null : stepOutput.getClass();
|
||||
Serializable valueAsType = stepOutputClass == null ? link.getConditionValue() : ValueUtils.getValueAsType(stepOutputClass, link.getConditionValue());
|
||||
if(Objects.equals(valueAsType, stepOutput))
|
||||
{
|
||||
@@ -365,6 +403,11 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
}
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(evaluatedConditionValues))
|
||||
{
|
||||
LOG.info("Evaluated at least 1 link condition value, but found no matches", logPair("evaluatedConditionValues", evaluatedConditionValues));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// if we didn't find a next-step, but there is something on the stack, //
|
||||
// then look for a pop out of that frame //
|
||||
@@ -385,7 +428,7 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private WorkflowStepOutput executeStep(WorkflowStep step, WorkflowTypeExecutorInterface workflowTypeExecutor, WorkflowExecutionContext context) throws QException
|
||||
private WorkflowStepOutput executeStep(WorkflowStep step, ExecutionPayload payload, WorkflowExecutionContext context) throws QException
|
||||
{
|
||||
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
||||
if(workflowStepType == null)
|
||||
@@ -399,8 +442,7 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
}
|
||||
|
||||
WorkflowStepExecutorInterface workflowStepExecutor = QCodeLoader.getAdHoc(WorkflowStepExecutorInterface.class, workflowStepType.getExecutor());
|
||||
|
||||
workflowTypeExecutor.preStep(step, context);
|
||||
payload.workflowTypeExecutor().preStep(step, context);
|
||||
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(step.getInputValuesJson());
|
||||
Map<String, Object> mapValues = jsonObject.toMap();
|
||||
@@ -415,13 +457,23 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
||||
}
|
||||
|
||||
WorkflowStepOutput workflowStepOutput = workflowStepExecutor.execute(step, inputValues, context);
|
||||
workflowStepOutput = workflowTypeExecutor.postStep(step, context, workflowStepOutput);
|
||||
workflowStepOutput = payload.workflowTypeExecutor().postStep(step, context, workflowStepOutput);
|
||||
|
||||
return workflowStepOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
ExecutionPayload getPayload()
|
||||
{
|
||||
return (this.payload);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRunLogStep;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* interface for workflow step executors that are of a variable output type,
|
||||
* and which can execute 0 or more of their outbound links.
|
||||
***************************************************************************/
|
||||
public interface WorkflowMultiForkingStepExecutorInterface extends WorkflowStepExecutorInterface
|
||||
{
|
||||
QLogger LOG = QLogger.getLogger(WorkflowMultiForkingStepExecutorInterface.class);
|
||||
|
||||
/***************************************************************************
|
||||
* the Execute method for such steps should perform an executor.runStepLoop
|
||||
* for each of the outbound links of the step (assuming they are supposed
|
||||
* allowed to run).
|
||||
***************************************************************************/
|
||||
@Override
|
||||
default WorkflowStepOutput execute(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// get the various sub-workflows ("fork links") to run from the step //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
WorkflowExecutor executor = context.getExecutor();
|
||||
ExecutionPayload payload = executor.getPayload();
|
||||
|
||||
List<WorkflowLink> forkLinks = payload.linkMap().get(step.getStepNo());
|
||||
preRun(step, inputValues, context);
|
||||
sortForks(forkLinks, step, inputValues, context);
|
||||
|
||||
Integer stopStepNo = FindFirstJoinPoint.execute(forkLinks.stream().map(wl -> wl.getToStepNo()).toList(), payload.stepMap(), payload.linkMap());
|
||||
boolean shouldWorkflowImmediatelyStop = false;
|
||||
for(WorkflowLink link : forkLinks)
|
||||
{
|
||||
WorkflowRunLogStep workflowRunLogStep = new WorkflowRunLogStep();
|
||||
workflowRunLogStep.setWorkflowStepId(step.getId());
|
||||
workflowRunLogStep.setSeqNo(payload.seqNo().getAndIncrement());
|
||||
workflowRunLogStep.setStartTimestamp(Instant.now());
|
||||
|
||||
WorkflowStepOutput workflowStepOutput = conditionallyExecuteFork(step, link, inputValues, context);
|
||||
if(Objects.equals(workflowStepOutput.outputData(), true))
|
||||
{
|
||||
shouldWorkflowImmediatelyStop = executor.runStepLoop(link.getToStepNo(), context, stopStepNo);
|
||||
}
|
||||
|
||||
workflowRunLogStep.setOutputData(ValueUtils.getValueAsString(workflowStepOutput.outputData()));
|
||||
workflowRunLogStep.setMessage(workflowStepOutput.message());
|
||||
workflowRunLogStep.setEndTimestamp(Instant.now());
|
||||
payload.logStepList().add(workflowRunLogStep);
|
||||
|
||||
if(shouldWorkflowImmediatelyStop)
|
||||
{
|
||||
LOG.info("Stopping fork-link loop, due to step output indicating workflow should immediately stop", logPair("linkStartStepNo", link.getToStepNo()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WorkflowStepOutput workflowStepOutput = makeWorkflowStepOutput(step, inputValues, context);
|
||||
postRun(step, inputValues, context);
|
||||
|
||||
if(shouldWorkflowImmediatelyStop)
|
||||
{
|
||||
workflowStepOutput = new WorkflowStepOutput(workflowStepOutput.outputData(), workflowStepOutput.message(), true);
|
||||
}
|
||||
|
||||
return workflowStepOutput;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* make sure the forks are executed in the order that the step expects.
|
||||
***************************************************************************/
|
||||
void sortForks(List<WorkflowLink> forkLinks, WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context);
|
||||
|
||||
/***************************************************************************
|
||||
* decide if a particular fork should be executed - and if so, then do so.
|
||||
***************************************************************************/
|
||||
WorkflowStepOutput conditionallyExecuteFork(WorkflowStep step, WorkflowLink fork, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException;
|
||||
|
||||
/***************************************************************************
|
||||
* make output for the entire step, e.g., taking into account all of its
|
||||
* forks.
|
||||
***************************************************************************/
|
||||
WorkflowStepOutput makeWorkflowStepOutput(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException;
|
||||
|
||||
/***************************************************************************
|
||||
* optional code to run before anything else in this interface
|
||||
***************************************************************************/
|
||||
default void preRun(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* optional code to run after anything else in this interface
|
||||
***************************************************************************/
|
||||
default void postRun(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,26 @@ import java.io.Serializable;
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public record WorkflowStepOutput(Serializable outputData, String message)
|
||||
public record WorkflowStepOutput(Serializable outputData, String message, boolean shouldWorkflowImmediatelyStop)
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public WorkflowStepOutput(Serializable outputData, String message)
|
||||
{
|
||||
this(outputData, message, false);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public WorkflowStepOutput(Serializable outputData)
|
||||
{
|
||||
this(outputData, null);
|
||||
this(outputData, null, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +58,7 @@ public record WorkflowStepOutput(Serializable outputData, String message)
|
||||
*******************************************************************************/
|
||||
public WorkflowStepOutput()
|
||||
{
|
||||
this(null, null);
|
||||
this(null, null, false);
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* interface for the code that validates the overall structure of a workflow,
|
||||
* e.g., when it's being saved. This would be where you'd validate relationships
|
||||
* between steps (e.g., this step must have some other step within it)
|
||||
*******************************************************************************/
|
||||
public interface WorkflowTypeValidatorInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
* validate that a workflow can be saved.
|
||||
*
|
||||
* @param workflow the workflow record under which a new revision is being saved
|
||||
* @param workflowRevision the new revision record that's been built
|
||||
* @param workflowSteps list of workflow steps being saved
|
||||
* @param workflowLinks list of workflow links being saved
|
||||
* @param errors out param - any validation errors should be added to this list.
|
||||
***************************************************************************/
|
||||
void validate(QRecord workflow, QRecord workflowRevision, List<WorkflowStep> workflowSteps, List<WorkflowLink> workflowLinks, List<String> errors) throws QException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* list of nodes in a graph representation of a workflow
|
||||
***************************************************************************/
|
||||
public record NodeSequence(List<StepNode> nodes)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public NodeSequence()
|
||||
{
|
||||
this(new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public void add(StepNode node)
|
||||
{
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public StepNode get(int i)
|
||||
{
|
||||
return nodes.get(i);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public int size()
|
||||
{
|
||||
return (nodes.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Single step, plus, if it opens a nested scope (e.g., branches, or a container)
|
||||
* then those subSequences, along with their conditionalValues (guards/labels)
|
||||
* as map keys
|
||||
***************************************************************************/
|
||||
public record StepNode(WorkflowStep step, Map<String, NodeSequence> subSequences)
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor for step w/o subSequences
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StepNode(WorkflowStep step)
|
||||
{
|
||||
this(step, new HashMap<>());
|
||||
}
|
||||
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||
import com.kingsrook.qbits.workflows.execution.FindFirstJoinPoint;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Class that can convert a list of steps & links (as a workflow revision is
|
||||
* stored in a database) to a hierarchical graph data structure - a list of nodes,
|
||||
* each of which is a step, and optionally a map of lists of sub-branches
|
||||
* (which can be more useful for various kinds of processing).
|
||||
*******************************************************************************/
|
||||
public class StepNodeGraphBuilder
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(StepNodeGraphBuilder.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public static NodeSequence buildNodeSequence(Integer startStepNo, List<WorkflowStep> workflowSteps, List<WorkflowLink> workflowLinks)
|
||||
{
|
||||
Map<Integer, WorkflowStep> stepMap = CollectionUtils.listToMap(workflowSteps, ws -> ws.getStepNo());
|
||||
ListingHash<Integer, WorkflowLink> linksFromMap = new ListingHash<>();
|
||||
Map<Integer, Set<Integer>> linksToMap = new HashMap<>();
|
||||
|
||||
for(WorkflowLink workflowLink : workflowLinks)
|
||||
{
|
||||
linksFromMap.add(workflowLink.getFromStepNo(), workflowLink);
|
||||
|
||||
if(workflowLink.getToStepNo() != null)
|
||||
{
|
||||
Set<Integer> subToSet = linksToMap.computeIfAbsent(workflowLink.getToStepNo(), k -> new HashSet<>());
|
||||
subToSet.add(workflowLink.getFromStepNo());
|
||||
}
|
||||
}
|
||||
|
||||
NodeSequence nodeSequence = new NodeSequence();
|
||||
buildNodeSequence(nodeSequence, stepMap.get(startStepNo), stepMap, linksFromMap, null, 0);
|
||||
return (nodeSequence);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private static void buildNodeSequence(NodeSequence nodeSequence, WorkflowStep startStep, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksFromMap, Integer joinStepNo, Integer depth)
|
||||
{
|
||||
WorkflowsRegistry workflowsRegistry = WorkflowsRegistry.of(QContext.getQInstance());
|
||||
|
||||
WorkflowStep currentWorkflowStep = startStep;
|
||||
while(currentWorkflowStep != null)
|
||||
{
|
||||
WorkflowStepType workflowStepType = workflowsRegistry.getWorkflowStepType(currentWorkflowStep.getWorkflowStepTypeName());
|
||||
if(workflowStepType == null)
|
||||
{
|
||||
LOG.warn("Unrecognized workflow step type: " + currentWorkflowStep.getWorkflowStepTypeName());
|
||||
return;
|
||||
}
|
||||
|
||||
if(Objects.equals(currentWorkflowStep.getStepNo(), joinStepNo))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if we've reached the join step, it's the end of this sequence, so return //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
StepNode stepNode = new StepNode(currentWorkflowStep);
|
||||
nodeSequence.add(stepNode);
|
||||
|
||||
List<WorkflowLink> outboundLinks = linksFromMap.get(currentWorkflowStep.getStepNo());
|
||||
if(CollectionUtils.nullSafeIsEmpty(outboundLinks))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// if no outbound links, then we're at the end of this sequence, so return //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
else if(outboundLinks.size() == 1 && !StringUtils.hasContent(outboundLinks.get(0).getConditionValue())) // ?? avoid being tripped by a branch w/ only 1 side populated at the end of the graph
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// 1 outbound link here means we're not in a switch/if-else/fork //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
WorkflowLink outboundLink = outboundLinks.get(0);
|
||||
currentWorkflowStep = outboundLink.getToStepNo() == null ? null : stepMap.get(outboundLink.getToStepNo());
|
||||
|
||||
////////////////////////////////
|
||||
// todo is this == null okay? //
|
||||
////////////////////////////////
|
||||
if(currentWorkflowStep == null || currentWorkflowStep.getStepNo().equals(joinStepNo))
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// this means we're ending a nested (recursive) call //
|
||||
///////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(workflowStepType.getOutboundLinkMode().equals(OutboundLinkMode.TWO) || workflowStepType.getOutboundLinkMode().equals(OutboundLinkMode.VARIABLE))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this means we are starting a fork. find where it ends - where the branches first re-converge //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Integer branchJoin = FindFirstJoinPoint.executeForLinks(outboundLinks, stepMap, linksFromMap);
|
||||
|
||||
////////////////////////////////////////////
|
||||
// for each branch, make a recursive call //
|
||||
////////////////////////////////////////////
|
||||
for(WorkflowLink outboundLink : outboundLinks)
|
||||
{
|
||||
String conditionValue = outboundLink.getConditionValue();
|
||||
NodeSequence branchSequence = new NodeSequence();
|
||||
stepNode.subSequences().put(conditionValue, branchSequence);
|
||||
|
||||
if(branchJoin != null && Objects.equals(branchJoin, outboundLink.getToStepNo()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// this indicates an empty branch - so continue to next outbound link //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a step out of the branch (e.g., to a join), then don't process it in here. //
|
||||
// also though, this line doesn't actually get hit, so... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(Objects.equals(outboundLink.getToStepNo(), joinStepNo))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(outboundLink.getToStepNo() != null)
|
||||
{
|
||||
buildNodeSequence(branchSequence, stepMap.get(outboundLink.getToStepNo()), stepMap, linksFromMap, branchJoin, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// after handling all the branches, then continue the loop with the join step //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
currentWorkflowStep = branchJoin == null ? null : stepMap.get(branchJoin);
|
||||
}
|
||||
else if(workflowStepType.getOutboundLinkMode().equals(OutboundLinkMode.CONTAINER))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if the container is non-empty, there should be a push link for it //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
Optional<WorkflowLink> pushLink = outboundLinks.stream().filter(ol -> "push".equals(ol.getConditionValue())).findFirst();
|
||||
if(pushLink.isPresent())
|
||||
{
|
||||
if(pushLink.get().getToStepNo() != null)
|
||||
{
|
||||
NodeSequence containerSequence = new NodeSequence();
|
||||
stepNode.subSequences().put("push", containerSequence);
|
||||
buildNodeSequence(containerSequence, stepMap.get(pushLink.get().getToStepNo()), stepMap, linksFromMap, null, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<WorkflowLink> popLink = outboundLinks.stream().filter(ol -> "pop".equals(ol.getConditionValue())).findFirst();
|
||||
if(popLink.isPresent())
|
||||
{
|
||||
if(popLink.get().getToStepNo() != null)
|
||||
{
|
||||
currentWorkflowStep = stepMap.get(popLink.get().getToStepNo());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("expected to have multiple outbound links for a step of type: " + currentWorkflowStep.getWorkflowStepTypeName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+10
@@ -98,6 +98,11 @@ public class RecordWorkflowInputRecordFilterWidget extends AbstractWidgetRendere
|
||||
public static void applyApiNameAndVersionFromInputToWidgetData(RenderWidgetInput input, FilterAndColumnsSetupData widgetData)
|
||||
{
|
||||
String apiName = input.getQueryParams().get("workflowRevision.apiName");
|
||||
if(!StringUtils.hasContent(apiName))
|
||||
{
|
||||
apiName = input.getQueryParams().get("apiName");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(apiName))
|
||||
{
|
||||
widgetData.setApiName(apiName);
|
||||
@@ -113,6 +118,11 @@ public class RecordWorkflowInputRecordFilterWidget extends AbstractWidgetRendere
|
||||
}
|
||||
|
||||
String apiVersion = input.getQueryParams().get("workflowRevision.apiVersion");
|
||||
if(!StringUtils.hasContent(apiVersion))
|
||||
{
|
||||
apiVersion = input.getQueryParams().get("apiVersion");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(apiVersion))
|
||||
{
|
||||
widgetData.setApiVersion(apiVersion);
|
||||
|
||||
+18
-8
@@ -83,18 +83,28 @@ public class RecordWorkflowUtils
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
* expects name in inputValues to be "queryFilterJson"
|
||||
***************************************************************************/
|
||||
public static QQueryFilter getFilterFromInput(Map<String, ?> inputValues) throws QException
|
||||
{
|
||||
Object object = inputValues.get("queryFilterJson");
|
||||
if(object == null)
|
||||
{
|
||||
throw (new QException("Could not find filter in input values"));
|
||||
}
|
||||
|
||||
return getFilterFromJsonOrObject(object);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* supported input types are: QQueryFilter, String (json), Map.
|
||||
***************************************************************************/
|
||||
public static QQueryFilter getFilterFromJsonOrObject(Object object) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
Object object = inputValues.get("queryFilterJson");
|
||||
if(object == null)
|
||||
{
|
||||
throw (new QException("Could not find filter in input values"));
|
||||
}
|
||||
|
||||
if(object instanceof QQueryFilter qQueryFilter)
|
||||
{
|
||||
return (qQueryFilter);
|
||||
@@ -114,7 +124,7 @@ public class RecordWorkflowUtils
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw new QException("Error getting filter from input values", e);
|
||||
throw new QException("Error getting filter from Object", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+60
-4
@@ -25,16 +25,21 @@ package com.kingsrook.qbits.workflows.implementations.recordworkflows;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||
import com.kingsrook.qbits.workflows.implementations.WorkflowStepUtils;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
||||
@@ -128,12 +133,24 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, newValue);
|
||||
updatedField = fieldAndJoinTable.field().clone();
|
||||
updatedField.setName("value");
|
||||
updatedField.setLabel("Value (" + fieldAndJoinTable.field().getLabel() + ")");
|
||||
}
|
||||
|
||||
if(fieldAndJoinTable != null)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// check the workflow registry to see if there is help content for this field //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
WorkflowsRegistry workflowsRegistry = WorkflowsRegistry.of(QContext.getQInstance());
|
||||
WorkflowStepType workflowStepType = workflowsRegistry.getWorkflowStepType(UpdateInputRecordFieldStep.NAME);
|
||||
if(workflowStepType != null)
|
||||
{
|
||||
for(QFieldMetaData fieldMetaData : CollectionUtils.nonNullList(workflowStepType.getInputFields()))
|
||||
{
|
||||
updatedField = fieldAndJoinTable.field().clone();
|
||||
updatedField.setName("value");
|
||||
updatedField.setLabel("Value (" + fieldAndJoinTable.field().getLabel() + ")");
|
||||
if(fieldMetaData.getName().equals("value"))
|
||||
{
|
||||
updatedField.setHelpContents(fieldMetaData.getHelpContents());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +160,8 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
|
||||
}
|
||||
}
|
||||
|
||||
adjustPossibleValueSourceFilterCriteriaInputs(updatedField.getPossibleValueSourceFilter());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// build the output object, starting with the new meta-data of the 'value' field //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -186,6 +205,43 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* if the field has a possible-value-source filter on it, and that filter
|
||||
* uses an input field e.g., ${input.someField} - consider that this value
|
||||
* *might* be an attribute on the workflow so add an optional
|
||||
* ??${input.workflow.someField} after it.
|
||||
***************************************************************************/
|
||||
static void adjustPossibleValueSourceFilterCriteriaInputs(QQueryFilter filter)
|
||||
{
|
||||
if(filter == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
|
||||
{
|
||||
List<Serializable> values = CollectionUtils.nonNullList(criteria.getValues());
|
||||
ListIterator<Serializable> valueIterator = values.listIterator();
|
||||
while(valueIterator.hasNext())
|
||||
{
|
||||
Serializable value = valueIterator.next();
|
||||
if(value instanceof String stringValue && stringValue.startsWith("${input."))
|
||||
{
|
||||
String inputFieldName = stringValue.replace("${input.", "").replace("}", "");
|
||||
String updatedValue = stringValue + "??${input.workflow." + inputFieldName + "}";
|
||||
valueIterator.set(updatedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
|
||||
{
|
||||
adjustPossibleValueSourceFilterCriteriaInputs(subFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
|
||||
+88
-29
@@ -60,7 +60,10 @@ import org.json.JSONObject;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** workflow step that updates a record with one value in one field
|
||||
* Workflow step that updates a record with one value in one field
|
||||
*
|
||||
* This class includes some public static methods that can be useful for other
|
||||
* workflow step implementations that have similar functionality.
|
||||
*******************************************************************************/
|
||||
public class UpdateInputRecordFieldStep extends WorkflowStepType implements WorkflowStepExecutorInterface, WorkflowStepValidatorInterface
|
||||
{
|
||||
@@ -101,7 +104,15 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
* For a given fieldName, tableName, and api and version (which come from a
|
||||
* workflowRevision), get QFieldMetaData for that field (if it's in that
|
||||
* API Version).
|
||||
*
|
||||
* @param fieldNameMaybeWithTableNamePrefix - field name - which can be
|
||||
* prefixed by tableName + "."
|
||||
* @param tableName table name
|
||||
* @param workflowRevision from which api name and version will come
|
||||
* @return optional containing QFieldMetaData if found, or empty if not.
|
||||
***************************************************************************/
|
||||
public static Optional<QFieldMetaData> getApiField(String fieldNameMaybeWithTableNamePrefix, String tableName, WorkflowRevision workflowRevision) throws QException
|
||||
{
|
||||
@@ -121,6 +132,31 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String getDynamicStepSummary(Integer workflowId, Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
QRecord workflowRecord = GetAction.execute(Workflow.TABLE_NAME, workflowId);
|
||||
String tableName = workflowRecord.getValueString("tableName");
|
||||
return getStepSummary(inputValues, tableName, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Helper for {@link #getDynamicStepSummary(Integer, Map)} or
|
||||
* {@link #execute(WorkflowStep, Map, WorkflowExecutionContext)} to generate
|
||||
* a string like "${fieldLabel} will be / was set to ${value}" or "... cleared out"
|
||||
* where the value will be a translated possible value if applicable.
|
||||
*
|
||||
* @param inputValues map of values user assigned to the step. expected to
|
||||
* include String fieldName and String value.
|
||||
* @param tableName table that the field should come from. Many times this
|
||||
* may be the tableName on the workflow - but it could be
|
||||
* a hard-coded tableName in other implementations of this
|
||||
* step type.
|
||||
* @param isPastTense to format the message as "will be" or "was". e.g.,
|
||||
* for getDynamicStepSummary would be false ("will be")
|
||||
* and for execute would be true ("was").
|
||||
***************************************************************************/
|
||||
public static String getStepSummary(Map<String, Serializable> inputValues, String tableName, boolean isPastTense)
|
||||
{
|
||||
String fieldName = ValueUtils.getValueAsString(inputValues.get("fieldName"));
|
||||
String value = ValueUtils.getValueAsString(inputValues.get("value"));
|
||||
@@ -137,45 +173,38 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
||||
}
|
||||
}
|
||||
|
||||
return getStepSummary(workflowId, fieldName, apiName, apiVersion, value, false);
|
||||
return getStepSummary(fieldName, tableName, apiName, apiVersion, value, isPastTense);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
*
|
||||
***************************************************************************/
|
||||
private String getStepSummary(Integer workflowId, String fieldName, String apiName, String apiVersion, String value, boolean isPastTense)
|
||||
private static String getStepSummary(String fieldName, String tableName, String apiName, String apiVersion, String value, boolean isPastTense)
|
||||
{
|
||||
String fieldLabel = null;
|
||||
QTableMetaData table = null;
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
try
|
||||
{
|
||||
if(workflowId != null)
|
||||
WorkflowRevision workflowRevision = new WorkflowRevision().withApiName(apiName).withApiVersion(apiVersion);
|
||||
if(WorkflowStepUtils.useApi(workflowRevision))
|
||||
{
|
||||
QRecord workflowRecord = GetAction.execute(Workflow.TABLE_NAME, workflowId);
|
||||
table = QContext.getQInstance().getTable(workflowRecord.getValueString("tableName"));
|
||||
|
||||
WorkflowRevision workflowRevision = new WorkflowRevision().withApiName(apiName).withApiVersion(apiVersion);
|
||||
if(WorkflowStepUtils.useApi(workflowRevision))
|
||||
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, workflowRevision);
|
||||
if(optionalApiField.isPresent())
|
||||
{
|
||||
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, table.getName(), workflowRevision);
|
||||
if(optionalApiField.isPresent())
|
||||
{
|
||||
// fieldName = optionalApiField.field().getName(); // hmm?
|
||||
fieldLabel = optionalApiField.get().getLabel();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
|
||||
fieldName = fieldAndJoinTable.field().getName();
|
||||
fieldLabel = fieldAndJoinTable.field().getLabel();
|
||||
fieldLabel = optionalApiField.get().getLabel();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
|
||||
fieldName = fieldAndJoinTable.field().getName();
|
||||
fieldLabel = fieldAndJoinTable.field().getLabel();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -183,15 +212,22 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
||||
}
|
||||
}
|
||||
|
||||
String verb = isPastTense ? "was" : "will be";
|
||||
|
||||
if(StringUtils.hasContent(fieldLabel))
|
||||
{
|
||||
String verb = isPastTense ? "was" : "will be";
|
||||
if(StringUtils.hasContent(value))
|
||||
{
|
||||
String displayValue = value;
|
||||
try
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// field names from this PVS are always table.field - but QValueFormatter doesn't expect that - so strip away table. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldName != null && fieldName.startsWith(tableName + "."))
|
||||
{
|
||||
fieldName = fieldName.replace(tableName + ".", "");
|
||||
}
|
||||
|
||||
QRecord record = new QRecord().withValue(fieldName, value);
|
||||
QValueFormatter.setDisplayValuesInRecordsIncludingPossibleValueTranslations(table, List.of(record));
|
||||
displayValue = record.getDisplayValue(fieldName);
|
||||
@@ -241,7 +277,8 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
||||
|
||||
context.doesRecordNeedUpdated.set(true);
|
||||
|
||||
String stepSummary = getStepSummary(context.getWorkflow().getId(), fieldName, context.getWorkflowRevision().getApiName(), context.getWorkflowRevision().getApiVersion(), value, true);
|
||||
String tableName = context.getWorkflow().getTableName();
|
||||
String stepSummary = getStepSummary(fieldName, tableName, context.getWorkflowRevision().getApiName(), context.getWorkflowRevision().getApiVersion(), value, true);
|
||||
return new WorkflowStepOutput(value, stepSummary);
|
||||
}
|
||||
|
||||
@@ -284,12 +321,34 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
||||
@Override
|
||||
public void validate(WorkflowStep step, Map<String, Serializable> inputValues, QRecord workflowRevision, QRecord workflow, List<String> errors) throws QException
|
||||
{
|
||||
if(WorkflowStepUtils.useApi(new WorkflowRevision(workflowRevision)))
|
||||
validate(workflow.getValueString("tableName"), inputValues, workflowRevision, errors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* implementation logic for {@link #validate(WorkflowStep, Map, QRecord, QRecord, List)}.
|
||||
* makes sure that, if the workflow uses API, that the field is a valid field
|
||||
* for the api version being used in the workflow revision.
|
||||
*
|
||||
* @param tableName table that the field should come from. Many times this
|
||||
* may be the tableName on the workflow - but it could be
|
||||
* a hard-coded tableName in other implementations of this
|
||||
* step type.
|
||||
* @param inputValues map of values user assigned to the step. expected to
|
||||
* include String fieldName.
|
||||
* @param workflowRevision from which api name and version will come
|
||||
* @param errors Out param - list of error message strings, which will be
|
||||
* added to if the input fieldName isn't in the table for the
|
||||
* api version on the workflowRevision
|
||||
***************************************************************************/
|
||||
public static void validate(String tableName, Map<String, Serializable> inputValues, QRecord workflowRevision, List<String> errors) throws QException
|
||||
{
|
||||
if(workflowRevision != null && WorkflowStepUtils.useApi(new WorkflowRevision(workflowRevision)))
|
||||
{
|
||||
String fieldName = ValueUtils.getValueAsString(inputValues.get("fieldName"));
|
||||
String apiName = ValueUtils.getValueAsString(workflowRevision.getValueString("apiName"));
|
||||
String apiVersion = ValueUtils.getValueAsString(workflowRevision.getValueString("apiVersion"));
|
||||
String tableName = ValueUtils.getValueAsString(workflow.getValueString("tableName"));
|
||||
|
||||
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, new WorkflowRevision(workflowRevision));
|
||||
if(optionalApiField.isEmpty())
|
||||
|
||||
@@ -84,7 +84,7 @@ public class WorkflowLink extends QRecordEntity
|
||||
@QField(isRequired = true)
|
||||
private Integer fromStepNo;
|
||||
|
||||
@QField(isRequired = true)
|
||||
@QField()
|
||||
private Integer toStepNo;
|
||||
|
||||
@QField(maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
||||
import com.kingsrook.qbits.workflows.definition.OutboundLinkOption;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* process to get values that the backend must compute about a workflow step,
|
||||
* based on input values, e.g., as a user is editing the step in a UI.
|
||||
*
|
||||
* Originally this was just the step summary (e.g., workflowStepType.getDynamicStepSummary)
|
||||
*
|
||||
* Then, when variable outboundLinkModes were added, also responsible for returning
|
||||
* the link options to use for a step, based on its input values.
|
||||
*
|
||||
*******************************************************************************/
|
||||
public class GetWorkflowStepDynamicValuesProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "getWorkflowStepDynamicValues";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
|
||||
.withStep(new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(getClass()))
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.withField(new QFieldMetaData("workflowId", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("workflowStepTypeName", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("values", QFieldType.INTEGER)) // JSON
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
Integer workflowId = runBackendStepInput.getValueInteger("workflowId");
|
||||
|
||||
String workflowStepTypeName = runBackendStepInput.getValueString("workflowStepTypeName");
|
||||
String values = runBackendStepInput.getValueString("values");
|
||||
Map<String, Serializable> valueAsMap = ValueUtils.getValueAsMap(values);
|
||||
|
||||
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(workflowStepTypeName);
|
||||
runBackendStepOutput.addValue("summary", workflowStepType.getDynamicStepSummary(workflowId, valueAsMap));
|
||||
|
||||
if(OutboundLinkMode.VARIABLE.equals(workflowStepType.getOutboundLinkMode()))
|
||||
{
|
||||
List<OutboundLinkOption> dynamicOutboundLinkOptions = workflowStepType.getDynamicOutboundLinkOptions(workflowId, valueAsMap);
|
||||
if(dynamicOutboundLinkOptions != null)
|
||||
{
|
||||
ArrayList<OutboundLinkOption> outboundLinkOptions = CollectionUtils.useOrWrap(dynamicOutboundLinkOptions, new TypeToken<>() {});
|
||||
runBackendStepOutput.addValue("outboundLinkOptions", outboundLinkOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
** process to get a dynamic summary of a workflow step, e.g., as it's being
|
||||
** edited in a UI.
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "0.115.0 - November 2025 - Replaced by GetWorkflowStepDynamicValuesProcess")
|
||||
public class GetWorkflowStepSummaryProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "getWorkflowStepSummary";
|
||||
@@ -58,7 +59,6 @@ public class GetWorkflowStepSummaryProcess implements BackendStep, MetaDataProdu
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
|
||||
+27
-1
@@ -41,12 +41,14 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
@@ -116,8 +118,12 @@ public class GetWorkflowTypeDefinitionProcess implements BackendStep, MetaDataPr
|
||||
throw new QException("Workflow type not found: " + workflowTypeName);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// save any help content on current workflow and put back after customization //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Map<String, List<QHelpContent>> helpContent = workflowType.getHelpContent();
|
||||
workflowType = workflowType.customizeBasedOnWorkflow(workflowRecord);
|
||||
|
||||
workflowType.setHelpContent(helpContent);
|
||||
runBackendStepOutput.addValue("workflowType", workflowType);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -167,4 +173,24 @@ public class GetWorkflowTypeDefinitionProcess implements BackendStep, MetaDataPr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected Map<String, List<HelpContent>> getWorkflowTypeHelpContent(Map<String, HelpContent> helpContentMap)
|
||||
{
|
||||
Map<String, List<HelpContent>> workflowTypeHelpContent = new LinkedHashMap<>();
|
||||
|
||||
String[] workflowTypeKeys = new String[] { "workflow:editor;field:workflowName", "workflow:editor;field:versionNo", "workflow:editor;field:apiName", "workflow:editor;field:apiVersion", "workflow:tester;field:workflowTestScenarioId" };
|
||||
for(String key : workflowTypeKeys)
|
||||
{
|
||||
if(helpContentMap.containsKey(key))
|
||||
{
|
||||
workflowTypeHelpContent.put(key, List.of(helpContentMap.get(key)));
|
||||
}
|
||||
}
|
||||
return (workflowTypeHelpContent);
|
||||
}
|
||||
|
||||
}
|
||||
+67
-10
@@ -29,8 +29,10 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowType;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepValidatorInterface;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeValidatorInterface;
|
||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||
@@ -140,6 +142,38 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
||||
throw (new QUserFacingException("Workflow not found by id: " + workflowId));
|
||||
}
|
||||
|
||||
String stepsJSON = runBackendStepInput.getValueString("steps");
|
||||
if(!StringUtils.hasContent(stepsJSON))
|
||||
{
|
||||
throw (new QUserFacingException("Workflow steps input was not provided."));
|
||||
}
|
||||
|
||||
List<WorkflowStep> workflowSteps;
|
||||
try
|
||||
{
|
||||
workflowSteps = JsonUtils.toObject(stepsJSON, new TypeReference<>() {});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QUserFacingException("Error parsing workflow steps json: " + e.getMessage(), e));
|
||||
}
|
||||
|
||||
String linksJSON = runBackendStepInput.getValueString("links");
|
||||
if(!StringUtils.hasContent(linksJSON))
|
||||
{
|
||||
throw (new QUserFacingException("Workflow links input was not provided."));
|
||||
}
|
||||
|
||||
List<WorkflowLink> workflowLinks;
|
||||
try
|
||||
{
|
||||
workflowLinks = JsonUtils.toObject(linksJSON, new TypeReference<>() {});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QUserFacingException("Error parsing workflow links json: " + e.getMessage(), e));
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// look up next versionNo //
|
||||
////////////////////////////
|
||||
@@ -166,15 +200,15 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
||||
// insert the workflow revision //
|
||||
//////////////////////////////////
|
||||
String commitMessage = runBackendStepInput.getValueString("commitMessage");
|
||||
String userName = ObjectUtils.tryElse(() -> QContext.getQSession().getUser().getFullName(), "Unknown");
|
||||
WorkflowRevision workflowRevision = new WorkflowRevision();
|
||||
workflowRevision.setWorkflowId(workflowId);
|
||||
workflowRevision.setVersionNo(versionNo);
|
||||
workflowRevision.setCommitMessage(StringUtils.hasContent(commitMessage) ? commitMessage : "New workflow revision created by " + QContext.getQSession().getUser().getFullName());
|
||||
workflowRevision.setCommitMessage(StringUtils.hasContent(commitMessage) ? commitMessage : "New workflow revision created by " + userName);
|
||||
workflowRevision.setApiName(runBackendStepInput.getValueString("apiName"));
|
||||
workflowRevision.setApiVersion(runBackendStepInput.getValueString("apiVersion"));
|
||||
workflowRevision.setAuthor(ObjectUtils.tryElse(() -> QContext.getQSession().getUser().getFullName(), "Unknown"));
|
||||
workflowRevision.setAuthor(userName);
|
||||
|
||||
List<WorkflowStep> workflowSteps = JsonUtils.toObject(runBackendStepInput.getValueString("steps"), new TypeReference<>() {});
|
||||
if(!workflowSteps.isEmpty())
|
||||
{
|
||||
workflowRevision.setStartStepNo(workflowSteps.get(0).getStepNo());
|
||||
@@ -208,19 +242,24 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// store steps (first setting revision id, validating, and putting a fresh summary on each) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<String> errorsForThisStep = new ArrayList<>();
|
||||
workflowSteps.forEach(step ->
|
||||
{
|
||||
try
|
||||
{
|
||||
step.setWorkflowRevisionId(insertedRevisionId);
|
||||
|
||||
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
||||
Map<String, Serializable> inputValues = ValueUtils.getValueAsMap(step.getInputValuesJson());
|
||||
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
||||
if(workflowStepType == null)
|
||||
{
|
||||
throw (new QUserFacingException("Unknown workflow step type " + step.getWorkflowStepTypeName()));
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// summary - if it fails, :shrug: //
|
||||
////////////////////////////////////
|
||||
Map<String, Serializable> inputValues = ValueUtils.getValueAsMap(step.getInputValuesJson());
|
||||
try
|
||||
{
|
||||
step.setSummary(workflowStepType.getDynamicStepSummary(workflowId, inputValues));
|
||||
@@ -237,18 +276,37 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
||||
if(validatorCodeReference != null)
|
||||
{
|
||||
WorkflowStepValidatorInterface validator = QCodeLoader.getAdHoc(WorkflowStepValidatorInterface.class, validatorCodeReference);
|
||||
validator.validate(step, inputValues, workflowRevisionRecord, workflowRecord, errors);
|
||||
validator.validate(step, inputValues, workflowRevisionRecord, workflowRecord, errorsForThisStep);
|
||||
if(!errorsForThisStep.isEmpty())
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// put Step # in front of all errors //
|
||||
///////////////////////////////////////
|
||||
errors.addAll(errorsForThisStep.stream().map(e -> "Step " + step.getStepNo() + ": " + e).toList());
|
||||
errorsForThisStep.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
errors.add("Error processing step [" + step.getSummary() + "]: " + e.getMessage());
|
||||
errors.add("Error processing Step " + step.getStepNo() + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// do workflow-level validation as well, if so configured //
|
||||
////////////////////////////////////////////////////////////
|
||||
WorkflowType workflowType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowType(workflowRecord.getValueString("workflowTypeName"));
|
||||
if(workflowType.getValidator() != null)
|
||||
{
|
||||
WorkflowTypeValidatorInterface validator = QCodeLoader.getAdHoc(WorkflowTypeValidatorInterface.class, workflowType.getValidator());
|
||||
validator.validate(workflowRecord, workflowRevisionRecord, workflowSteps, workflowLinks, errors);
|
||||
}
|
||||
|
||||
if(!errors.isEmpty())
|
||||
{
|
||||
throw (new QUserFacingException("Validation errors in steps: " + StringUtils.joinWithCommasAndAnd(errors)));
|
||||
String message = errors.size() + " validation error" + StringUtils.plural(errors) + " occurred before the workflow could be saved:\n" + StringUtils.join("\n", errors);
|
||||
throw (new QUserFacingException(message));
|
||||
}
|
||||
|
||||
new InsertAction().execute(new InsertInput(WorkflowStep.TABLE_NAME).withRecordEntities(workflowSteps).withTransaction(transaction));
|
||||
@@ -256,7 +314,6 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
||||
/////////////////
|
||||
// store links //
|
||||
/////////////////
|
||||
List<WorkflowLink> workflowLinks = JsonUtils.toObject(runBackendStepInput.getValueString("links"), new TypeReference<>() {});
|
||||
workflowLinks.forEach(link -> link.setWorkflowRevisionId(insertedRevisionId));
|
||||
new InsertAction().execute(new InsertInput(WorkflowLink.TABLE_NAME).withRecordEntities(workflowLinks).withTransaction(transaction));
|
||||
|
||||
|
||||
@@ -32,11 +32,13 @@ import com.kingsrook.qbits.workflows.definition.WorkflowType;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowExecutionContext;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowInput;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowMultiForkingStepExecutorInterface;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepExecutorInterface;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepOutput;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeExecutorInterface;
|
||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeTesterInterface;
|
||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
@@ -56,6 +58,8 @@ public class TestWorkflowDefinitions
|
||||
public static final String BOOLEAN_CONDITIONAL = "booleanConditional";
|
||||
public static final String CONTAINER = "container";
|
||||
|
||||
public static final String SPLIT_BY_LETTERS_IN_NAME = "splitByLettersInName";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
@@ -93,6 +97,12 @@ public class TestWorkflowDefinitions
|
||||
.withOutboundLinkMode(OutboundLinkMode.CONTAINER)
|
||||
.withLabel("Container")
|
||||
.withDescription("Group steps in the UI"));
|
||||
|
||||
WorkflowsRegistry.of(QContext.getQInstance()).registerWorkflowStepType(new WorkflowStepType()
|
||||
.withName(SPLIT_BY_LETTERS_IN_NAME)
|
||||
.withOutboundLinkMode(OutboundLinkMode.VARIABLE)
|
||||
.withLabel("Split by letters in name")
|
||||
.withExecutor(new QCodeReference(SplitByLettersInNameExecutor.class)));
|
||||
}
|
||||
|
||||
|
||||
@@ -225,4 +235,57 @@ public class TestWorkflowDefinitions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public static class SplitByLettersInNameExecutor implements WorkflowMultiForkingStepExecutorInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public WorkflowStepOutput conditionallyExecuteFork(WorkflowStep step, WorkflowLink fork, Map<String, Serializable> inputValues, WorkflowExecutionContext context)
|
||||
{
|
||||
String conditionValue = fork.getConditionValue();
|
||||
String name = ValueUtils.getValueAsString(context.getValues().get("name"));
|
||||
|
||||
if(name.contains(conditionValue))
|
||||
{
|
||||
return (new WorkflowStepOutput(true, "name [" + name + "] contains [" + conditionValue + "]"));
|
||||
}
|
||||
else
|
||||
{
|
||||
return (new WorkflowStepOutput(false, "name [" + name + "] does not contain [" + conditionValue + "]"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public WorkflowStepOutput makeWorkflowStepOutput(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||
{
|
||||
return new WorkflowStepOutput(1, "Output");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void sortForks(List<WorkflowLink> forkLinks, WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context)
|
||||
{
|
||||
/////////////////////
|
||||
// noop by default //
|
||||
/////////////////////
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qbits.workflows.implementations.recordworkflows.RecordWorkflowsDefinition;
|
||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||
@@ -149,7 +148,7 @@ public class WorkflowsTestDataSource
|
||||
public static Workflow insertWorkflowAndInitialRevision(String workflowTypeName, String tableName, String apiName, String apiVersion) throws QException
|
||||
{
|
||||
QRecord workflow = new InsertAction().execute(new InsertInput(Workflow.TABLE_NAME).withRecordEntity(new Workflow()
|
||||
.withWorkflowTypeName(RecordWorkflowsDefinition.WORKFLOW_TYPE)
|
||||
.withWorkflowTypeName(workflowTypeName)
|
||||
.withTableName(tableName)
|
||||
.withName("test-" + UUID.randomUUID())
|
||||
)).getRecords().get(0);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.definition;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for WorkflowsRegistry
|
||||
*******************************************************************************/
|
||||
class WorkflowsRegistryTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
String stepTypeName = "myStepType";
|
||||
WorkflowsRegistry workflowsRegistry = new WorkflowsRegistry();
|
||||
|
||||
workflowsRegistry.registerWorkflowStepType(new WorkflowStepType()
|
||||
.withName(stepTypeName)
|
||||
.withDescription("originalDescription")
|
||||
.withLabel("originalLabel")
|
||||
.withInputFields(List.of(new QFieldMetaData("myField", QFieldType.STRING))));
|
||||
|
||||
/////////////////////
|
||||
// set description //
|
||||
/////////////////////
|
||||
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), new QHelpContent("newDescription"), Map.of("workflowStepType", stepTypeName, "slot", "description"));
|
||||
assertEquals("newDescription", workflowsRegistry.getWorkflowStepType(stepTypeName).getDescription());
|
||||
|
||||
///////////////////////
|
||||
// clear description //
|
||||
///////////////////////
|
||||
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), null, Map.of("workflowStepType", stepTypeName, "slot", "description"));
|
||||
assertNull(workflowsRegistry.getWorkflowStepType(stepTypeName).getDescription());
|
||||
|
||||
///////////////
|
||||
// set label //
|
||||
///////////////
|
||||
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), new QHelpContent("newLabel"), Map.of("workflowStepType", stepTypeName, "slot", "label"));
|
||||
assertEquals("newLabel", workflowsRegistry.getWorkflowStepType(stepTypeName).getLabel());
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// clear label, though we can't, so it says as it was //
|
||||
////////////////////////////////////////////////////////
|
||||
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), null, Map.of("workflowStepType", stepTypeName, "slot", "label"));
|
||||
assertEquals("newLabel", workflowsRegistry.getWorkflowStepType(stepTypeName).getLabel());
|
||||
|
||||
////////////////////
|
||||
// set field help //
|
||||
////////////////////
|
||||
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), new QHelpContent("fieldHelp"), Map.of("workflowStepType", stepTypeName, "field", "myField"));
|
||||
assertEquals("fieldHelp", workflowsRegistry.getWorkflowStepType(stepTypeName).getInputFields().get(0).getHelpContents().get(0).getContent());
|
||||
|
||||
//////////////////////
|
||||
// clear field help //
|
||||
//////////////////////
|
||||
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), null, Map.of("workflowStepType", stepTypeName, "field", "myField"));
|
||||
assertNull(workflowsRegistry.getWorkflowStepType(stepTypeName).getInputFields().get(0).getHelpContents());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for FindFirstJoinPoint
|
||||
*******************************************************************************/
|
||||
class FindFirstJoinPointTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
* 1
|
||||
* / | \
|
||||
* / | \
|
||||
* 2 3 4
|
||||
* | | / \
|
||||
* | 5 6 7
|
||||
* | | | |
|
||||
* | | | 8
|
||||
* | | \ /
|
||||
* | | 9
|
||||
* \ | /
|
||||
* 10
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
Map<Integer, WorkflowStep> nodeMap = makeNodeMap(10);
|
||||
|
||||
ListingHash<Integer, WorkflowLink> outEdges = new ListingHash<>();
|
||||
addEdge(outEdges, 1, 2);
|
||||
addEdge(outEdges, 1, 3);
|
||||
addEdge(outEdges, 1, 4);
|
||||
addEdge(outEdges, 2, 10);
|
||||
addEdge(outEdges, 3, 5);
|
||||
addEdge(outEdges, 4, 6);
|
||||
addEdge(outEdges, 5, 10);
|
||||
addEdge(outEdges, 6, 9);
|
||||
addEdge(outEdges, 7, 8);
|
||||
addEdge(outEdges, 8, 9);
|
||||
addEdge(outEdges, 9, 10);
|
||||
|
||||
/////////////////////////////////
|
||||
// start from the top (node 1) //
|
||||
/////////////////////////////////
|
||||
assertEquals(10, FindFirstJoinPoint.execute(List.of(2, 3, 4), nodeMap, outEdges));
|
||||
|
||||
////////////////////////////////////////////
|
||||
// start from the rightmost fork (node 4) //
|
||||
////////////////////////////////////////////
|
||||
assertEquals(9, FindFirstJoinPoint.execute(List.of(6, 7), nodeMap, outEdges));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// remove node 10 - so they all just end and don't re-join (so expect null out) //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
nodeMap.remove(10);
|
||||
outEdges.remove(9);
|
||||
outEdges.remove(2);
|
||||
outEdges.remove(5);
|
||||
assertNull(FindFirstJoinPoint.execute(List.of(2, 3, 4), nodeMap, outEdges));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private void addEdge(ListingHash<Integer, WorkflowLink> outEdges, Integer fromStepNo, Integer toStepNo)
|
||||
{
|
||||
outEdges.add(fromStepNo, new WorkflowLink().withToStepNo(toStepNo));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private Map<Integer, WorkflowStep> makeNodeMap(int n)
|
||||
{
|
||||
Map<Integer, WorkflowStep> nodeMap = new HashMap<>();
|
||||
for(int i = 1; i <= n; i++)
|
||||
{
|
||||
addNode(nodeMap, i);
|
||||
}
|
||||
return (nodeMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private void addNode(Map<Integer, WorkflowStep> nodeMap, Integer stepNo)
|
||||
{
|
||||
nodeMap.put(stepNo, new WorkflowStep().withStepNo(stepNo));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,8 +29,16 @@ import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
||||
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -301,6 +309,183 @@ class WorkflowExecutorTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testMultiForkingStep() throws Exception
|
||||
{
|
||||
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||
|
||||
///////////////////////////////////
|
||||
// build a workflow with 3 forks //
|
||||
// . 1 //
|
||||
// ./ | \ //
|
||||
// 2 3 4 //
|
||||
///////////////////////////////////
|
||||
Workflow workflow = WorkflowsTestDataSource.insertWorkflowAndInitialRevision(TestWorkflowDefinitions.TEST_WORKFLOW_TYPE, null);
|
||||
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 1)), // "a"
|
||||
WorkflowsTestDataSource.newStep(3, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 2)), // "b"
|
||||
WorkflowsTestDataSource.newStep(4, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 4)) // "c"
|
||||
));
|
||||
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||
WorkflowsTestDataSource.newLink(1, 2, "a"), // += 1
|
||||
WorkflowsTestDataSource.newLink(1, 3, "b"), // += 2
|
||||
WorkflowsTestDataSource.newLink(1, 4, "c"), // += 3
|
||||
WorkflowsTestDataSource.newLink(2, null),
|
||||
WorkflowsTestDataSource.newLink(3, null),
|
||||
WorkflowsTestDataSource.newLink(4, null)
|
||||
));
|
||||
|
||||
UnsafeFunction<String, Integer, ?> run = (String name) ->
|
||||
{
|
||||
WorkflowOutput output = executeWorkflow(workflow.getId(), Map.of("seedValue", 0, "name", name));
|
||||
return ValueUtils.getValueAsInteger(output.getContext().getValues().get("sum"));
|
||||
};
|
||||
|
||||
assertEquals(1, run.apply("dave"));
|
||||
assertEquals(3, run.apply("barry"));
|
||||
assertEquals(5, run.apply("carl"));
|
||||
assertEquals(7, run.apply("braces"));
|
||||
assertEquals(0, run.apply("lou"));
|
||||
|
||||
//////////////////////////////////////
|
||||
// add a step (5) after the 3 forks //
|
||||
//////////////////////////////////////
|
||||
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||
WorkflowsTestDataSource.newStep(5, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 8))
|
||||
));
|
||||
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||
WorkflowsTestDataSource.newLink(2, 5),
|
||||
WorkflowsTestDataSource.newLink(3, 5),
|
||||
WorkflowsTestDataSource.newLink(4, 5)
|
||||
));
|
||||
|
||||
//////////////////////////////////
|
||||
// get rid of the links to null //
|
||||
//////////////////////////////////
|
||||
new DeleteAction().execute(new DeleteInput(WorkflowLink.TABLE_NAME).withQueryFilter(new QQueryFilter(new QFilterCriteria("toStepNo", QCriteriaOperator.IS_BLANK))));
|
||||
|
||||
assertEquals(10, run.apply("bobby"));
|
||||
assertEquals(15, run.apply("braces"));
|
||||
assertEquals(8, run.apply("lou"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNestedMultiForkingStep() throws Exception
|
||||
{
|
||||
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// build a workflow with 3 top-level forks, one of which has 2 nested ones //
|
||||
// . 1 //
|
||||
// ./ | \ //
|
||||
// 2 3 4 //
|
||||
// | | | \ //
|
||||
// | | 5 6 //
|
||||
// .\ \ / / //
|
||||
// . 7 //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
Workflow workflow = WorkflowsTestDataSource.insertWorkflowAndInitialRevision(TestWorkflowDefinitions.TEST_WORKFLOW_TYPE, null);
|
||||
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 1)), // "a"
|
||||
WorkflowsTestDataSource.newStep(3, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 2)), // "b"
|
||||
WorkflowsTestDataSource.newStep(4, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||
WorkflowsTestDataSource.newStep(5, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 4)), // "c"
|
||||
WorkflowsTestDataSource.newStep(6, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 8)), // "d"
|
||||
WorkflowsTestDataSource.newStep(7, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 16))
|
||||
));
|
||||
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||
WorkflowsTestDataSource.newLink(1, 2, "a"), // += 1
|
||||
WorkflowsTestDataSource.newLink(1, 3, "b"), // += 2
|
||||
WorkflowsTestDataSource.newLink(1, 4, ""), //
|
||||
WorkflowsTestDataSource.newLink(4, 5, "c"), // += 4
|
||||
WorkflowsTestDataSource.newLink(4, 6, "d"), // += 8
|
||||
WorkflowsTestDataSource.newLink(2, 7), // += 16
|
||||
WorkflowsTestDataSource.newLink(3, 7), // += 16
|
||||
WorkflowsTestDataSource.newLink(5, 7), // += 16
|
||||
WorkflowsTestDataSource.newLink(6, 7) // += 16
|
||||
));
|
||||
|
||||
UnsafeFunction<String, Integer, ?> run = (String name) ->
|
||||
{
|
||||
WorkflowOutput output = executeWorkflow(workflow.getId(), Map.of("seedValue", 0, "name", name));
|
||||
return ValueUtils.getValueAsInteger(output.getContext().getValues().get("sum"));
|
||||
};
|
||||
|
||||
assertEquals(17, run.apply("allen"));
|
||||
assertEquals(19, run.apply("barry"));
|
||||
assertEquals(21, run.apply("carl"));
|
||||
assertEquals(24, run.apply("donny"));
|
||||
assertEquals(16, run.apply("lou"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testStackedMultiForkingStep() throws Exception
|
||||
{
|
||||
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// build a workflow with two 2-level forks //
|
||||
// . 1 //
|
||||
// ./ \ //
|
||||
// 2 3 //
|
||||
// .\ / //
|
||||
// . 4 //
|
||||
// ./ \ //
|
||||
// 5 6 //
|
||||
/////////////////////////////////////////////
|
||||
Workflow workflow = WorkflowsTestDataSource.insertWorkflowAndInitialRevision(TestWorkflowDefinitions.TEST_WORKFLOW_TYPE, null);
|
||||
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 1)), // "a"
|
||||
WorkflowsTestDataSource.newStep(3, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 2)), // "b"
|
||||
WorkflowsTestDataSource.newStep(4, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||
WorkflowsTestDataSource.newStep(5, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 4)), // "c"
|
||||
WorkflowsTestDataSource.newStep(6, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 8)) // "d"
|
||||
));
|
||||
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||
WorkflowsTestDataSource.newLink(1, 2, "a"), // += 1
|
||||
WorkflowsTestDataSource.newLink(1, 3, "b"), // += 2
|
||||
WorkflowsTestDataSource.newLink(2, 4),
|
||||
WorkflowsTestDataSource.newLink(3, 4),
|
||||
WorkflowsTestDataSource.newLink(4, 5, "c"), // += 4
|
||||
WorkflowsTestDataSource.newLink(4, 6, "d"), // += 8
|
||||
WorkflowsTestDataSource.newLink(5, null),
|
||||
WorkflowsTestDataSource.newLink(6, null)
|
||||
));
|
||||
|
||||
UnsafeFunction<String, Integer, ?> run = (String name) ->
|
||||
{
|
||||
WorkflowOutput output = executeWorkflow(workflow.getId(), Map.of("seedValue", 0, "name", name));
|
||||
return ValueUtils.getValueAsInteger(output.getContext().getValues().get("sum"));
|
||||
};
|
||||
|
||||
assertEquals(1, run.apply("allen"));
|
||||
assertEquals(3, run.apply("barry"));
|
||||
assertEquals(5, run.apply("carl"));
|
||||
assertEquals(8, run.apply("donny"));
|
||||
assertEquals(4, run.apply("connie"));
|
||||
assertEquals(12, run.apply("doc"));
|
||||
assertEquals(15, run.apply("abcd"));
|
||||
assertEquals(0, run.apply("lou"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
||||
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for StepNodeGraphBuilder
|
||||
*******************************************************************************/
|
||||
class StepNodeGraphBuilderTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void beforeEach() throws QException
|
||||
{
|
||||
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSimpleFlat()
|
||||
{
|
||||
List<WorkflowStep> steps = new ArrayList<>();
|
||||
WorkflowStep step1 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
WorkflowStep step2 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
WorkflowStep step3 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
|
||||
List<WorkflowLink> links = new ArrayList<>();
|
||||
links.add(WorkflowsTestDataSource.newLink(step1.getStepNo(), step2.getStepNo()));
|
||||
links.add(WorkflowsTestDataSource.newLink(step2.getStepNo(), step3.getStepNo()));
|
||||
|
||||
NodeSequence nodeSequence = new StepNodeGraphBuilder().buildNodeSequence(step1.getStepNo(), steps, links);
|
||||
|
||||
assertSame(step1, nodeSequence.get(0).step());
|
||||
assertSame(step2, nodeSequence.get(1).step());
|
||||
assertSame(step3, nodeSequence.get(2).step());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* we'll evolve this graph through these 4 versions:
|
||||
*
|
||||
* . add1 > add1 > add1 . > (wrap it all in a container)
|
||||
* . | > | > |
|
||||
* .branch > branch > branch
|
||||
* ./ > / \ > / \
|
||||
* add2 > add2 add3 > add2 add3
|
||||
* . \ /
|
||||
* . add4
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSimpleBranch()
|
||||
{
|
||||
List<WorkflowStep> steps = new ArrayList<>();
|
||||
List<WorkflowLink> links = new ArrayList<>();
|
||||
|
||||
WorkflowStep add1 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
WorkflowStep branch = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.BOOLEAN_CONDITIONAL, null);
|
||||
WorkflowStep add2 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
|
||||
links.add(WorkflowsTestDataSource.newLink(add1.getStepNo(), branch.getStepNo()));
|
||||
links.add(WorkflowsTestDataSource.newLink(branch.getStepNo(), add2.getStepNo(), "true"));
|
||||
|
||||
{
|
||||
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(add1.getStepNo(), steps, links);
|
||||
assertEquals(2, nodeSequence.size());
|
||||
assertSame(add1, nodeSequence.get(0).step());
|
||||
assertSame(branch, nodeSequence.get(1).step());
|
||||
|
||||
StepNode branchNode = nodeSequence.get(1);
|
||||
assertEquals(1, branchNode.subSequences().size());
|
||||
assertEquals(1, branchNode.subSequences().get("true").size());
|
||||
assertSame(add2, branchNode.subSequences().get("true").get(0).step());
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// add a step in the false branch //
|
||||
////////////////////////////////////
|
||||
WorkflowStep add3 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
links.add(WorkflowsTestDataSource.newLink(branch.getStepNo(), add3.getStepNo(), "false"));
|
||||
|
||||
{
|
||||
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(add1.getStepNo(), steps, links);
|
||||
assertEquals(2, nodeSequence.size());
|
||||
assertSame(add1, nodeSequence.get(0).step());
|
||||
assertSame(branch, nodeSequence.get(1).step());
|
||||
|
||||
StepNode branchNode = nodeSequence.get(1);
|
||||
assertEquals(2, branchNode.subSequences().size());
|
||||
assertEquals(1, branchNode.subSequences().get("true").size());
|
||||
assertSame(add2, branchNode.subSequences().get("true").get(0).step());
|
||||
assertEquals(1, branchNode.subSequences().get("false").size());
|
||||
assertSame(add3, branchNode.subSequences().get("false").get(0).step());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// add a step after the branch now, and re-run //
|
||||
/////////////////////////////////////////////////
|
||||
WorkflowStep add4 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||
links.add(WorkflowsTestDataSource.newLink(add2.getStepNo(), add4.getStepNo()));
|
||||
links.add(WorkflowsTestDataSource.newLink(add3.getStepNo(), add4.getStepNo()));
|
||||
|
||||
{
|
||||
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(add1.getStepNo(), steps, links);
|
||||
assertEquals(3, nodeSequence.size());
|
||||
assertSame(add1, nodeSequence.get(0).step());
|
||||
assertSame(branch, nodeSequence.get(1).step());
|
||||
assertSame(add4, nodeSequence.get(2).step());
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// now put it all in a container //
|
||||
///////////////////////////////////
|
||||
WorkflowStep container = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.CONTAINER, null);
|
||||
links.add(WorkflowsTestDataSource.newLink(container.getStepNo(), add1.getStepNo(), "push"));
|
||||
{
|
||||
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(container.getStepNo(), steps, links);
|
||||
assertEquals(1, nodeSequence.size());
|
||||
assertSame(container, nodeSequence.get(0).step());
|
||||
|
||||
StepNode containerNode = nodeSequence.get(0);
|
||||
assertEquals(1, containerNode.subSequences().size());
|
||||
NodeSequence containedSequence = containerNode.subSequences().get("push");
|
||||
|
||||
assertEquals(3, containedSequence.size());
|
||||
assertSame(add1, containedSequence.get(0).step());
|
||||
assertSame(branch, containedSequence.get(1).step());
|
||||
assertSame(add4, containedSequence.get(2).step());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.implementations.recordworkflows;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterInput;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterOutput;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.RunFormAdjusterProcess;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for UpdateInputRecordFieldMetaDataAdjuster
|
||||
*******************************************************************************/
|
||||
class UpdateInputRecordFieldMetaDataAdjusterTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testExecute() throws QException
|
||||
{
|
||||
Map<String, Serializable> allValues = Map.of(
|
||||
"workflowValuesJSON", JsonUtils.toJson(Map.of("tableName", TABLE_NAME_PERSON)),
|
||||
"workflowRevisionValuesJSON", JsonUtils.toJson(Map.of()),
|
||||
"value", 1
|
||||
);
|
||||
|
||||
///////////////////////
|
||||
// run for on-change //
|
||||
///////////////////////
|
||||
FormAdjusterOutput onChangeOutput = new UpdateInputRecordFieldMetaDataAdjuster().execute(new FormAdjusterInput()
|
||||
.withFieldName("fieldName")
|
||||
.withNewValue(TABLE_NAME_PERSON + ".firstName")
|
||||
.withAllValues(allValues)
|
||||
.withEvent(RunFormAdjusterProcess.EVENT_ON_CHANGE));
|
||||
assertEquals(Set.of("value"), onChangeOutput.getFieldsToClear());
|
||||
|
||||
assertEquals(1, onChangeOutput.getUpdatedFieldMetaData().size());
|
||||
assertThat(onChangeOutput.getUpdatedFieldDisplayValues()).isNullOrEmpty();
|
||||
QFrontendFieldMetaData updatedValueField = onChangeOutput.getUpdatedFieldMetaData().get("value");
|
||||
assertEquals("Value (First Name)", updatedValueField.getLabel());
|
||||
|
||||
/////////////////////
|
||||
// run for on-load //
|
||||
/////////////////////
|
||||
new InsertAction().execute(new InsertInput(TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", 1).withValue("name", "Square")));
|
||||
|
||||
FormAdjusterOutput onLoadOutput = new UpdateInputRecordFieldMetaDataAdjuster().execute(new FormAdjusterInput()
|
||||
.withFieldName("fieldName")
|
||||
.withNewValue(TABLE_NAME_PERSON + ".favoriteShapeId")
|
||||
.withAllValues(allValues)
|
||||
.withEvent(RunFormAdjusterProcess.EVENT_ON_LOAD));
|
||||
assertThat(onLoadOutput.getFieldsToClear()).isNullOrEmpty();
|
||||
assertEquals(Map.of("value", "Square"), onLoadOutput.getUpdatedFieldDisplayValues());
|
||||
updatedValueField = onLoadOutput.getUpdatedFieldMetaData().get("value");
|
||||
assertEquals("Value (Favorite Shape)", updatedValueField.getLabel());
|
||||
assertEquals(TABLE_NAME_SHAPE, updatedValueField.getPossibleValueSourceName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAdjustPossibleValueSourceFilterCriteriaInputs()
|
||||
{
|
||||
QFieldMetaData field = new QFieldMetaData();
|
||||
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||
assertNull(field.getPossibleValueSourceFilter());
|
||||
|
||||
field.setPossibleValueSourceFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 1)));
|
||||
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||
assertEquals(1, field.getPossibleValueSourceFilter().getCriteria().get(0).getValues().get(0));
|
||||
|
||||
field.setPossibleValueSourceFilter(new QQueryFilter(new QFilterCriteria("someId", QCriteriaOperator.EQUALS, "${input.someId}")));
|
||||
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||
assertEquals("${input.someId}??${input.workflow.someId}", field.getPossibleValueSourceFilter().getCriteria().get(0).getValues().get(0));
|
||||
|
||||
field.setPossibleValueSourceFilter(new QQueryFilter().withSubFilter(new QQueryFilter(new QFilterCriteria("someId", QCriteriaOperator.EQUALS, "${input.someId}"))));
|
||||
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||
assertEquals("${input.someId}??${input.workflow.someId}", field.getPossibleValueSourceFilter().getSubFilters().get(0).getCriteria().get(0).getValues().get(0));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.implementations.recordworkflows;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for UpdateInputRecordFieldStep
|
||||
*******************************************************************************/
|
||||
class UpdateInputRecordFieldStepTest extends BaseTest
|
||||
{
|
||||
private static WorkflowRevision workflowRevisionV1 = new WorkflowRevision().withApiName(API_NAME).withApiVersion(V1);
|
||||
private static WorkflowRevision workflowRevisionV2 = new WorkflowRevision().withApiName(API_NAME).withApiVersion(V2);
|
||||
private static WorkflowRevision workflowRevisionV3 = new WorkflowRevision().withApiName(API_NAME).withApiVersion(V3);
|
||||
private static WorkflowRevision workflowRevisionWithoutApi = new WorkflowRevision();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testGetApiField() throws QException
|
||||
{
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".firstName", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isPresent().get().hasFieldOrPropertyWithValue("name", "firstName");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField("firstName", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isPresent().get().hasFieldOrPropertyWithValue("name", "firstName");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField("noSuchField", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isEmpty();
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".noSuchField", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isEmpty();
|
||||
|
||||
/////////////////////////////////////
|
||||
// this field is excluded from api //
|
||||
/////////////////////////////////////
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".debt", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isEmpty();
|
||||
|
||||
///////////////////////////////
|
||||
// this field is added in V3 //
|
||||
///////////////////////////////
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".salary", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isEmpty();
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".salary", TABLE_NAME_PERSON, workflowRevisionV3))
|
||||
.isPresent().get().hasFieldOrPropertyWithValue("name", "salary");
|
||||
|
||||
//////////////////////////////////////////
|
||||
// confirm shoeCount/noOfShoes renaming //
|
||||
//////////////////////////////////////////
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".shoeCount", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isPresent().get().hasFieldOrPropertyWithValue("name", "shoeCount");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".shoeCount", TABLE_NAME_PERSON, workflowRevisionV2))
|
||||
.isEmpty();
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".noOfShoes", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||
.isEmpty();
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".noOfShoes", TABLE_NAME_PERSON, workflowRevisionV2))
|
||||
.isPresent().get().hasFieldOrPropertyWithValue("name", "noOfShoes");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testGetStepSummary() throws QException
|
||||
{
|
||||
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName", "value", "John"), TABLE_NAME_PERSON, false))
|
||||
.isEqualTo("First Name will be set to 'John'");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName", "value", "John"), TABLE_NAME_PERSON, true))
|
||||
.isEqualTo("First Name was set to 'John'");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName", "value", ""), TABLE_NAME_PERSON, false))
|
||||
.isEqualTo("First Name will be cleared out");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName"), TABLE_NAME_PERSON, true))
|
||||
.isEqualTo("First Name was cleared out");
|
||||
|
||||
new InsertAction().execute(new InsertInput(TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", 1).withValue("name", "Square")));
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".favoriteShapeId", "value", "1"), TABLE_NAME_PERSON, false))
|
||||
.isEqualTo("Favorite Shape will be set to 'Square'");
|
||||
|
||||
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".favoriteShapeId", "value", "1"), TABLE_NAME_PERSON, true))
|
||||
.isEqualTo("Favorite Shape was set to 'Square'");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testValidate() throws QException
|
||||
{
|
||||
assertThat(runValidate(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName"), workflowRevisionV1)).isEmpty();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// debt field - found if not using api, else not found as it is excluded from api //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
assertThat(runValidate(Map.of("fieldName", TABLE_NAME_PERSON + ".debt"), workflowRevisionWithoutApi)).isEmpty();
|
||||
assertThat(runValidate(Map.of("fieldName", TABLE_NAME_PERSON + ".debt"), workflowRevisionV1)).hasSize(1)
|
||||
.element(0).asString()
|
||||
.contains("Could not find field 'person.debt' in table '" + TABLE_NAME_PERSON + "' for API '" + API_NAME + "' version 'v1'");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private List<String> runValidate(Map<String, Serializable> inputValues, WorkflowRevision workflowRevision) throws QException
|
||||
{
|
||||
List<String> errors = new ArrayList<>();
|
||||
UpdateInputRecordFieldStep.validate(TABLE_NAME_PERSON, inputValues, workflowRevision == null ? null : workflowRevision.toQRecord(), errors);
|
||||
return (errors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.workflows.processes;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.workflows.BaseTest;
|
||||
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
||||
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for StoreNewWorkflowRevisionProcess
|
||||
*******************************************************************************/
|
||||
class StoreNewWorkflowRevisionProcessTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||
Integer workflowId = WorkflowsTestDataSource.insertTestWorkflow();
|
||||
|
||||
RunBackendStepInput input = new RunBackendStepInput();
|
||||
|
||||
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("Workflow id is required");
|
||||
|
||||
input.addValue("workflowId", -999);
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("Workflow not found by id: -999");
|
||||
|
||||
input.addValue("workflowId", workflowId);
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("steps input was not provided");
|
||||
|
||||
input.addValue("steps", "1, 2, 3");
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("Error parsing workflow steps");
|
||||
|
||||
input.addValue("steps", JsonUtils.toJson(List.of(
|
||||
WorkflowsTestDataSource.newStep(1, "myStepType", Map.of("foo", 1)),
|
||||
WorkflowsTestDataSource.newStep(2, "yourStepType", Map.of("bar", true, "baz", false))
|
||||
)));
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("links input was not provided");
|
||||
|
||||
input.addValue("links", "not json");
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("Error parsing workflow links");
|
||||
|
||||
input.addValue("links", JsonUtils.toJson(List.of(WorkflowsTestDataSource.newLink(1, 2))));
|
||||
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||
.hasMessageContaining("2 validation errors occurred before the workflow could be saved:")
|
||||
.hasMessageContaining("Error processing Step 1: Unknown workflow step type myStepType")
|
||||
.hasMessageContaining("Error processing Step 2: Unknown workflow step type yourStepType");
|
||||
|
||||
input.addValue("steps", JsonUtils.toJson(List.of(
|
||||
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("foo", 1)),
|
||||
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.BOOLEAN_CONDITIONAL, Map.of("bar", true, "baz", false))
|
||||
)));
|
||||
new StoreNewWorkflowRevisionProcess().run(input, output);
|
||||
assertNotNull(output.getValue("workflowRevisionId"));
|
||||
assertEquals(2, output.getValue("versionNo"));
|
||||
|
||||
new StoreNewWorkflowRevisionProcess().run(input, output);
|
||||
assertEquals(3, output.getValue("versionNo"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user