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