Merged feature/CE-938-order-release-automation into integration/sprint-43

This commit is contained in:
2024-05-29 10:48:00 -05:00
30 changed files with 1214 additions and 124 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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> 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);
}
}

View File

@ -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;
@ -71,9 +72,19 @@ public class RunBackendStepAction
QStepMetaData stepMetaData = process.getStep(runBackendStepInput.getStepName());
if(stepMetaData == null)
{
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))
{

View File

@ -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());

View File

@ -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);
}
/*******************************************************************************
**
*******************************************************************************/
@ -656,6 +672,8 @@ public class QInstanceValidator
if(assertCondition(qInstance.getTable(exposedJoin.getJoinTable()) != null, joinPrefix + "is referencing an unrecognized table"))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath."))
{
if(joinGraph != null)
{
joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
@ -668,6 +686,7 @@ public class QInstanceValidator
}
}
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());
@ -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 //
///////////////////////////////////////////////////////////////////////////////
@ -1486,8 +1513,12 @@ public class QInstanceValidator
}
if(process.getVariantBackend() != null)
{
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

View File

@ -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<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
/*******************************************************************************
@ -139,4 +145,36 @@ public class ProcessState implements Serializable
{
this.stepList = stepList;
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
}

View File

@ -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<QFrontendStepMetaData> updatedFrontendStepList = null;
private String overrideLastStepName; // todo - does this need to go into state too??
private List<AuditInput> auditInputList = new ArrayList<>();
@ -416,7 +415,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
return (this.processState.getUpdatedFrontendStepList());
}
@ -426,18 +425,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public RunBackendStepOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
}

View File

@ -46,8 +46,6 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
private String processUUID;
private Optional<Exception> exception = Optional.empty();
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
/*******************************************************************************
@ -334,32 +332,21 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
**
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
**
*******************************************************************************/
public RunProcessOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
return this.processState.getUpdatedFrontendStepList();
}
}

View File

@ -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");

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
{
}

View File

@ -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
*******************************************************************************/

View File

@ -218,6 +218,7 @@ public abstract class QRecordEntity
}
/*******************************************************************************
**
*******************************************************************************/
@ -295,10 +296,22 @@ public abstract class QRecordEntity
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
}
else
{
Optional<QIgnore> ignoreAnnotation = getQIgnoreAnnotation(c, fieldName);
Optional<QAssociation> 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
{
LOG.info("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
@ -360,6 +373,16 @@ public abstract class QRecordEntity
/*******************************************************************************
**
*******************************************************************************/
public static Optional<QIgnore> 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.");
}
}
}

View File

@ -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.");
}
}
}

View File

@ -60,6 +60,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map<String, QStepMetaData> 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);
}
}

View File

@ -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
**

View File

@ -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 //
///////////////////////////////////////////////////////////
@QIgnore
private String tableName;
private Set<String> 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<String> getJoinTableNames()
{
return (this.joinTableNames);

View File

@ -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));
}

View File

@ -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<String, String> getJobParametersMap()
{
if(CollectionUtils.nullSafeIsEmpty(this.jobParameters))
@ -469,6 +471,7 @@ public class ScheduledJob extends QRecordEntity
}
/*******************************************************************************
** Getter for repeatSeconds
*******************************************************************************/

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -63,7 +63,7 @@ public class ProcessLockMetaDataProducer implements MetaDataProducerInterface<Me
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("processLockTypeId", "key")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "processLockTypeId", "key")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("holder", "checkInTimestamp", "expiresAtTimestamp")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "sessionUUID", "details", "checkInTimestamp", "expiresAtTimestamp")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
);

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.processes.locks;
import java.io.Serializable;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
@ -44,7 +43,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -71,7 +72,7 @@ public class ProcessLockUtils
/*******************************************************************************
**
*******************************************************************************/
public static ProcessLock create(String key, String typeName, String holder) throws UnableToObtainProcessLockException, QException
public static ProcessLock create(String key, String typeName, String details) throws UnableToObtainProcessLockException, QException
{
ProcessLockType lockType = getProcessLockTypeByName(typeName);
if(lockType == null)
@ -80,13 +81,14 @@ public class ProcessLockUtils
}
QSession qSession = QContext.getQSession();
holder = qSession.getUser().getIdReference() + "-" + qSession.getUuid() + "-" + holder;
Instant now = Instant.now();
ProcessLock processLock = new ProcessLock()
.withKey(key)
.withProcessLockTypeId(lockType.getId())
.withHolder(holder)
.withSessionUUID(ObjectUtils.tryAndRequireNonNullElse(() -> 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,43 @@ 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;
}
@ -212,27 +261,118 @@ public class ProcessLockUtils
/*******************************************************************************
**
*******************************************************************************/
public static void checkIn(ProcessLock processLock) throws QException
public CheckInInput withExpiresAtTimestamp(Instant expiresAtTimestamp)
{
ProcessLockType lockType = getProcessLockTypeById(processLock.getProcessLockTypeId());
if(lockType == null)
{
throw (new QException("Unrecognized process lock type id: " + processLock.getProcessLockTypeId()));
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;
}
Instant now = Instant.now();
QRecord recordToUpdate = new QRecord()
.withValue("id", processLock.getId())
.withValue("checkInTimestamp", now);
.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", now.plusSeconds(defaultExpirationSeconds));
recordToUpdate.setValue("expiresAtTimestamp", Instant.now().plusSeconds(defaultExpirationSeconds));
}
}
}
new UpdateAction().execute(new UpdateInput(ProcessLock.TABLE_NAME).withRecord(recordToUpdate));
LOG.debug("Updated processLock checkInTimestamp", logPair("id", processLock.getId()), logPair("checkInTimestamp", now));
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));
}
@ -240,9 +380,53 @@ public class ProcessLockUtils
/*******************************************************************************
**
*******************************************************************************/
public static void release(ProcessLock processLock) throws QException
public static void releaseById(Integer id)
{
if(id == null)
{
LOG.debug("No id passed in to releaseById - returning with noop");
return;
}
ProcessLock processLock = null;
try
{
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));
}
if(processLock != null)
{
release(processLock);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void release(ProcessLock processLock)
{
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()));
}
}

View File

@ -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 <T extends QRecordEntity> Map<Serializable, T> loadTableToMap(String tableName, String keyFieldName, Class<T> entityClass, QQueryFilter filter) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<QRecord> records = queryOutput.getRecords();
Map<Serializable, T> 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 <T extends QRecordEntity> Map<Serializable, T> loadTableToMap(String tableName, String keyFieldName, Class<T> entityClass) throws QException
{
return (loadTableToMap(tableName, keyFieldName, entityClass, null));
return (loadTableToMap(tableName, keyFieldName, entityClass, (Consumer<QueryInput>) null));
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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++;
}
}
}

View File

@ -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) -> {
});
}
}

View File

@ -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"));
}
}

View File

@ -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());
}
}

View File

@ -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<String, Serializable> getSecurityKeyValues()
{
return (this.securityKeyValues);

View File

@ -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<String, Object> resultForCaller = new HashMap<>();
context.result(JsonUtils.toJson(resultForCaller));
}
catch(Exception e)
{
QJavalinImplementation.handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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<String> 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<String> 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<String> 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());
}
}