diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java
new file mode 100644
index 00000000..1e70f88c
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java
@@ -0,0 +1,110 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.processes;
+
+
+import java.util.Optional;
+import java.util.UUID;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
+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.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.state.StateType;
+import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** Action handler for running the cancel step of a qqq process
+ *
+ *******************************************************************************/
+public class CancelProcessAction extends RunProcessAction
+{
+ private static final QLogger LOG = QLogger.getLogger(CancelProcessAction.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public RunProcessOutput execute(RunProcessInput runProcessInput) throws QException
+ {
+ ActionHelper.validateSession(runProcessInput);
+
+ QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
+ if(process == null)
+ {
+ throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
+ }
+
+ if(runProcessInput.getProcessUUID() == null)
+ {
+ throw (new QBadRequestException("Cannot cancel process - processUUID was not given."));
+ }
+
+ UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
+ Optional processState = getState(runProcessInput.getProcessUUID());
+ if(processState.isEmpty())
+ {
+ throw (new QBadRequestException("Cannot cancel process - State for process UUID [" + runProcessInput.getProcessUUID() + "] was not found."));
+ }
+
+ RunProcessOutput runProcessOutput = new RunProcessOutput();
+ try
+ {
+ if(process.getCancelStep() != null)
+ {
+ LOG.info("Running cancel step for process", logPair("processName", process.getName()));
+ runBackendStep(runProcessInput, process, runProcessOutput, stateKey, process.getCancelStep(), process, processState.get());
+ }
+ else
+ {
+ LOG.debug("Process does not have a custom cancel step to run.", logPair("processName", process.getName()));
+ }
+ }
+ catch(QException qe)
+ {
+ ////////////////////////////////////////////////////////////
+ // upon exception (e.g., one thrown by a step), throw it. //
+ ////////////////////////////////////////////////////////////
+ throw (qe);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error cancelling process", e));
+ }
+ finally
+ {
+ //////////////////////////////////////////////////////
+ // always put the final state in the process result //
+ //////////////////////////////////////////////////////
+ runProcessOutput.setProcessState(processState.get());
+ }
+
+ return (runProcessOutput);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
index ba9afdbd..8b0d7dd3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
@@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@@ -72,7 +73,17 @@ public class RunBackendStepAction
QStepMetaData stepMetaData = process.getStep(runBackendStepInput.getStepName());
if(stepMetaData == null)
{
- throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]");
+ if(process.getCancelStep() != null && Objects.equals(process.getCancelStep().getName(), runBackendStepInput.getStepName()))
+ {
+ /////////////////////////////////////
+ // special case for cancel step... //
+ /////////////////////////////////////
+ stepMetaData = process.getCancelStep();
+ }
+ else
+ {
+ throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]");
+ }
}
if(!(stepMetaData instanceof QBackendStepMetaData backendStepMetaData))
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java
index d2b82ee2..8624b926 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java
@@ -82,7 +82,7 @@ public class RunProcessAction
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
- public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
+ public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
////////////////////////////////////////////////////////////////////////////////////////////////
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
@@ -335,6 +335,13 @@ public class RunProcessAction
///////////////////////////////////////////////////
runProcessInput.seedFromProcessState(optionalProcessState.get());
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - //
+ // it is only needed on the transitional edge from a backend-step to a frontend step, but not //
+ // in the other directly //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ optionalProcessState.get().setUpdatedFrontendStepList(null);
+
///////////////////////////////////////////////////////////////////////////
// if there were values from the caller, put those (back) in the request //
///////////////////////////////////////////////////////////////////////////
@@ -357,7 +364,7 @@ public class RunProcessAction
/*******************************************************************************
** Run a single backend step.
*******************************************************************************/
- private RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
+ protected 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());
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index f371e86e..765e8e7c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -109,6 +109,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
import org.quartz.CronExpression;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@@ -151,6 +152,7 @@ public class QInstanceValidator
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
JoinGraph joinGraph = null;
+ long start = System.currentTimeMillis();
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
@@ -191,6 +193,9 @@ public class QInstanceValidator
validateUniqueTopLevelNames(qInstance);
runPlugins(QInstance.class, qInstance, qInstance);
+
+ long end = System.currentTimeMillis();
+ LOG.info("Validation (and enrichment) performance", logPair("millis", (end - start)));
}
catch(Exception e)
{
@@ -209,6 +214,17 @@ public class QInstanceValidator
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void revalidate(QInstance qInstance) throws QInstanceValidationException
+ {
+ qInstance.setHasBeenValidated(null);
+ validate(qInstance);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -657,17 +673,20 @@ public class QInstanceValidator
{
if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath."))
{
- joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
-
- boolean foundJoinConnection = false;
- for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
+ if(joinGraph != null)
{
- if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
+ joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
+
+ boolean foundJoinConnection = false;
+ for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
{
- foundJoinConnection = true;
+ if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
+ {
+ foundJoinConnection = true;
+ }
}
+ assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
}
- assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
assertCondition(!usedJoinPaths.contains(exposedJoin.getJoinPath()), tablePrefix + "has more than one join with the joinPath: " + exposedJoin.getJoinPath());
usedJoinPaths.add(exposedJoin.getJoinPath());
@@ -1468,7 +1487,7 @@ public class QInstanceValidator
warn("Error loading expectedType for field [" + fieldMetaData.getName() + "] in process [" + processName + "]: " + e.getMessage());
}
- validateSimpleCodeReference("Process " + processName + " code reference: ", codeReference, expectedClass);
+ validateSimpleCodeReference("Process " + processName + " code reference:", codeReference, expectedClass);
}
}
}
@@ -1476,6 +1495,14 @@ public class QInstanceValidator
}
}
+ if(process.getCancelStep() != null)
+ {
+ if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
+ {
+ validateSimpleCodeReference("Process " + processName + " cancel step code reference: ", process.getCancelStep().getCode(), BackendStep.class);
+ }
+ }
+
///////////////////////////////////////////////////////////////////////////////
// if the process has a schedule, make sure required schedule data populated //
///////////////////////////////////////////////////////////////////////////////
@@ -1487,7 +1514,11 @@ public class QInstanceValidator
if(process.getVariantBackend() != null)
{
- assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
+ if(qInstance.getBackends() != null)
+ {
+ assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
+ }
+
assertCondition(process.getVariantRunStrategy() != null, "A variant run strategy was not set for process " + processName + " (which does specify a variant backend)");
}
else
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java
index ccc95108..c418bd07 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java
@@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
/*******************************************************************************
@@ -41,6 +42,11 @@ public class ProcessState implements Serializable
private List stepList = new ArrayList<>();
private Optional nextStepName = Optional.empty();
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private List updatedFrontendStepList = null;
+
/*******************************************************************************
@@ -139,4 +145,36 @@ public class ProcessState implements Serializable
{
this.stepList = stepList;
}
+
+
+
+ /*******************************************************************************
+ ** Getter for updatedFrontendStepList
+ *******************************************************************************/
+ public List getUpdatedFrontendStepList()
+ {
+ return (this.updatedFrontendStepList);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for updatedFrontendStepList
+ *******************************************************************************/
+ public void setUpdatedFrontendStepList(List updatedFrontendStepList)
+ {
+ this.updatedFrontendStepList = updatedFrontendStepList;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for updatedFrontendStepList
+ *******************************************************************************/
+ public ProcessState withUpdatedFrontendStepList(List updatedFrontendStepList)
+ {
+ this.updatedFrontendStepList = updatedFrontendStepList;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
index 4cb6aa3b..4754fcaa 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
@@ -49,8 +49,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
private ProcessState processState;
private Exception exception; // todo - make optional
- private String overrideLastStepName;
- private List updatedFrontendStepList = null;
+ private String overrideLastStepName; // todo - does this need to go into state too??
private List auditInputList = new ArrayList<>();
@@ -416,7 +415,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
*******************************************************************************/
public List getUpdatedFrontendStepList()
{
- return (this.updatedFrontendStepList);
+ return (this.processState.getUpdatedFrontendStepList());
}
@@ -426,18 +425,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
*******************************************************************************/
public void setUpdatedFrontendStepList(List updatedFrontendStepList)
{
- this.updatedFrontendStepList = updatedFrontendStepList;
- }
-
-
-
- /*******************************************************************************
- ** Fluent setter for updatedFrontendStepList
- *******************************************************************************/
- public RunBackendStepOutput withUpdatedFrontendStepList(List updatedFrontendStepList)
- {
- this.updatedFrontendStepList = updatedFrontendStepList;
- return (this);
+ this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java
index 7e05a660..30a5642a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java
@@ -46,8 +46,6 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
private String processUUID;
private Optional exception = Optional.empty();
- private List updatedFrontendStepList = null;
-
/*******************************************************************************
@@ -334,32 +332,21 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
/*******************************************************************************
- ** Getter for updatedFrontendStepList
- *******************************************************************************/
- public List getUpdatedFrontendStepList()
- {
- return (this.updatedFrontendStepList);
- }
-
-
-
- /*******************************************************************************
- ** Setter for updatedFrontendStepList
+ **
*******************************************************************************/
public void setUpdatedFrontendStepList(List updatedFrontendStepList)
{
- this.updatedFrontendStepList = updatedFrontendStepList;
+ this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
/*******************************************************************************
- ** Fluent setter for updatedFrontendStepList
+ **
*******************************************************************************/
- public RunProcessOutput withUpdatedFrontendStepList(List updatedFrontendStepList)
+ public List getUpdatedFrontendStepList()
{
- this.updatedFrontendStepList = updatedFrontendStepList;
- return (this);
+ return this.processState.getUpdatedFrontendStepList();
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java
index cd899e9b..9989f670 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java
@@ -69,7 +69,7 @@ public enum WidgetType
DYNAMIC_FORM("dynamicForm"),
DATA_BAG_VIEWER("dataBagViewer"),
PIVOT_TABLE_SETUP("pivotTableSetup"),
- REPORT_SETUP("reportSetup"),
+ FILTER_AND_COLUMNS_SETUP("filterAndColumnsSetup"),
SCRIPT_VIEWER("scriptViewer");
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QIgnore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QIgnore.java
new file mode 100644
index 00000000..9c702049
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QIgnore.java
@@ -0,0 +1,39 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.data;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/*******************************************************************************
+ ** Marker - that a piece of code should be ignored (e.g., a field not treated as
+ ** a @QField)
+ *******************************************************************************/
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QIgnore
+{
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
index 7c42e98f..dffc5947 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
@@ -35,12 +35,15 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang3.SerializationUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -462,6 +465,7 @@ public class QRecord implements Serializable
}
+
/*******************************************************************************
** Getter for a single field's value
**
@@ -616,6 +620,22 @@ public class QRecord implements Serializable
+ /*******************************************************************************
+ ** Getter for errors
+ **
+ *******************************************************************************/
+ @JsonIgnore
+ public String getErrorsAsString()
+ {
+ if(CollectionUtils.nullSafeHasContents(errors))
+ {
+ return StringUtils.join("; ", errors.stream().map(e -> e.getMessage()).toList());
+ }
+ return ("");
+ }
+
+
+
/*******************************************************************************
** Setter for errors
**
@@ -732,6 +752,22 @@ public class QRecord implements Serializable
+ /*******************************************************************************
+ ** Getter for warnings
+ **
+ *******************************************************************************/
+ @JsonIgnore
+ public String getWarningsAsString()
+ {
+ if(CollectionUtils.nullSafeHasContents(warnings))
+ {
+ return StringUtils.join("; ", warnings.stream().map(e -> e.getMessage()).toList());
+ }
+ return ("");
+ }
+
+
+
/*******************************************************************************
** Setter for warnings
*******************************************************************************/
@@ -742,6 +778,18 @@ public class QRecord implements Serializable
+ /*******************************************************************************
+ ** Fluently Add one warning to this record
+ **
+ *******************************************************************************/
+ public QRecord withWarning(QWarningMessage warning)
+ {
+ addWarning(warning);
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Fluent setter for warnings
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java
index 622ad1b5..89de8bea 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java
@@ -218,6 +218,7 @@ public abstract class QRecordEntity
}
+
/*******************************************************************************
**
*******************************************************************************/
@@ -296,7 +297,19 @@ public abstract class QRecordEntity
}
else
{
- LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
+ Optional ignoreAnnotation = getQIgnoreAnnotation(c, fieldName);
+ Optional associationAnnotation = getQAssociationAnnotation(c, fieldName);
+
+ if(ignoreAnnotation.isPresent() || associationAnnotation.isPresent())
+ {
+ ////////////////////////////////////////////////////////////
+ // silently skip if marked as an association or an ignore //
+ ////////////////////////////////////////////////////////////
+ }
+ else
+ {
+ LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
+ }
}
}
else
@@ -360,6 +373,16 @@ public abstract class QRecordEntity
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Optional getQIgnoreAnnotation(Class extends QRecordEntity> c, String ignoreName)
+ {
+ return (getAnnotationOnField(c, QIgnore.class, ignoreName));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -419,9 +442,9 @@ public abstract class QRecordEntity
}
else
{
- if(!method.getName().equals("getClass"))
+ if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null)
{
- LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
+ LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
}
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java
index f5530b3b..f2350825 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java
@@ -145,7 +145,7 @@ public interface QRecordEnum
{
if(!method.getName().equals("getClass") && !method.getName().equals("getDeclaringClass") && !method.getName().equals("getPossibleValueId"))
{
- LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
+ LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
}
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
index 44c9259b..fc7c7687 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
@@ -60,6 +60,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private List stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map steps; // this is the full map of possible steps
+ private QBackendStepMetaData cancelStep;
+
private QIcon icon;
private QScheduleMetaData schedule;
@@ -675,6 +677,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
}
+
/*******************************************************************************
** Getter for variantRunStrategy
*******************************************************************************/
@@ -746,4 +749,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return steps;
}
+
+
+ /*******************************************************************************
+ ** Getter for cancelStep
+ *******************************************************************************/
+ public QBackendStepMetaData getCancelStep()
+ {
+ return (this.cancelStep);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for cancelStep
+ *******************************************************************************/
+ public void setCancelStep(QBackendStepMetaData cancelStep)
+ {
+ this.cancelStep = cancelStep;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for cancelStep
+ *******************************************************************************/
+ public QProcessMetaData withCancelStep(QBackendStepMetaData cancelStep)
+ {
+ this.cancelStep = cancelStep;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
index 99e52602..9819f5b3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
@@ -640,6 +640,18 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
+ /*******************************************************************************
+ ** fluent setter for both recordLabelFormat and recordLabelFields
+ *******************************************************************************/
+ public QTableMetaData withRecordLabelFormatAndFields(String format, String... fields)
+ {
+ setRecordLabelFormat(format);
+ setRecordLabelFields(Arrays.asList(fields));
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for recordLabelFields
**
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/querystats/QueryStat.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/querystats/QueryStat.java
index a0e5f513..ef14ac72 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/querystats/QueryStat.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/querystats/QueryStat.java
@@ -28,6 +28,7 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
@@ -77,7 +78,9 @@ public class QueryStat extends QRecordEntity
///////////////////////////////////////////////////////////
// non-persistent fields - used to help build the record //
///////////////////////////////////////////////////////////
- private String tableName;
+ @QIgnore
+ private String tableName;
+
private Set joinTableNames;
private QQueryFilter queryFilter;
@@ -384,6 +387,7 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for queryFilter
*******************************************************************************/
+ @QIgnore
public QQueryFilter getQueryFilter()
{
return (this.queryFilter);
@@ -446,6 +450,7 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for joinTableNames
*******************************************************************************/
+ @QIgnore
public Set getJoinTableNames()
{
return (this.joinTableNames);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
index 799adf0d..5b4132c4 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
@@ -73,6 +73,7 @@ public class SavedReportsMetaDataProvider
public static final String RENDER_REPORT_PROCESS_VALUES_WIDGET = "renderReportProcessValuesWidget";
+
/*******************************************************************************
**
*******************************************************************************/
@@ -234,7 +235,7 @@ public class SavedReportsMetaDataProvider
.withName("reportSetupWidget")
.withLabel("Filters and Columns")
.withIsCard(true)
- .withType(WidgetType.REPORT_SETUP.getType())
+ .withType(WidgetType.FILTER_AND_COLUMNS_SETUP.getType())
.withCodeReference(new QCodeReference(DefaultWidgetRenderer.class));
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java
index 726d160b..117f07ac 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java
@@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
@@ -434,6 +435,7 @@ public class ScheduledJob extends QRecordEntity
/*******************************************************************************
** Getter for jobParameters - but a map of just the key=value pairs.
*******************************************************************************/
+ @QIgnore
public Map getJobParametersMap()
{
if(CollectionUtils.nullSafeIsEmpty(this.jobParameters))
@@ -469,6 +471,7 @@ public class ScheduledJob extends QRecordEntity
}
+
/*******************************************************************************
** Getter for repeatSeconds
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
index 02786ce8..37354746 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
@@ -144,6 +144,12 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
BackendStepPostRunOutput postRunOutput = new BackendStepPostRunOutput(runBackendStepOutput);
BackendStepPostRunInput postRunInput = new BackendStepPostRunInput(runBackendStepInput);
transformStep.postRun(postRunInput, postRunOutput);
+
+ // todo figure out what kind of test we can get on this
+ if(postRunOutput.getUpdatedFrontendStepList() != null)
+ {
+ runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
index 34139d9c..5d1a6e2d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
@@ -141,6 +141,11 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
BackendStepPostRunOutput postRunOutput = new BackendStepPostRunOutput(runBackendStepOutput);
BackendStepPostRunInput postRunInput = new BackendStepPostRunInput(runBackendStepInput);
transformStep.postRun(postRunInput, postRunOutput);
+
+ if(postRunOutput.getUpdatedFrontendStepList() != null)
+ {
+ runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java
index 157b0dd7..752652bf 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java
@@ -51,8 +51,14 @@ public class ProcessLock extends QRecordEntity
@QField(possibleValueSourceName = ProcessLockType.TABLE_NAME)
private Integer processLockTypeId;
- @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
- private String holder;
+ @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
+ private String userId;
+
+ @QField(label = "Session UUID", maxLength = 36, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
+ private String sessionUUID;
+
+ @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
+ private String details;
@QField()
private Instant checkInTimestamp;
@@ -205,37 +211,6 @@ public class ProcessLock extends QRecordEntity
- /*******************************************************************************
- ** Getter for holder
- *******************************************************************************/
- public String getHolder()
- {
- return (this.holder);
- }
-
-
-
- /*******************************************************************************
- ** Setter for holder
- *******************************************************************************/
- public void setHolder(String holder)
- {
- this.holder = holder;
- }
-
-
-
- /*******************************************************************************
- ** Fluent setter for holder
- *******************************************************************************/
- public ProcessLock withHolder(String holder)
- {
- this.holder = holder;
- return (this);
- }
-
-
-
/*******************************************************************************
** Getter for checkInTimestamp
*******************************************************************************/
@@ -327,4 +302,97 @@ public class ProcessLock extends QRecordEntity
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for userId
+ *******************************************************************************/
+ public String getUserId()
+ {
+ return (this.userId);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userId
+ *******************************************************************************/
+ public void setUserId(String userId)
+ {
+ this.userId = userId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userId
+ *******************************************************************************/
+ public ProcessLock withUserId(String userId)
+ {
+ this.userId = userId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sessionUUID
+ *******************************************************************************/
+ public String getSessionUUID()
+ {
+ return (this.sessionUUID);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sessionUUID
+ *******************************************************************************/
+ public void setSessionUUID(String sessionUUID)
+ {
+ this.sessionUUID = sessionUUID;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sessionUUID
+ *******************************************************************************/
+ public ProcessLock withSessionUUID(String sessionUUID)
+ {
+ this.sessionUUID = sessionUUID;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for details
+ *******************************************************************************/
+ public String getDetails()
+ {
+ return (this.details);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for details
+ *******************************************************************************/
+ public void setDetails(String details)
+ {
+ this.details = details;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for details
+ *******************************************************************************/
+ public ProcessLock withDetails(String details)
+ {
+ this.details = details;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java
index 3f10f86f..b7382f24 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java
@@ -63,7 +63,7 @@ public class ProcessLockMetaDataProducer implements MetaDataProducerInterface qSession.getUuid(), null))
+ .withUserId(ObjectUtils.tryAndRequireNonNullElse(() -> qSession.getUser().getIdReference(), null))
+ .withDetails(details)
.withCheckInTimestamp(now);
Integer defaultExpirationSeconds = lockType.getDefaultExpirationSeconds();
@@ -106,12 +108,22 @@ public class ProcessLockUtils
QRecord existingLockRecord = new GetAction().executeForRecord(new GetInput(ProcessLock.TABLE_NAME).withUniqueKey(Map.of("key", key, "processLockTypeId", lockType.getId())));
if(existingLockRecord != null)
{
- existingLockDetails.append("Held by: ").append(existingLockRecord.getValueString("holder"));
- Instant expiresAtTimestamp = existingLockRecord.getValueInstant("expiresAtTimestamp");
+ ProcessLock existingLock = new ProcessLock(existingLockRecord);
+ if(StringUtils.hasContent(existingLock.getUserId()))
+ {
+ existingLockDetails.append("Held by: ").append(existingLock.getUserId());
+ }
+
+ if(StringUtils.hasContent(existingLock.getDetails()))
+ {
+ existingLockDetails.append("; with details: ").append(existingLock.getDetails());
+ }
+
+ Instant expiresAtTimestamp = existingLock.getExpiresAtTimestamp();
if(expiresAtTimestamp != null)
{
ZonedDateTime zonedExpiresAt = expiresAtTimestamp.atZone(ValueUtils.getSessionOrInstanceZoneId());
- existingLockDetails.append("; Expires at: ").append(QValueFormatter.formatDateTimeWithZone(zonedExpiresAt));
+ existingLockDetails.append("; expiring at: ").append(QValueFormatter.formatDateTimeWithZone(zonedExpiresAt));
}
if(expiresAtTimestamp != null && expiresAtTimestamp.isBefore(now))
@@ -119,10 +131,9 @@ public class ProcessLockUtils
/////////////////////////////////////////////////////////////////////////////////
// if existing lock has expired, then we can delete it and try to insert again //
/////////////////////////////////////////////////////////////////////////////////
- Serializable id = existingLockRecord.getValue("id");
- LOG.info("Existing lock has expired - deleting it and trying again.", logPair("id", id),
- logPair("key", key), logPair("type", typeName), logPair("holder", holder), logPair("expiresAtTimestamp", expiresAtTimestamp));
- new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(id));
+ LOG.info("Existing lock has expired - deleting it and trying again.", logPair("id", existingLock.getId()),
+ logPair("key", key), logPair("type", typeName), logPair("details", details), logPair("expiresAtTimestamp", expiresAtTimestamp));
+ new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(existingLock.getId()));
insertOutputRecord = tryToInsert(processLock);
}
}
@@ -141,12 +152,12 @@ public class ProcessLockUtils
// if at this point, we have errors on the last attempted insert, then give up //
/////////////////////////////////////////////////////////////////////////////////
LOG.info("Errors in process lock record after attempted insert", logPair("errors", insertOutputRecord.getErrors()),
- logPair("key", key), logPair("type", typeName), logPair("holder", holder));
+ logPair("key", key), logPair("type", typeName), logPair("details", details));
throw (new UnableToObtainProcessLockException("A Process Lock already exists for key [" + key + "] of type [" + typeName + "], " + existingLockDetails));
}
LOG.info("Created process lock", logPair("id", processLock.getId()),
- logPair("key", key), logPair("type", typeName), logPair("holder", holder), logPair("expiresAtTimestamp", processLock.getExpiresAtTimestamp()));
+ logPair("key", key), logPair("type", typeName), logPair("details", details), logPair("expiresAtTimestamp", processLock.getExpiresAtTimestamp()));
return new ProcessLock(insertOutputRecord);
}
@@ -169,6 +180,7 @@ public class ProcessLockUtils
{
Instant giveUpTime = Instant.now().plus(maxWait);
+ UnableToObtainProcessLockException lastCaughtUnableToObtainProcessLockException = null;
while(true)
{
try
@@ -178,6 +190,7 @@ public class ProcessLockUtils
}
catch(UnableToObtainProcessLockException e)
{
+ lastCaughtUnableToObtainProcessLockException = e;
if(Instant.now().plus(sleepBetweenTries).isBefore(giveUpTime))
{
SleepUtils.sleep(sleepBetweenTries);
@@ -189,7 +202,12 @@ public class ProcessLockUtils
}
}
- throw (new UnableToObtainProcessLockException("Unable to obtain process lock for key [" + key + "] in type [" + type + "] after [" + maxWait + "]"));
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // var can never be null with current code-path, but prefer defensiveness regardless. //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ @SuppressWarnings("ConstantValue")
+ String suffix = lastCaughtUnableToObtainProcessLockException == null ? "" : ": " + lastCaughtUnableToObtainProcessLockException.getMessage();
+ throw (new UnableToObtainProcessLockException("Unable to obtain process lock for key [" + key + "] in type [" + type + "] after [" + maxWait + "]" + suffix));
}
@@ -199,12 +217,162 @@ public class ProcessLockUtils
*******************************************************************************/
public static ProcessLock getById(Integer id) throws QException
{
+ if(id == null)
+ {
+ return (null);
+ }
+
QRecord existingLockRecord = new GetAction().executeForRecord(new GetInput(ProcessLock.TABLE_NAME).withPrimaryKey(id));
if(existingLockRecord != null)
{
return (new ProcessLock(existingLockRecord));
}
- return null;
+ return (null);
+ }
+
+
+
+ /*******************************************************************************
+ ** input wrapper for an overload of the checkin method, to allow more flexibility
+ ** w/ whether or not you want to update details & expiresAtTimestamp (e.g., so a
+ ** null can be passed in, to mean "set it to null" vs. "don't update it").
+ *******************************************************************************/
+ public static class CheckInInput
+ {
+ private ProcessLock processLock;
+ private Instant expiresAtTimestamp = null;
+ private boolean wasGivenExpiresAtTimestamp = false;
+ private String details = null;
+ private boolean wasGivenDetails = false;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public CheckInInput(ProcessLock processLock)
+ {
+ this.processLock = processLock;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public CheckInInput withExpiresAtTimestamp(Instant expiresAtTimestamp)
+ {
+ this.expiresAtTimestamp = expiresAtTimestamp;
+ this.wasGivenExpiresAtTimestamp = true;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public CheckInInput withDetails(String details)
+ {
+ this.details = details;
+ this.wasGivenDetails = true;
+ return (this);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Do a check-in, with a specific value for the expiresAtTimestamp - which can
+ ** be set to null to make it null in the lock.
+ **
+ ** If you don't want to specify the expiresAtTimestamp, call the overload that
+ ** doesn't take the timestamp - in which case it'll either stay the same as it
+ ** was, or will be set based on the type's default.
+ *******************************************************************************/
+ public static void checkIn(CheckInInput input)
+ {
+ ProcessLock processLock = input.processLock;
+
+ try
+ {
+ if(processLock == null)
+ {
+ LOG.debug("Null processLock passed in - will not checkin.");
+ return;
+ }
+
+ QRecord recordToUpdate = new QRecord()
+ .withValue("id", processLock.getId())
+ .withValue("checkInTimestamp", Instant.now());
+
+ ///////////////////////////////////////////////////////////////////
+ // if the input was given a details string, update the details //
+ // use boolean instead of null to know whether or not to do this //
+ ///////////////////////////////////////////////////////////////////
+ if(input.wasGivenDetails)
+ {
+ recordToUpdate.setValue("details", input.details);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the input object had an expires-at timestamp put in it, then use that value (null or otherwise) for the expires-at-timestamp //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(input.wasGivenExpiresAtTimestamp)
+ {
+ recordToUpdate.setValue("expiresAtTimestamp", input.expiresAtTimestamp);
+ }
+ else
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // else, do the default thing - which is, look for a default in the lock type //
+ ////////////////////////////////////////////////////////////////////////////////
+ ProcessLockType lockType = getProcessLockTypeById(processLock.getProcessLockTypeId());
+ if(lockType != null)
+ {
+ Integer defaultExpirationSeconds = lockType.getDefaultExpirationSeconds();
+ if(defaultExpirationSeconds != null)
+ {
+ recordToUpdate.setValue("expiresAtTimestamp", Instant.now().plusSeconds(defaultExpirationSeconds));
+ }
+ }
+ }
+
+ new UpdateAction().execute(new UpdateInput(ProcessLock.TABLE_NAME).withRecord(recordToUpdate));
+ LOG.debug("Checked in on process lock", logPair("id", processLock.getId()));
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error checking-in on process lock", e, logPair("processLockId", () -> processLock.getId()));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Do a check-in, with a specific value for the expiresAtTimestamp - which can
+ ** be set to null to make it null in the lock.
+ **
+ ** If you don't want to specify the expiresAtTimestamp, call the overload that
+ ** doesn't take the timestamp - in which case it'll either stay the same as it
+ ** was, or will be set based on the type's default.
+ *******************************************************************************/
+ public static void checkIn(ProcessLock processLock, Instant expiresAtTimestamp)
+ {
+ checkIn(new CheckInInput(processLock).withExpiresAtTimestamp(expiresAtTimestamp));
+ }
+
+
+
+ /*******************************************************************************
+ ** Do a check-in, updating the expires-timestamp based on the lock type's default.
+ ** (or leaving it the same as it was (null or otherwise) if there is no default
+ ** on the type).
+ *******************************************************************************/
+ public static void checkIn(ProcessLock processLock)
+ {
+ checkIn(new CheckInInput(processLock));
}
@@ -212,27 +380,32 @@ public class ProcessLockUtils
/*******************************************************************************
**
*******************************************************************************/
- public static void checkIn(ProcessLock processLock) throws QException
+ public static void releaseById(Integer id)
{
- ProcessLockType lockType = getProcessLockTypeById(processLock.getProcessLockTypeId());
- if(lockType == null)
+ if(id == null)
{
- throw (new QException("Unrecognized process lock type id: " + processLock.getProcessLockTypeId()));
+ LOG.debug("No id passed in to releaseById - returning with noop");
+ return;
}
- Instant now = Instant.now();
- QRecord recordToUpdate = new QRecord()
- .withValue("id", processLock.getId())
- .withValue("checkInTimestamp", now);
-
- Integer defaultExpirationSeconds = lockType.getDefaultExpirationSeconds();
- if(defaultExpirationSeconds != null)
+ ProcessLock processLock = null;
+ try
{
- recordToUpdate.setValue("expiresAtTimestamp", now.plusSeconds(defaultExpirationSeconds));
+ processLock = ProcessLockUtils.getById(id);
+ if(processLock == null)
+ {
+ LOG.info("Process lock not found in releaseById call", logPair("id", id));
+ }
+ }
+ catch(QException e)
+ {
+ LOG.warn("Exception releasing processLock byId", e, logPair("id", id));
}
- new UpdateAction().execute(new UpdateInput(ProcessLock.TABLE_NAME).withRecord(recordToUpdate));
- LOG.debug("Updated processLock checkInTimestamp", logPair("id", processLock.getId()), logPair("checkInTimestamp", now));
+ if(processLock != null)
+ {
+ release(processLock);
+ }
}
@@ -240,9 +413,20 @@ public class ProcessLockUtils
/*******************************************************************************
**
*******************************************************************************/
- public static void release(ProcessLock processLock) throws QException
+ public static void release(ProcessLock processLock)
{
- DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(processLock.getId()));
+ try
+ {
+ DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(processLock.getId()));
+ if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
+ {
+ throw (new QException("Error deleting processLock record: " + deleteOutput.getRecordsWithErrors().get(0).getErrorsAsString()));
+ }
+ }
+ catch(QException e)
+ {
+ LOG.warn("Exception releasing processLock", e, logPair("processLockId", () -> processLock.getId()));
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
index a76fe9b0..a3e81f44 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
@@ -343,6 +343,36 @@ public class GeneralProcessUtils
+ /*******************************************************************************
+ ** Load rows from a table matching the specified filter, into a map, keyed by the keyFieldName.
+ **
+ ** Note - null values from the key field are NOT put in the map.
+ **
+ ** If multiple values are found for the key, they'll squash each other, and only
+ ** one (random) value will appear.
+ *******************************************************************************/
+ public static Map loadTableToMap(String tableName, String keyFieldName, Class entityClass, QQueryFilter filter) throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(tableName);
+ queryInput.setFilter(filter);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ List records = queryOutput.getRecords();
+
+ Map map = new HashMap<>();
+ for(QRecord record : records)
+ {
+ Serializable value = record.getValue(keyFieldName);
+ if(value != null)
+ {
+ map.put(value, QRecordEntity.fromQRecord(entityClass, record));
+ }
+ }
+ return (map);
+ }
+
+
+
/*******************************************************************************
** Load rows from a table matching the specified filter, into a map, keyed by the keyFieldName.
**
@@ -412,7 +442,7 @@ public class GeneralProcessUtils
*******************************************************************************/
public static Map loadTableToMap(String tableName, String keyFieldName, Class entityClass) throws QException
{
- return (loadTableToMap(tableName, keyFieldName, entityClass, null));
+ return (loadTableToMap(tableName, keyFieldName, entityClass, (Consumer) null));
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java
new file mode 100644
index 00000000..a26523a3
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.processes;
+
+
+import java.util.UUID;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+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.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for CancelProcessAction
+ *******************************************************************************/
+public class CancelProcessActionTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testBadInputs()
+ {
+ RunProcessInput input = new RunProcessInput();
+ assertThatThrownBy(() -> new CancelProcessAction().execute(input))
+ .hasMessageContaining("Process [null] is not defined");
+
+ input.setProcessName("foobar");
+ assertThatThrownBy(() -> new CancelProcessAction().execute(input))
+ .hasMessageContaining("Process [foobar] is not defined");
+
+ input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE);
+ assertThatThrownBy(() -> new CancelProcessAction().execute(input))
+ .hasMessageContaining("processUUID was not given");
+
+ input.setProcessUUID(UUID.randomUUID().toString());
+ assertThatThrownBy(() -> new CancelProcessAction().execute(input))
+ .hasMessageContaining("State for process UUID")
+ .hasMessageContaining("was not found");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ try
+ {
+ ///////////////////////////////////////////////////////////////
+ // start up the process - having it break upon frontend step //
+ ///////////////////////////////////////////////////////////////
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE);
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
+ input.setProcessUUID(runProcessOutput.getProcessUUID());
+
+ /////////////////////////////////////////////////////////////////////////////////
+ // try to run the cancel action, but, with no cancel step, it should exit noop //
+ /////////////////////////////////////////////////////////////////////////////////
+ QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(CancelProcessAction.class);
+ new CancelProcessAction().execute(input);
+ assertThat(collectingLogger.getCollectedMessages())
+ .anyMatch(m -> m.getMessage().contains("does not have a custom cancel step"));
+ collectingLogger.clear();
+
+ ///////////////////////////////////////
+ // add a cancel step to this process //
+ ///////////////////////////////////////
+ QContext.getQInstance().getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE)
+ .setCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(CancelStep.class)));
+
+ new CancelProcessAction().execute(input);
+ assertThat(collectingLogger.getCollectedMessages())
+ .noneMatch(m -> m.getMessage().contains("does not have a custom cancel step"))
+ .anyMatch(m -> m.getMessage().contains("Running cancel step"));
+ assertEquals(1, CancelStep.callCount);
+ }
+ finally
+ {
+ QLogger.deactivateCollectingLoggerForClass(CancelProcessAction.class);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class CancelStep implements BackendStep
+ {
+ static int callCount = 0;
+
+
+
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ callCount++;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
index cacd8b59..96267363 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
@@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
@@ -69,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
@@ -154,7 +156,7 @@ public class QInstanceValidatorTest extends BaseTest
@Test
public void test_validateEmptyBackends()
{
- assertValidationFailureReasons((qInstance) -> qInstance.setBackends(new HashMap<>()),
+ assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.setBackends(new HashMap<>()),
"At least 1 backend must be defined");
}
@@ -393,6 +395,26 @@ public class QInstanceValidatorTest extends BaseTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test_validateProcessCancelSteps()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData()),
+ "Cancel step is missing a code reference");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference())),
+ "missing a code reference name", "missing a code type");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(ValidAuthCustomizer.class))),
+ "CodeReference is not of the expected type");
+
+ assertValidationSuccess((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(CancelProcessActionTest.CancelStep.class))));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -537,7 +559,8 @@ public class QInstanceValidatorTest extends BaseTest
////////////////////////////////////////////////////
// make sure if remove all plugins, we don't fail //
////////////////////////////////////////////////////
- assertValidationSuccess((qInstance) -> {});
+ assertValidationSuccess((qInstance) -> {
+ });
}
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java
index 72f88d3e..0bd1ddf3 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java
@@ -31,13 +31,18 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
+import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
+import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
+import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.data.QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS;
import static com.kingsrook.qqq.backend.core.model.data.QRecord.BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -250,4 +255,40 @@ class QRecordTest extends BaseTest
assertNotEquals(originalMap, cloneWithMapValue.getValue("myMap"));
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetErrorsAndWarningsAsString()
+ {
+ assertEquals("", new QRecord().getErrorsAsString());
+ assertEquals("one", new QRecord()
+ .withError(new BadInputStatusMessage("one"))
+ .getErrorsAsString());
+ assertEquals("one; two", new QRecord()
+ .withError(new BadInputStatusMessage("one"))
+ .withError(new SystemErrorStatusMessage("two"))
+ .getErrorsAsString());
+
+ assertEquals("", new QRecord().getWarningsAsString());
+ assertEquals("A", new QRecord()
+ .withWarning(new QWarningMessage("A"))
+ .getWarningsAsString());
+ assertEquals("A; B; C", new QRecord()
+ .withWarning(new QWarningMessage("A"))
+ .withWarning(new QWarningMessage("B"))
+ .withWarning(new QWarningMessage("C"))
+ .getWarningsAsString());
+
+ ///////////////////////////////////////////////////////////////////////////////////
+ // make sure this AsString method doesn't get included in our json serialization //
+ ///////////////////////////////////////////////////////////////////////////////////
+ String json = JsonUtils.toJson(new QRecord()
+ .withError(new BadInputStatusMessage("one")));
+ JSONObject jsonObject = new JSONObject(json);
+ assertFalse(jsonObject.has("errorsAsString"));
+ }
+
}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java
index b665fed0..2d58a3fd 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java
@@ -31,16 +31,19 @@ import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -66,6 +69,7 @@ class ProcessLockUtilsTest extends BaseTest
}
metaData.addSelfToInstance(qInstance);
+ new QInstanceValidator().revalidate(qInstance);
new InsertAction().execute(new InsertInput(ProcessLockType.TABLE_NAME).withRecordEntities(List.of(
new ProcessLockType()
@@ -102,7 +106,10 @@ class ProcessLockUtilsTest extends BaseTest
// make sure we can't create a second for the same key //
/////////////////////////////////////////////////////////
assertThatThrownBy(() -> ProcessLockUtils.create("1", "typeA", "you"))
- .isInstanceOf(UnableToObtainProcessLockException.class);
+ .isInstanceOf(UnableToObtainProcessLockException.class)
+ .hasMessageContaining("Held by: " + QContext.getQSession().getUser().getIdReference())
+ .hasMessageContaining("with details: me")
+ .hasMessageNotContaining("expiring at: 20");
/////////////////////////////////////////////////////////
// make sure we can create another for a different key //
@@ -124,7 +131,7 @@ class ProcessLockUtilsTest extends BaseTest
//////////////////////
processLock = ProcessLockUtils.create("1", "typeA", "you");
assertNotNull(processLock.getId());
- assertThat(processLock.getHolder()).endsWith("you");
+ assertEquals("you", processLock.getDetails());
assertThatThrownBy(() -> ProcessLockUtils.create("1", "notAType", "you"))
.isInstanceOf(QException.class)
@@ -149,7 +156,7 @@ class ProcessLockUtilsTest extends BaseTest
/////////////////////////////////////////////////////////////////////////
processLock = ProcessLockUtils.create("1", "typeB", "you", Duration.of(1, ChronoUnit.SECONDS), Duration.of(3, ChronoUnit.SECONDS));
assertNotNull(processLock.getId());
- assertThat(processLock.getHolder()).endsWith("you");
+ assertThat(processLock.getDetails()).endsWith("you");
}
@@ -169,7 +176,10 @@ class ProcessLockUtilsTest extends BaseTest
// make sure someone else fails, if they don't wait long enough //
//////////////////////////////////////////////////////////////////
assertThatThrownBy(() -> ProcessLockUtils.create("1", "typeC", "you", Duration.of(1, ChronoUnit.SECONDS), Duration.of(3, ChronoUnit.SECONDS)))
- .isInstanceOf(UnableToObtainProcessLockException.class);
+ .isInstanceOf(UnableToObtainProcessLockException.class)
+ .hasMessageContaining("Held by: " + QContext.getQSession().getUser().getIdReference())
+ .hasMessageContaining("with details: me")
+ .hasMessageContaining("expiring at: 20");
}
@@ -189,8 +199,183 @@ class ProcessLockUtilsTest extends BaseTest
ProcessLockUtils.checkIn(processLock);
ProcessLock freshLock = ProcessLockUtils.getById(processLock.getId());
+ assertNotNull(freshLock);
assertNotEquals(originalCheckIn, freshLock.getCheckInTimestamp());
assertNotEquals(originalExpiration, freshLock.getExpiresAtTimestamp());
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReleaseById() throws QException
+ {
+ ////////////////////////////////////////////
+ // assert no exceptions for these 2 cases //
+ ////////////////////////////////////////////
+ ProcessLockUtils.releaseById(null);
+ ProcessLockUtils.releaseById(1);
+
+ ProcessLock processLock = ProcessLockUtils.create("1", "typeA", "me");
+ ProcessLockUtils.releaseById(processLock.getId());
+ assertNull(ProcessLockUtils.getById(processLock.getId()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testUserAndSessionNullness() throws QException
+ {
+ {
+ QContext.getQSession().setUser(new QUser().withIdReference("me"));
+ ProcessLock processLock = ProcessLockUtils.create("1", "typeA", null);
+ assertNull(processLock.getDetails());
+ assertEquals("me", processLock.getUserId());
+ assertEquals(QContext.getQSession().getUuid(), processLock.getSessionUUID());
+ }
+
+ {
+ ProcessLock processLock = ProcessLockUtils.create("2", "typeA", "foo");
+ assertEquals("foo", processLock.getDetails());
+ assertEquals("me", processLock.getUserId());
+ assertEquals(QContext.getQSession().getUuid(), processLock.getSessionUUID());
+ }
+
+ {
+ QContext.getQSession().setUser(null);
+ ProcessLock processLock = ProcessLockUtils.create("3", "typeA", "bar");
+ assertEquals("bar", processLock.getDetails());
+ assertNull(processLock.getUserId());
+ assertEquals(QContext.getQSession().getUuid(), processLock.getSessionUUID());
+ }
+
+ {
+ QContext.getQSession().setUuid(null);
+ ProcessLock processLock = ProcessLockUtils.create("4", "typeA", "baz");
+ assertEquals("baz", processLock.getDetails());
+ assertNull(processLock.getUserId());
+ assertNull(processLock.getSessionUUID());
+ }
+
+ {
+ QContext.getQSession().setUuid(null);
+ ProcessLock processLock = ProcessLockUtils.create("5", "typeA", "");
+ assertEquals("", processLock.getDetails());
+ assertNull(processLock.getUserId());
+ assertNull(processLock.getSessionUUID());
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testCheckInExpiresAtTimestampsWithNoDefault() throws QException
+ {
+ /////////////////////////////////////////
+ // this type has no default expiration //
+ /////////////////////////////////////////
+ ProcessLock processLock = ProcessLockUtils.create("1", "typeA", null);
+ assertNull(processLock.getExpiresAtTimestamp());
+
+ /////////////////////////////////////////////////////////////
+ // checkin w/o specifying an expires-time - leaves it null //
+ /////////////////////////////////////////////////////////////
+ ProcessLockUtils.checkIn(processLock);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNull(processLock.getExpiresAtTimestamp());
+
+ //////////////////////////////////////////////
+ // checkin specifying null - leaves it null //
+ //////////////////////////////////////////////
+ ProcessLockUtils.checkIn(processLock, null);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNull(processLock.getExpiresAtTimestamp());
+
+ //////////////////////////////////////////////
+ // checkin w/ a time - sets it to that time //
+ //////////////////////////////////////////////
+ Instant specifiedTime = Instant.now();
+ ProcessLockUtils.checkIn(processLock, specifiedTime);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertEquals(specifiedTime, processLock.getExpiresAtTimestamp());
+
+ ///////////////////////////////////////////////////////////
+ // checkin w/o specifying time - leaves it previous time //
+ ///////////////////////////////////////////////////////////
+ SleepUtils.sleep(1, TimeUnit.MILLISECONDS);
+ ProcessLockUtils.checkIn(processLock);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertEquals(specifiedTime, processLock.getExpiresAtTimestamp());
+
+ ////////////////////////////////////////////////////
+ // checkin specifying null - puts it back to null //
+ ////////////////////////////////////////////////////
+ ProcessLockUtils.checkIn(processLock, null);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNull(processLock.getExpiresAtTimestamp());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testCheckInExpiresAtTimestampsWithSomeDefault() throws QException
+ {
+ /////////////////////////////////////////
+ // this type has a default expiration //
+ /////////////////////////////////////////
+ ProcessLock processLock = ProcessLockUtils.create("1", "typeB", null);
+ assertNotNull(processLock.getExpiresAtTimestamp());
+ Instant expiresAtTimestamp = processLock.getExpiresAtTimestamp();
+
+ ///////////////////////////////////////////////////////////////
+ // checkin w/o specifying an expires-time - moves it forward //
+ ///////////////////////////////////////////////////////////////
+ SleepUtils.sleep(1, TimeUnit.MILLISECONDS);
+ ProcessLockUtils.checkIn(processLock);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNotNull(processLock.getExpiresAtTimestamp());
+ assertNotEquals(expiresAtTimestamp, processLock.getExpiresAtTimestamp());
+
+ ///////////////////////////////////////////////
+ // checkin specifying null - sets it to null //
+ ///////////////////////////////////////////////
+ ProcessLockUtils.checkIn(processLock, null);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNull(processLock.getExpiresAtTimestamp());
+
+ //////////////////////////////////////////////
+ // checkin w/ a time - sets it to that time //
+ //////////////////////////////////////////////
+ Instant specifiedTime = Instant.now();
+ ProcessLockUtils.checkIn(processLock, specifiedTime);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertEquals(specifiedTime, processLock.getExpiresAtTimestamp());
+
+ /////////////////////////////////////////////////////////////////////////
+ // checkin w/o specifying time - uses the default and moves it forward //
+ /////////////////////////////////////////////////////////////////////////
+ SleepUtils.sleep(1, TimeUnit.MILLISECONDS);
+ ProcessLockUtils.checkIn(processLock);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNotEquals(specifiedTime, processLock.getExpiresAtTimestamp());
+
+ ////////////////////////////////////////////////////
+ // checkin specifying null - puts it back to null //
+ ////////////////////////////////////////////////////
+ ProcessLockUtils.checkIn(processLock, null);
+ processLock = ProcessLockUtils.getById(processLock.getId());
+ assertNull(processLock.getExpiresAtTimestamp());
+ }
+
}
\ No newline at end of file
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java
index 8f6cbcc3..3e8352eb 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java
@@ -30,6 +30,7 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -425,6 +426,7 @@ public class APILog extends QRecordEntity
/*******************************************************************************
** Getter for securityKeyValues
*******************************************************************************/
+ @QIgnore
public Map getSecurityKeyValues()
{
return (this.securityKeyValues);
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
index 957b5a21..5faa8c08 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
@@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
+import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessAction;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
@@ -130,6 +131,7 @@ public class QJavalinProcessHandler
post("/step/{step}", QJavalinProcessHandler::processStep);
get("/status/{jobUUID}", QJavalinProcessHandler::processStatus);
get("/records", QJavalinProcessHandler::processRecords);
+ get("/cancel", QJavalinProcessHandler::processCancel);
});
get("/possibleValues/{fieldName}", QJavalinProcessHandler::possibleValues);
@@ -768,6 +770,32 @@ public class QJavalinProcessHandler
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void processCancel(Context context)
+ {
+ try
+ {
+ RunProcessInput runProcessInput = new RunProcessInput();
+ QJavalinImplementation.setupSession(context, runProcessInput);
+
+ runProcessInput.setProcessName(context.pathParam("processName"));
+ runProcessInput.setProcessUUID(context.pathParam("processUUID"));
+
+ new CancelProcessAction().execute(runProcessInput);
+
+ Map resultForCaller = new HashMap<>();
+ context.result(JsonUtils.toJson(resultForCaller));
+ }
+ catch(Exception e)
+ {
+ QJavalinImplementation.handleException(context, e);
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
index 6a7ec840..74aa407c 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
@@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.javalin;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
+import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
@@ -604,4 +605,45 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
assertEquals(1, jsonObject.getJSONArray("options").getJSONObject(0).getInt("id"));
assertEquals("Darin Kelkhoff (1)", jsonObject.getJSONArray("options").getJSONObject(0).getString("label"));
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test_processCancel()
+ {
+ /////////////////////////
+ // 400s for bad inputs //
+ /////////////////////////
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/noSuchProcess/" + UUID.randomUUID() + "/cancel").asString();
+ assertEquals(400, response.getStatus());
+ assertThat(response.getBody()).contains("Process [noSuchProcess] is not defined in this instance");
+ }
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/" + UUID.randomUUID() + "/cancel").asString();
+ assertEquals(400, response.getStatus());
+ assertThat(response.getBody()).matches(".*State for process UUID.*not found.*");
+ }
+
+ ///////////////////////////////////
+ // start a process, get its uuid //
+ ///////////////////////////////////
+ String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
+ HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT)
+ .header("Content-Type", "application/json").asString();
+
+ JSONObject jsonObject = assertProcessStepCompleteResponse(response);
+ String processUUID = jsonObject.getString("processUUID");
+ assertNotNull(processUUID, "Process UUID should not be null.");
+
+ /////////////////////////////////////////
+ // now cancel that, and expect success //
+ /////////////////////////////////////////
+ response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE + "/" + processUUID + "/cancel").asString();
+ assertEquals(200, response.getStatus());
+ }
+
}