CE-1180 Better built-in support for processes with dynamic flows - that is:

- in a backendStepOutput, you can set overrideLastStepName and call updateStepList
- those values then flow through RunProcessAction to the runProcessOutput, then out through the javalin to the frontend.
This commit is contained in:
2024-05-10 15:16:59 -05:00
parent f009d50631
commit 889697f86f
6 changed files with 522 additions and 10 deletions

View File

@ -190,7 +190,25 @@ public class RunProcessAction
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
/////////////////////////////////////////////////////////////////////////////////////////
// if the step returned an override lastStepName, use that to determine how we proceed //
/////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getOverrideLastStepName() != null)
{
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
lastStepName = runBackendStepOutput.getOverrideLastStepName();
}
/////////////////////////////////////////////////////////////////////////////////////////////
// similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
/////////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
{
LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
}
}
else
{
@ -339,7 +357,7 @@ public class RunProcessAction
/*******************************************************************************
** Run a single backend step.
*******************************************************************************/
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
private RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
@ -368,14 +386,16 @@ public class RunProcessAction
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
}
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, lastFunctionResult.getProcessState());
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, runBackendStepOutput.getProcessState());
if(lastFunctionResult.getException() != null)
if(runBackendStepOutput.getException() != null)
{
runProcessOutput.setException(lastFunctionResult.getException());
throw (lastFunctionResult.getException());
runProcessOutput.setException(runBackendStepOutput.getException());
throw (runBackendStepOutput.getException());
}
return (runBackendStepOutput);
}

View File

@ -27,10 +27,14 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -40,9 +44,14 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class RunBackendStepOutput extends AbstractActionOutput implements Serializable
{
private String processName;
private ProcessState processState;
private Exception exception; // todo - make optional
private String overrideLastStepName;
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
private List<AuditInput> auditInputList = new ArrayList<>();
@ -78,6 +87,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
public void seedFromRequest(RunBackendStepInput runBackendStepInput)
{
this.processState = runBackendStepInput.getProcessState();
this.processName = runBackendStepInput.getProcessName();
}
@ -312,4 +322,122 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
auditInput.addAuditSingleInput(auditSingleInput);
}
/*******************************************************************************
** Getter for overrideLastStepName
*******************************************************************************/
public String getOverrideLastStepName()
{
return (this.overrideLastStepName);
}
/*******************************************************************************
** Setter for overrideLastStepName
*******************************************************************************/
public void setOverrideLastStepName(String overrideLastStepName)
{
this.overrideLastStepName = overrideLastStepName;
}
/*******************************************************************************
** Fluent setter for overrideLastStepName
*******************************************************************************/
public RunBackendStepOutput withOverrideLastStepName(String overrideLastStepName)
{
this.overrideLastStepName = overrideLastStepName;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void updateStepList(List<String> stepList)
{
getProcessState().setStepList(stepList);
if(processName == null)
{
throw (new QRuntimeException("ProcessName was not set in this object, therefore updateStepList cannot complete successfully. Try to manually call setProcessName as a work around."));
}
QProcessMetaData processMetaData = QContext.getQInstance().getProcess(processName);
ArrayList<QFrontendStepMetaData> updatedFrontendStepList = new ArrayList<>(stepList.stream()
.map(name -> processMetaData.getStep(name))
.filter(step -> step instanceof QFrontendStepMetaData)
.map(step -> (QFrontendStepMetaData) step)
.toList());
setUpdatedFrontendStepList(updatedFrontendStepList);
}
/*******************************************************************************
** Getter for processName
*******************************************************************************/
public String getProcessName()
{
return (this.processName);
}
/*******************************************************************************
** Setter for processName
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
*******************************************************************************/
public RunBackendStepOutput withProcessName(String processName)
{
this.processName = processName;
return (this);
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public RunBackendStepOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
}

View File

@ -32,6 +32,7 @@ import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -45,6 +46,8 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
private String processUUID;
private Optional<Exception> exception = Optional.empty();
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
/*******************************************************************************
@ -327,4 +330,36 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
{
return exception;
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public RunProcessOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
}

View File

@ -0,0 +1,211 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.qqq.backend.core.actions.processes;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessTest.NoopBackendStep;
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.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.apache.commons.lang.BooleanUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
**
*******************************************************************************/
public class RunProcessUpdateStepListTest extends BaseTest
{
private static final String PROCESS_NAME = RunProcessUpdateStepListTest.class.getSimpleName();
private final static String STEP_START = "start";
private final static String STEP_A = "a";
private final static String STEP_B = "b";
private final static String STEP_C = "c";
private final static String STEP_1 = "1";
private final static String STEP_2 = "2";
private final static String STEP_3 = "3";
private final static String STEP_END = "end";
private final static List<String> LETTERS_STEP_LIST = List.of(
STEP_START,
STEP_A,
STEP_B,
STEP_C,
STEP_END
);
private final static List<String> NUMBERS_STEP_LIST = List.of(
STEP_START,
STEP_1,
STEP_2,
STEP_3,
STEP_END
);
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGoingLettersPath() throws QException
{
QContext.getQInstance().addProcess(defineProcess());
////////////////////////////////////////////////////////////
// start the process, telling it to go the "letters" path //
////////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(PROCESS_NAME);
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
runProcessInput.setValues(MapBuilder.of("which", "letters"));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
////////////////////////////////////////////////////////////////////////////////////////////////////
// assert that we got back the next-step name of A, and the updated list of frontend steps (A, C) //
////////////////////////////////////////////////////////////////////////////////////////////////////
Optional<String> nextStepName = runProcessOutput.getProcessState().getNextStepName();
assertTrue(nextStepName.isPresent());
assertEquals(STEP_A, nextStepName.get());
assertEquals(List.of(STEP_A, STEP_C, STEP_END), runProcessOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList());
/////////////////////////////////////////////////
// resume the process after that frontend step //
/////////////////////////////////////////////////
runProcessInput.setProcessUUID(runProcessOutput.getProcessUUID());
runProcessInput.setStartAfterStep(nextStepName.get());
runProcessOutput = new RunProcessAction().execute(runProcessInput);
///////////////////////////////////////////////////////////////////////////////////////
// assert we got back C as the next-step now, and no updated frontend list this time //
///////////////////////////////////////////////////////////////////////////////////////
nextStepName = runProcessOutput.getProcessState().getNextStepName();
assertTrue(nextStepName.isPresent());
assertEquals(STEP_C, nextStepName.get());
assertNull(runProcessOutput.getUpdatedFrontendStepList());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGoingNumbersPathAndSkippingAhead() throws QException
{
QContext.getQInstance().addProcess(defineProcess());
////////////////////////////////////////////////////////////////////////////////////
// start the process, telling it to go the "numbers" path, and to skip ahead some //
////////////////////////////////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(PROCESS_NAME);
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
runProcessInput.setValues(MapBuilder.of("which", "numbers", "skipSomeSteps", true));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
////////////////////////////////////////////////////////////////////////////////////////////////////
// assert that we got back the next-step name of 2, and the updated list of frontend steps (1, 3) //
////////////////////////////////////////////////////////////////////////////////////////////////////
Optional<String> nextStepName = runProcessOutput.getProcessState().getNextStepName();
assertTrue(nextStepName.isPresent());
assertEquals(STEP_END, nextStepName.get());
assertEquals(List.of(STEP_2, STEP_END), runProcessOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList());
}
/*******************************************************************************
**
*******************************************************************************/
public QProcessMetaData defineProcess()
{
QProcessMetaData process = new QProcessMetaData()
.withName(PROCESS_NAME)
.withStepList(List.of(
new QBackendStepMetaData()
.withName(STEP_START)
.withCode(new QCodeReference(StartStep.class)),
new QFrontendStepMetaData()
.withName(STEP_END)
));
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_A));
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_B).withCode(new QCodeReference(NoopBackendStep.class)));
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_C));
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_1).withCode(new QCodeReference(NoopBackendStep.class)));
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_2));
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_3).withCode(new QCodeReference(NoopBackendStep.class)));
return (process);
}
/*******************************************************************************
**
*******************************************************************************/
public static class StartStep implements BackendStep
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
boolean skipSomeSteps = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("skipSomeSteps"));
if(runBackendStepInput.getValueString("which").equals("letters"))
{
runBackendStepOutput.updateStepList(LETTERS_STEP_LIST);
if(skipSomeSteps)
{
runBackendStepOutput.setOverrideLastStepName(STEP_C);
}
}
else
{
runBackendStepOutput.updateStepList(NUMBERS_STEP_LIST);
if(skipSomeSteps)
{
runBackendStepOutput.setOverrideLastStepName(STEP_2);
}
}
}
}
}