Compare commits

..

27 Commits

Author SHA1 Message Date
11e1fb86b2 CE-938 move updatedFrontendStepList into state 2024-05-29 10:17:43 -05:00
eb8bf12047 CE-938 Adding cancel-process action, cancelStep meta-data 2024-05-28 16:59:09 -05:00
66b2b4ff4c CE-938 More flexible check in (can update message and control expires-timestamp) 2024-05-24 16:59:56 -05:00
a6e0741175 CE-938: added new general process utils 2024-05-24 16:15:30 -05:00
2b7432167d Actually add the @QIgnore annotation :) 2024-05-23 13:00:19 -05:00
0a12c76829 Add withRecordLabelFormatAndFields to continue to make table-meta-data setup slightly less verbose 2024-05-23 12:56:49 -05:00
3fe6828550 Apply @QIgnore annotation, to silence some debug logs done as part of entity annotaiton processing 2024-05-23 12:56:22 -05:00
27a17183ae CE-938: renamed reportSetup widget to filterAndColumns 2024-05-22 16:23:25 -05:00
94bf10fe6e CE-938 Adding some null-tolerance 2024-05-22 12:05:26 -05:00
610915bf94 CE-938 Committed too early last time 2024-05-21 12:18:59 -05:00
3e26ea94ee CE-938 Make session & user explicit fields, instead of packing into "holder" 2024-05-21 11:35:00 -05:00
e6190b4fe2 CE-938 Add getErrorsAsString; getWarningsAsString; withWarning 2024-05-20 11:35:18 -05:00
1c582621aa CE-938 Add releaseById; Remove throws from release methods (so you don't always have to try-catch yourself); more robust holder processing 2024-05-20 11:34:57 -05:00
b91da93858 CE-938 Add missing javadoc 2024-05-19 20:29:25 -05:00
522dafca69 CE-938 Initial checkin of ProcessLocks 2024-05-19 20:26:05 -05:00
82f0f177fb CE-938 add overload that takes a Duration 2024-05-19 20:24:55 -05:00
9c79ce3272 CE-938 add isPrimaryKey to @QField 2024-05-19 20:24:55 -05:00
85eae36c28 CE-938 Add concept of MetaDataProducerMultiOutput 2024-05-19 20:24:55 -05:00
485bc618e0 CE-938 update memoization to say if it should store null values or not 2024-05-19 20:21:03 -05:00
be69836b5b Merged wip/qqq-bom-pom into dev 2024-05-15 20:16:53 -05:00
d528f984d4 Add qqq-bom as child module; mark qqq-bom-pom as packaging:pom 2024-05-15 20:03:37 -05:00
3335e29535 Merged wip/qqq-bom-pom into dev 2024-05-15 19:51:09 -05:00
04547577f7 For CE-1280 - add helpContent to process steps 2024-05-15 19:31:40 -05:00
2d9ea8b73f Merge pull request #91 from Kingsrook/feature/CE-1240-out-of-stock-summary-page
CE-1240: added multi table widget type
2024-05-15 19:17:21 -05:00
1292c04040 Merge pull request #90 from Kingsrook/feature/CE-1180-order-address-validation
Feature/ce 1180 order address validation
2024-05-15 19:15:44 -05:00
0e7c55e108 CE-1240: added multi table widget type 2024-05-03 20:29:50 -05:00
b8ac6d5d61 Initial attempt at a bom (bill of materials) pom 2024-05-03 20:24:27 -05:00
49 changed files with 3009 additions and 65 deletions

View File

@ -29,6 +29,7 @@
<packaging>pom</packaging>
<modules>
<module>qqq-bom</module>
<module>qqq-backend-core</module>
<module>qqq-backend-module-api</module>
<module>qqq-backend-module-filesystem</module>

View File

@ -0,0 +1,110 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.processes;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Action handler for running the cancel step of a qqq process
*
*******************************************************************************/
public class CancelProcessAction extends RunProcessAction
{
private static final QLogger LOG = QLogger.getLogger(CancelProcessAction.class);
/*******************************************************************************
**
*******************************************************************************/
public RunProcessOutput execute(RunProcessInput runProcessInput) throws QException
{
ActionHelper.validateSession(runProcessInput);
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
if(process == null)
{
throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
}
if(runProcessInput.getProcessUUID() == null)
{
throw (new QBadRequestException("Cannot cancel process - processUUID was not given."));
}
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
Optional<ProcessState> processState = getState(runProcessInput.getProcessUUID());
if(processState.isEmpty())
{
throw (new QBadRequestException("Cannot cancel process - State for process UUID [" + runProcessInput.getProcessUUID() + "] was not found."));
}
RunProcessOutput runProcessOutput = new RunProcessOutput();
try
{
if(process.getCancelStep() != null)
{
LOG.info("Running cancel step for process", logPair("processName", process.getName()));
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, process.getCancelStep(), process, processState.get());
}
else
{
LOG.debug("Process does not have a custom cancel step to run.", logPair("processName", process.getName()));
}
}
catch(QException qe)
{
////////////////////////////////////////////////////////////
// upon exception (e.g., one thrown by a step), throw it. //
////////////////////////////////////////////////////////////
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error cancelling process", e));
}
finally
{
//////////////////////////////////////////////////////
// always put the final state in the process result //
//////////////////////////////////////////////////////
runProcessOutput.setProcessState(processState.get());
}
return (runProcessOutput);
}
}

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -71,7 +72,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))

View File

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

View File

@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -111,6 +112,7 @@ public class QInstanceHelpContentManager
String processName = nameValuePairs.get("process");
String fieldName = nameValuePairs.get("field");
String sectionName = nameValuePairs.get("section");
String stepName = nameValuePairs.get("step");
String widgetName = nameValuePairs.get("widget");
String slotName = nameValuePairs.get("slot");
@ -145,7 +147,7 @@ public class QInstanceHelpContentManager
}
else if(StringUtils.hasContent(processName))
{
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
}
else if(StringUtils.hasContent(widgetName))
{
@ -208,6 +210,10 @@ public class QInstanceHelpContentManager
optionalSection.get().removeHelpContent(roles);
}
}
else
{
LOG.info("Unrecognized key format for table help content", logPair("key", key));
}
}
@ -215,7 +221,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
@ -244,6 +250,30 @@ public class QInstanceHelpContentManager
optionalField.get().removeHelpContent(roles);
}
}
else if(StringUtils.hasContent(stepName))
{
/////////////////////////////
// handle a process screen //
/////////////////////////////
QFrontendStepMetaData frontendStep = process.getFrontendStep(stepName);
if(frontendStep == null)
{
LOG.info("Unrecognized process step in help content", logPair("key", key));
}
else if(helpContent != null)
{
frontendStep.withHelpContent(helpContent);
}
else
{
frontendStep.removeHelpContent(roles);
}
}
else
{
LOG.info("Unrecognized key format for process help content", logPair("key", key));
}
}

View File

@ -109,6 +109,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
import org.quartz.CronExpression;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -151,6 +152,7 @@ public class QInstanceValidator
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
JoinGraph joinGraph = null;
long start = System.currentTimeMillis();
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -191,6 +193,9 @@ public class QInstanceValidator
validateUniqueTopLevelNames(qInstance);
runPlugins(QInstance.class, qInstance, qInstance);
long end = System.currentTimeMillis();
LOG.info("Validation (and enrichment) performance", logPair("millis", (end - start)));
}
catch(Exception e)
{
@ -209,6 +214,17 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
public void revalidate(QInstance qInstance) throws QInstanceValidationException
{
qInstance.setHasBeenValidated(null);
validate(qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
@ -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

View File

@ -23,8 +23,8 @@ package com.kingsrook.qqq.backend.core.model;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
/*******************************************************************************
@ -42,7 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
** implement this interface. or, same idea for a QRecordEntity that provides
** its own TableMetaData.
*******************************************************************************/
public interface MetaDataProducerInterface<T extends TopLevelMetaDataInterface>
public interface MetaDataProducerInterface<T extends MetaDataProducerOutput>
{
int DEFAULT_SORT_ORDER = 500;

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
/*******************************************************************************
@ -41,6 +42,11 @@ public class ProcessState implements Serializable
private List<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
/*******************************************************************************
@ -139,4 +145,36 @@ public class ProcessState implements Serializable
{
this.stepList = stepList;
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
}

View File

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

View File

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

View File

@ -54,6 +54,7 @@ public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidget
/////////////////////////////////////////////////////////////
// note, these are used in QQQ FMD CompositeWidgetData.tsx //
/////////////////////////////////////////////////////////////
FLEX_COLUMN,
FLEX_ROW_WRAPPED,
FLEX_ROW_SPACE_BETWEEN,
TABLE_SUB_ROW_DETAILS,

View File

@ -0,0 +1,97 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.util.List;
/*******************************************************************************
** Model containing datastructure expected by frontend bar chart widget
**
*******************************************************************************/
public class MultiTableData extends QWidgetData
{
List<TableData> tableDataList;
/*******************************************************************************
**
*******************************************************************************/
public MultiTableData()
{
}
/*******************************************************************************
**
*******************************************************************************/
public MultiTableData(List<TableData> tableDataList)
{
setTableDataList(tableDataList);
}
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public String getType()
{
return WidgetType.MULTI_TABLE.getType();
}
/*******************************************************************************
** Getter for tableDataList
*******************************************************************************/
public List<TableData> getTableDataList()
{
return (this.tableDataList);
}
/*******************************************************************************
** Setter for tableDataList
*******************************************************************************/
public void setTableDataList(List<TableData> tableDataList)
{
this.tableDataList = tableDataList;
}
/*******************************************************************************
** Fluent setter for tableDataList
*******************************************************************************/
public MultiTableData withTableDataList(List<TableData> tableDataList)
{
this.tableDataList = tableDataList;
return (this);
}
}

View File

@ -42,6 +42,7 @@ public enum WidgetType
SMALL_LINE_CHART("smallLineChart"),
LOCATION("location"),
MULTI_STATISTICS("multiStatistics"),
MULTI_TABLE("multiTable"),
PIE_CHART("pieChart"),
QUICK_SIGHT_CHART("quickSightChart"),
STATISTICS("statistics"),
@ -68,7 +69,7 @@ public enum WidgetType
DYNAMIC_FORM("dynamicForm"),
DATA_BAG_VIEWER("dataBagViewer"),
PIVOT_TABLE_SETUP("pivotTableSetup"),
REPORT_SETUP("reportSetup"),
FILTER_AND_COLUMNS_SETUP("filterAndColumnsSetup"),
SCRIPT_VIEWER("scriptViewer");

View File

@ -49,6 +49,11 @@ public @interface QField
*******************************************************************************/
String backendName() default "";
/*******************************************************************************
**
*******************************************************************************/
boolean isPrimaryKey() default false;
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
** Marker - that a piece of code should be ignored (e.g., a field not treated as
** a @QField)
*******************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface QIgnore
{
}

View File

@ -35,12 +35,15 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang3.SerializationUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -462,6 +465,7 @@ public class QRecord implements Serializable
}
/*******************************************************************************
** Getter for a single field's value
**
@ -616,6 +620,22 @@ public class QRecord implements Serializable
/*******************************************************************************
** Getter for errors
**
*******************************************************************************/
@JsonIgnore
public String getErrorsAsString()
{
if(CollectionUtils.nullSafeHasContents(errors))
{
return StringUtils.join("; ", errors.stream().map(e -> e.getMessage()).toList());
}
return ("");
}
/*******************************************************************************
** Setter for errors
**
@ -732,6 +752,22 @@ public class QRecord implements Serializable
/*******************************************************************************
** Getter for warnings
**
*******************************************************************************/
@JsonIgnore
public String getWarningsAsString()
{
if(CollectionUtils.nullSafeHasContents(warnings))
{
return StringUtils.join("; ", warnings.stream().map(e -> e.getMessage()).toList());
}
return ("");
}
/*******************************************************************************
** Setter for warnings
*******************************************************************************/
@ -742,6 +778,18 @@ public class QRecord implements Serializable
/*******************************************************************************
** Fluently Add one warning to this record
**
*******************************************************************************/
public QRecord withWarning(QWarningMessage warning)
{
addWarning(warning);
return (this);
}
/*******************************************************************************
** Fluent setter for warnings
*******************************************************************************/

View File

@ -218,6 +218,7 @@ public abstract class QRecordEntity
}
/*******************************************************************************
**
*******************************************************************************/
@ -296,7 +297,19 @@ public abstract class QRecordEntity
}
else
{
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
Optional<QIgnore> ignoreAnnotation = getQIgnoreAnnotation(c, fieldName);
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
if(ignoreAnnotation.isPresent() || associationAnnotation.isPresent())
{
////////////////////////////////////////////////////////////
// silently skip if marked as an association or an ignore //
////////////////////////////////////////////////////////////
}
else
{
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
}
}
}
else
@ -360,6 +373,16 @@ public abstract class QRecordEntity
/*******************************************************************************
**
*******************************************************************************/
public static Optional<QIgnore> getQIgnoreAnnotation(Class<? extends QRecordEntity> c, String ignoreName)
{
return (getAnnotationOnField(c, QIgnore.class, ignoreName));
}
/*******************************************************************************
**
*******************************************************************************/
@ -419,9 +442,9 @@ public abstract class QRecordEntity
}
else
{
if(!method.getName().equals("getClass"))
if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null)
{
LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
}
}
}

View File

@ -145,7 +145,7 @@ public interface QRecordEnum
{
if(!method.getName().equals("getClass") && !method.getName().equals("getDeclaringClass") && !method.getName().equals("getPossibleValueId"))
{
LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
}
}
}

View File

@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
** MetaDataProducerHelper, to put point at a package full of these, and populate
** your whole QInstance.
*******************************************************************************/
public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface> implements MetaDataProducerInterface<T>
public abstract class MetaDataProducer<T extends MetaDataProducerOutput> implements MetaDataProducerInterface<T>
{
}

View File

@ -157,7 +157,7 @@ public class MetaDataProducerHelper
{
try
{
TopLevelMetaDataInterface metaData = producer.produce(instance);
MetaDataProducerOutput metaData = producer.produce(instance);
if(metaData != null)
{
metaData.addSelfToInstance(instance);

View File

@ -0,0 +1,101 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Output object for a MetaDataProducer, which contains multiple meta-data
** objects.
*******************************************************************************/
public class MetaDataProducerMultiOutput implements MetaDataProducerOutput
{
private List<MetaDataProducerOutput> contents;
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance instance)
{
for(MetaDataProducerOutput metaDataProducerOutput : CollectionUtils.nonNullList(contents))
{
metaDataProducerOutput.addSelfToInstance(instance);
}
}
/*******************************************************************************
**
*******************************************************************************/
public void add(MetaDataProducerOutput metaDataProducerOutput)
{
if(contents == null)
{
contents = new ArrayList<>();
}
contents.add(metaDataProducerOutput);
}
/*******************************************************************************
**
*******************************************************************************/
public MetaDataProducerMultiOutput with(MetaDataProducerOutput metaDataProducerOutput)
{
add(metaDataProducerOutput);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public <T extends MetaDataProducerOutput> List<T> getEach(Class<T> c)
{
List<T> rs = new ArrayList<>();
for(MetaDataProducerOutput content : contents)
{
if(content instanceof MetaDataProducerMultiOutput multiOutput)
{
rs.addAll(multiOutput.getEach(c));
}
else if(c.isInstance(content))
{
rs.add(c.cast(content));
}
}
return (rs);
}
}

View File

@ -0,0 +1,40 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata;
/*******************************************************************************
** Interface to mark objects that can be produced by a MetaDataProducer.
**
** These would usually be TopLevelMetaData objects (a table, a process, etc)
** but can also be a MetaDataProducerMultiOutput, to produce multiple objects
** from one producer.
*******************************************************************************/
public interface MetaDataProducerOutput
{
/*******************************************************************************
** call the appropriate methods on a QInstance to add ourselves to it.
*******************************************************************************/
void addSelfToInstance(QInstance instance);
}

View File

@ -26,7 +26,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
** Interface for meta-data classes that can be added directly (e.g, at the top
** level) to a QInstance (such as a QTableMetaData - not a QFieldMetaData).
*******************************************************************************/
public interface TopLevelMetaDataInterface
public interface TopLevelMetaDataInterface extends MetaDataProducerOutput
{
/*******************************************************************************

View File

@ -26,8 +26,12 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
/*******************************************************************************
@ -43,6 +47,8 @@ public class QFrontendStepMetaData extends QStepMetaData
private List<QFieldMetaData> recordListFields;
private Map<String, QFieldMetaData> formFieldMap;
private List<QHelpContent> helpContents;
/*******************************************************************************
@ -340,4 +346,61 @@ public class QFrontendStepMetaData extends QStepMetaData
return (rs);
}
/*******************************************************************************
** Getter for helpContents
*******************************************************************************/
public List<QHelpContent> getHelpContents()
{
return (this.helpContents);
}
/*******************************************************************************
** Setter for helpContents
*******************************************************************************/
public void setHelpContents(List<QHelpContent> helpContents)
{
this.helpContents = helpContents;
}
/*******************************************************************************
** Fluent setter for helpContents
*******************************************************************************/
public QFrontendStepMetaData withHelpContents(List<QHelpContent> helpContents)
{
this.helpContents = helpContents;
return (this);
}
/*******************************************************************************
** Fluent setter for adding 1 helpContent
*******************************************************************************/
public QFrontendStepMetaData withHelpContent(QHelpContent helpContent)
{
if(this.helpContents == null)
{
this.helpContents = new ArrayList<>();
}
QInstanceHelpContentManager.putHelpContentInList(helpContent, this.helpContents);
return (this);
}
/*******************************************************************************
** remove a single helpContent based on its set of roles
*******************************************************************************/
public void removeHelpContent(Set<HelpRole> roles)
{
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents);
}
}

View File

@ -60,6 +60,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map<String, QStepMetaData> steps; // this is the full map of possible steps
private QBackendStepMetaData cancelStep;
private QIcon icon;
private QScheduleMetaData schedule;
@ -675,6 +677,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
}
/*******************************************************************************
** Getter for variantRunStrategy
*******************************************************************************/
@ -746,4 +749,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return steps;
}
/*******************************************************************************
** Getter for cancelStep
*******************************************************************************/
public QBackendStepMetaData getCancelStep()
{
return (this.cancelStep);
}
/*******************************************************************************
** Setter for cancelStep
*******************************************************************************/
public void setCancelStep(QBackendStepMetaData cancelStep)
{
this.cancelStep = cancelStep;
}
/*******************************************************************************
** Fluent setter for cancelStep
*******************************************************************************/
public QProcessMetaData withCancelStep(QBackendStepMetaData cancelStep)
{
this.cancelStep = cancelStep;
return (this);
}
}

View File

@ -111,6 +111,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private ShareableTableMetaData shareableTableMetaData;
/*******************************************************************************
** Default constructor.
*******************************************************************************/
@ -158,11 +159,26 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
public QTableMetaData withFieldsFromEntity(Class<? extends QRecordEntity> entityClass) throws QException
{
List<QRecordEntityField> recordEntityFieldList = QRecordEntity.getFieldList(entityClass);
boolean setPrimaryKey = false;
for(QRecordEntityField recordEntityField : recordEntityFieldList)
{
QFieldMetaData field = new QFieldMetaData(recordEntityField.getGetter());
addField(field);
if(recordEntityField.getFieldAnnotation().isPrimaryKey())
{
if(setPrimaryKey)
{
throw (new QException("Attempt to set more than one field as primary key (" + primaryKeyField + "," + field.getName() + ")."));
}
setPrimaryKeyField(field.getName());
setPrimaryKey = true;
}
}
return (this);
}
@ -624,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
**
@ -1388,6 +1416,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
}
/*******************************************************************************
** Getter for shareableTableMetaData
*******************************************************************************/
@ -1417,5 +1446,4 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
return (this);
}
}

View File

@ -28,6 +28,7 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
@ -77,7 +78,9 @@ public class QueryStat extends QRecordEntity
///////////////////////////////////////////////////////////
// non-persistent fields - used to help build the record //
///////////////////////////////////////////////////////////
private String tableName;
@QIgnore
private String tableName;
private Set<String> joinTableNames;
private QQueryFilter queryFilter;
@ -384,6 +387,7 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for queryFilter
*******************************************************************************/
@QIgnore
public QQueryFilter getQueryFilter()
{
return (this.queryFilter);
@ -446,6 +450,7 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for joinTableNames
*******************************************************************************/
@QIgnore
public Set<String> getJoinTableNames()
{
return (this.joinTableNames);

View File

@ -73,6 +73,7 @@ public class SavedReportsMetaDataProvider
public static final String RENDER_REPORT_PROCESS_VALUES_WIDGET = "renderReportProcessValuesWidget";
/*******************************************************************************
**
*******************************************************************************/
@ -234,7 +235,7 @@ public class SavedReportsMetaDataProvider
.withName("reportSetupWidget")
.withLabel("Filters and Columns")
.withIsCard(true)
.withType(WidgetType.REPORT_SETUP.getType())
.withType(WidgetType.FILTER_AND_COLUMNS_SETUP.getType())
.withCodeReference(new QCodeReference(DefaultWidgetRenderer.class));
}

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
@ -434,6 +435,7 @@ public class ScheduledJob extends QRecordEntity
/*******************************************************************************
** Getter for jobParameters - but a map of just the key=value pairs.
*******************************************************************************/
@QIgnore
public Map<String, String> getJobParametersMap()
{
if(CollectionUtils.nullSafeIsEmpty(this.jobParameters))
@ -469,6 +471,7 @@ public class ScheduledJob extends QRecordEntity
}
/*******************************************************************************
** Getter for repeatSeconds
*******************************************************************************/

View File

@ -144,6 +144,12 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
BackendStepPostRunOutput postRunOutput = new BackendStepPostRunOutput(runBackendStepOutput);
BackendStepPostRunInput postRunInput = new BackendStepPostRunInput(runBackendStepInput);
transformStep.postRun(postRunInput, postRunOutput);
// todo figure out what kind of test we can get on this
if(postRunOutput.getUpdatedFrontendStepList() != null)
{
runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
}
}

View File

@ -141,6 +141,11 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
BackendStepPostRunOutput postRunOutput = new BackendStepPostRunOutput(runBackendStepOutput);
BackendStepPostRunInput postRunInput = new BackendStepPostRunInput(runBackendStepInput);
transformStep.postRun(postRunInput, postRunOutput);
if(postRunOutput.getUpdatedFrontendStepList() != null)
{
runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
}
}

View File

@ -0,0 +1,398 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.locks;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
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;
/*******************************************************************************
** QRecord Entity for ProcessLock table
*******************************************************************************/
public class ProcessLock extends QRecordEntity
{
public static final String TABLE_NAME = "processLock";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String key;
@QField(possibleValueSourceName = ProcessLockType.TABLE_NAME)
private Integer processLockTypeId;
@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;
@QField()
private Instant expiresAtTimestamp;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public ProcessLock()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public ProcessLock(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ProcessLock withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ProcessLock withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ProcessLock withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for key
*******************************************************************************/
public String getKey()
{
return (this.key);
}
/*******************************************************************************
** Setter for key
*******************************************************************************/
public void setKey(String key)
{
this.key = key;
}
/*******************************************************************************
** Fluent setter for key
*******************************************************************************/
public ProcessLock withKey(String key)
{
this.key = key;
return (this);
}
/*******************************************************************************
** Getter for checkInTimestamp
*******************************************************************************/
public Instant getCheckInTimestamp()
{
return (this.checkInTimestamp);
}
/*******************************************************************************
** Setter for checkInTimestamp
*******************************************************************************/
public void setCheckInTimestamp(Instant checkInTimestamp)
{
this.checkInTimestamp = checkInTimestamp;
}
/*******************************************************************************
** Fluent setter for checkInTimestamp
*******************************************************************************/
public ProcessLock withCheckInTimestamp(Instant checkInTimestamp)
{
this.checkInTimestamp = checkInTimestamp;
return (this);
}
/*******************************************************************************
** Getter for expiresAtTimestamp
*******************************************************************************/
public Instant getExpiresAtTimestamp()
{
return (this.expiresAtTimestamp);
}
/*******************************************************************************
** Setter for expiresAtTimestamp
*******************************************************************************/
public void setExpiresAtTimestamp(Instant expiresAtTimestamp)
{
this.expiresAtTimestamp = expiresAtTimestamp;
}
/*******************************************************************************
** Fluent setter for expiresAtTimestamp
*******************************************************************************/
public ProcessLock withExpiresAtTimestamp(Instant expiresAtTimestamp)
{
this.expiresAtTimestamp = expiresAtTimestamp;
return (this);
}
/*******************************************************************************
** Getter for processLockTypeId
*******************************************************************************/
public Integer getProcessLockTypeId()
{
return (this.processLockTypeId);
}
/*******************************************************************************
** Setter for processLockTypeId
*******************************************************************************/
public void setProcessLockTypeId(Integer processLockTypeId)
{
this.processLockTypeId = processLockTypeId;
}
/*******************************************************************************
** Fluent setter for processLockTypeId
*******************************************************************************/
public ProcessLock withProcessLockTypeId(Integer processLockTypeId)
{
this.processLockTypeId = processLockTypeId;
return (this);
}
/*******************************************************************************
** Getter for userId
*******************************************************************************/
public String getUserId()
{
return (this.userId);
}
/*******************************************************************************
** Setter for userId
*******************************************************************************/
public void setUserId(String userId)
{
this.userId = userId;
}
/*******************************************************************************
** Fluent setter for userId
*******************************************************************************/
public ProcessLock withUserId(String userId)
{
this.userId = userId;
return (this);
}
/*******************************************************************************
** Getter for sessionUUID
*******************************************************************************/
public String getSessionUUID()
{
return (this.sessionUUID);
}
/*******************************************************************************
** Setter for sessionUUID
*******************************************************************************/
public void setSessionUUID(String sessionUUID)
{
this.sessionUUID = sessionUUID;
}
/*******************************************************************************
** Fluent setter for sessionUUID
*******************************************************************************/
public ProcessLock withSessionUUID(String sessionUUID)
{
this.sessionUUID = sessionUUID;
return (this);
}
/*******************************************************************************
** Getter for details
*******************************************************************************/
public String getDetails()
{
return (this.details);
}
/*******************************************************************************
** Setter for details
*******************************************************************************/
public void setDetails(String details)
{
this.details = details;
}
/*******************************************************************************
** Fluent setter for details
*******************************************************************************/
public ProcessLock withDetails(String details)
{
this.details = details;
return (this);
}
}

View File

@ -0,0 +1,104 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.locks;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
** MetaData producer for Process Locks "system"
*******************************************************************************/
public class ProcessLockMetaDataProducer implements MetaDataProducerInterface<MetaDataProducerMultiOutput>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
{
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
////////////////////////
// process lock table //
////////////////////////
output.add(new QTableMetaData()
.withName(ProcessLock.TABLE_NAME)
.withFieldsFromEntity(ProcessLock.class)
.withIcon(new QIcon().withName("sync_lock"))
.withUniqueKey(new UniqueKey("processLockTypeId", "key"))
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("processLockTypeId", "key")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "processLockTypeId", "key")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "sessionUUID", "details", "checkInTimestamp", "expiresAtTimestamp")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
);
/////////////////////////////
// process lock type table //
/////////////////////////////
output.add(new QTableMetaData()
.withName(ProcessLockType.TABLE_NAME)
.withFieldsFromEntity(ProcessLockType.class)
.withIcon(new QIcon().withName("lock"))
.withUniqueKey(new UniqueKey("name"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name", "label")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("defaultExpirationSeconds")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
);
///////////////////////////
// process lock type PVS //
///////////////////////////
output.add(QPossibleValueSource.newForTable(ProcessLockType.TABLE_NAME));
/////////////////////////////////////////////////////
// join between process lock type and process lock //
/////////////////////////////////////////////////////
output.add(new QJoinMetaData()
.withLeftTable(ProcessLockType.TABLE_NAME)
.withRightTable(ProcessLock.TABLE_NAME)
.withInferredName()
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("name", "processLockTypeId"))
);
return output;
}
}

View File

@ -0,0 +1,262 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.locks;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
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;
/*******************************************************************************
** QRecord Entity for ProcessLockType table
*******************************************************************************/
public class ProcessLockType extends QRecordEntity
{
public static final String TABLE_NAME = "processLockType";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String name;
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String label;
@QField()
private Integer defaultExpirationSeconds;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public ProcessLockType()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public ProcessLockType(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ProcessLockType withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ProcessLockType withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ProcessLockType withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public ProcessLockType withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for label
*******************************************************************************/
public String getLabel()
{
return (this.label);
}
/*******************************************************************************
** Setter for label
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
*******************************************************************************/
public ProcessLockType withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for defaultExpirationSeconds
*******************************************************************************/
public Integer getDefaultExpirationSeconds()
{
return (this.defaultExpirationSeconds);
}
/*******************************************************************************
** Setter for defaultExpirationSeconds
*******************************************************************************/
public void setDefaultExpirationSeconds(Integer defaultExpirationSeconds)
{
this.defaultExpirationSeconds = defaultExpirationSeconds;
}
/*******************************************************************************
** Fluent setter for defaultExpirationSeconds
*******************************************************************************/
public ProcessLockType withDefaultExpirationSeconds(Integer defaultExpirationSeconds)
{
this.defaultExpirationSeconds = defaultExpirationSeconds;
return (this);
}
}

View File

@ -0,0 +1,475 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.locks;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Utility class for working with ProcessLock table - creating, checking-in,
** and releasing process locks.
*******************************************************************************/
public class ProcessLockUtils
{
private static final QLogger LOG = QLogger.getLogger(ProcessLockUtils.class);
private static Memoization<String, ProcessLockType> getProcessLockTypeByNameMemoization = new Memoization<String, ProcessLockType>()
.withTimeout(Duration.ofHours(1))
.withMayStoreNullValues(false);
private static Memoization<Integer, ProcessLockType> getProcessLockTypeByIdMemoization = new Memoization<Integer, ProcessLockType>()
.withTimeout(Duration.ofHours(1))
.withMayStoreNullValues(false);
/*******************************************************************************
**
*******************************************************************************/
public static ProcessLock create(String key, String typeName, String details) throws UnableToObtainProcessLockException, QException
{
ProcessLockType lockType = getProcessLockTypeByName(typeName);
if(lockType == null)
{
throw (new QException("Unrecognized process lock type: " + typeName));
}
QSession qSession = QContext.getQSession();
Instant now = Instant.now();
ProcessLock processLock = new ProcessLock()
.withKey(key)
.withProcessLockTypeId(lockType.getId())
.withSessionUUID(ObjectUtils.tryAndRequireNonNullElse(() -> qSession.getUuid(), null))
.withUserId(ObjectUtils.tryAndRequireNonNullElse(() -> qSession.getUser().getIdReference(), null))
.withDetails(details)
.withCheckInTimestamp(now);
Integer defaultExpirationSeconds = lockType.getDefaultExpirationSeconds();
if(defaultExpirationSeconds != null)
{
processLock.setExpiresAtTimestamp(now.plusSeconds(defaultExpirationSeconds));
}
QRecord insertOutputRecord = tryToInsert(processLock);
////////////////////////////////////////////////////////////
// if inserting failed... see if we can get existing lock //
////////////////////////////////////////////////////////////
StringBuilder existingLockDetails = new StringBuilder();
if(CollectionUtils.nullSafeHasContents(insertOutputRecord.getErrors()))
{
QRecord existingLockRecord = new GetAction().executeForRecord(new GetInput(ProcessLock.TABLE_NAME).withUniqueKey(Map.of("key", key, "processLockTypeId", lockType.getId())));
if(existingLockRecord != null)
{
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("; expiring at: ").append(QValueFormatter.formatDateTimeWithZone(zonedExpiresAt));
}
if(expiresAtTimestamp != null && expiresAtTimestamp.isBefore(now))
{
/////////////////////////////////////////////////////////////////////////////////
// if existing lock has expired, then we can delete it and try to insert again //
/////////////////////////////////////////////////////////////////////////////////
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);
}
}
else
{
/////////////////////////////////////////////////////////
// if existing lock doesn't exist, try to insert again //
/////////////////////////////////////////////////////////
insertOutputRecord = tryToInsert(processLock);
}
}
if(CollectionUtils.nullSafeHasContents(insertOutputRecord.getErrors()))
{
/////////////////////////////////////////////////////////////////////////////////
// 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("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("details", details), logPair("expiresAtTimestamp", processLock.getExpiresAtTimestamp()));
return new ProcessLock(insertOutputRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private static QRecord tryToInsert(ProcessLock processLock) throws QException
{
return new InsertAction().execute(new InsertInput(ProcessLock.TABLE_NAME).withRecordEntity(processLock)).getRecords().get(0);
}
/*******************************************************************************
**
*******************************************************************************/
public static ProcessLock create(String key, String type, String holderId, Duration sleepBetweenTries, Duration maxWait) throws UnableToObtainProcessLockException, QException
{
Instant giveUpTime = Instant.now().plus(maxWait);
UnableToObtainProcessLockException lastCaughtUnableToObtainProcessLockException = null;
while(true)
{
try
{
ProcessLock processLock = create(key, type, holderId);
return (processLock);
}
catch(UnableToObtainProcessLockException e)
{
lastCaughtUnableToObtainProcessLockException = e;
if(Instant.now().plus(sleepBetweenTries).isBefore(giveUpTime))
{
SleepUtils.sleep(sleepBetweenTries);
}
else
{
break;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
// 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));
}
/*******************************************************************************
**
*******************************************************************************/
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);
}
/*******************************************************************************
** 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));
}
/*******************************************************************************
**
*******************************************************************************/
public static void releaseById(Integer id)
{
if(id == null)
{
LOG.debug("No id passed in to releaseById - returning with noop");
return;
}
ProcessLock processLock = null;
try
{
processLock = ProcessLockUtils.getById(id);
if(processLock == null)
{
LOG.info("Process lock not found in releaseById call", logPair("id", id));
}
}
catch(QException e)
{
LOG.warn("Exception releasing processLock byId", e, logPair("id", id));
}
if(processLock != null)
{
release(processLock);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void release(ProcessLock processLock)
{
try
{
DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(processLock.getId()));
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
{
throw (new QException("Error deleting processLock record: " + deleteOutput.getRecordsWithErrors().get(0).getErrorsAsString()));
}
}
catch(QException e)
{
LOG.warn("Exception releasing processLock", e, logPair("processLockId", () -> processLock.getId()));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static ProcessLockType getProcessLockTypeByName(String name)
{
Optional<ProcessLockType> result = getProcessLockTypeByNameMemoization.getResult(name, n ->
{
QRecord qRecord = new GetAction().executeForRecord(new GetInput(ProcessLockType.TABLE_NAME).withUniqueKey(Map.of("name", name)));
if(qRecord != null)
{
return (new ProcessLockType(qRecord));
}
return (null);
});
return (result.orElse(null));
}
/*******************************************************************************
**
*******************************************************************************/
private static ProcessLockType getProcessLockTypeById(Integer id)
{
Optional<ProcessLockType> result = getProcessLockTypeByIdMemoization.getResult(id, i ->
{
QRecord qRecord = new GetAction().executeForRecord(new GetInput(ProcessLockType.TABLE_NAME).withPrimaryKey(id));
if(qRecord != null)
{
return (new ProcessLockType(qRecord));
}
return (null);
});
return (result.orElse(null));
}
}

View File

@ -0,0 +1,52 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.locks;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
/*******************************************************************************
** Lock thrown by ProcessLockUtils when you can't get the lock.
*******************************************************************************/
public class UnableToObtainProcessLockException extends QUserFacingException
{
/*******************************************************************************
**
*******************************************************************************/
public UnableToObtainProcessLockException(String message)
{
super(message);
}
/*******************************************************************************
**
*******************************************************************************/
public UnableToObtainProcessLockException(String message, Throwable cause)
{
super(message, cause);
}
}

View File

@ -343,6 +343,36 @@ public class GeneralProcessUtils
/*******************************************************************************
** Load rows from a table matching the specified filter, into a map, keyed by the keyFieldName.
**
** Note - null values from the key field are NOT put in the map.
**
** If multiple values are found for the key, they'll squash each other, and only
** one (random) value will appear.
*******************************************************************************/
public static <T extends QRecordEntity> Map<Serializable, T> loadTableToMap(String tableName, String keyFieldName, Class<T> entityClass, QQueryFilter filter) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<QRecord> records = queryOutput.getRecords();
Map<Serializable, T> map = new HashMap<>();
for(QRecord record : records)
{
Serializable value = record.getValue(keyFieldName);
if(value != null)
{
map.put(value, QRecordEntity.fromQRecord(entityClass, record));
}
}
return (map);
}
/*******************************************************************************
** Load rows from a table matching the specified filter, into a map, keyed by the keyFieldName.
**
@ -412,7 +442,7 @@ public class GeneralProcessUtils
*******************************************************************************/
public static <T extends QRecordEntity> Map<Serializable, T> loadTableToMap(String tableName, String keyFieldName, Class<T> entityClass) throws QException
{
return (loadTableToMap(tableName, keyFieldName, entityClass, null));
return (loadTableToMap(tableName, keyFieldName, entityClass, (Consumer<QueryInput>) null));
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.utils;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@ -55,4 +56,14 @@ public class SleepUtils
}
}
/*******************************************************************************
** overload for sleep that takes duration object
*******************************************************************************/
public static void sleep(Duration sleepDuration)
{
sleep(sleepDuration.toMillis(), TimeUnit.MILLISECONDS);
}
}

View File

@ -43,8 +43,9 @@ public class Memoization<K, V>
private final Map<K, MemoizedResult<V>> map = Collections.synchronizedMap(new LinkedHashMap<>());
private Duration timeout = Duration.ofSeconds(600);
private Integer maxSize = 1000;
private Duration timeout = Duration.ofSeconds(600);
private Integer maxSize = 1000;
private boolean mayStoreNullValues = true;
@ -58,6 +59,40 @@ public class Memoization<K, V>
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Memoization(Integer maxSize)
{
this.maxSize = maxSize;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Memoization(Duration timeout)
{
this.timeout = timeout;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Memoization(Duration timeout, Integer maxSize)
{
this.timeout = timeout;
this.maxSize = maxSize;
}
/*******************************************************************************
** Get the memoized Value for a given input Key - computing it if it wasn't previously
** memoized (or expired).
@ -153,6 +188,14 @@ public class Memoization<K, V>
*******************************************************************************/
public void storeResult(K key, V value)
{
//////////////////////////////////////////////////////////////////////////////////////////
// if the value is null, and we're not supposed to store nulls, then return w/o storing //
//////////////////////////////////////////////////////////////////////////////////////////
if(value == null && !mayStoreNullValues)
{
return;
}
map.put(key, new MemoizedResult<>(value));
//////////////////////////////////////
@ -277,4 +320,35 @@ public class Memoization<K, V>
return (this);
}
/*******************************************************************************
** Getter for mayStoreNullValues
*******************************************************************************/
public boolean getMayStoreNullValues()
{
return (this.mayStoreNullValues);
}
/*******************************************************************************
** Setter for mayStoreNullValues
*******************************************************************************/
public void setMayStoreNullValues(boolean mayStoreNullValues)
{
this.mayStoreNullValues = mayStoreNullValues;
}
/*******************************************************************************
** Fluent setter for mayStoreNullValues
*******************************************************************************/
public Memoization<K, V> withMayStoreNullValues(boolean mayStoreNullValues)
{
this.mayStoreNullValues = mayStoreNullValues;
return (this);
}
}

View File

@ -0,0 +1,139 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.processes;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for CancelProcessAction
*******************************************************************************/
public class CancelProcessActionTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBadInputs()
{
RunProcessInput input = new RunProcessInput();
assertThatThrownBy(() -> new CancelProcessAction().execute(input))
.hasMessageContaining("Process [null] is not defined");
input.setProcessName("foobar");
assertThatThrownBy(() -> new CancelProcessAction().execute(input))
.hasMessageContaining("Process [foobar] is not defined");
input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE);
assertThatThrownBy(() -> new CancelProcessAction().execute(input))
.hasMessageContaining("processUUID was not given");
input.setProcessUUID(UUID.randomUUID().toString());
assertThatThrownBy(() -> new CancelProcessAction().execute(input))
.hasMessageContaining("State for process UUID")
.hasMessageContaining("was not found");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
try
{
///////////////////////////////////////////////////////////////
// start up the process - having it break upon frontend step //
///////////////////////////////////////////////////////////////
RunProcessInput input = new RunProcessInput();
input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE);
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
input.setProcessUUID(runProcessOutput.getProcessUUID());
/////////////////////////////////////////////////////////////////////////////////
// try to run the cancel action, but, with no cancel step, it should exit noop //
/////////////////////////////////////////////////////////////////////////////////
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(CancelProcessAction.class);
new CancelProcessAction().execute(input);
assertThat(collectingLogger.getCollectedMessages())
.anyMatch(m -> m.getMessage().contains("does not have a custom cancel step"));
collectingLogger.clear();
///////////////////////////////////////
// add a cancel step to this process //
///////////////////////////////////////
QContext.getQInstance().getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE)
.setCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(CancelStep.class)));
new CancelProcessAction().execute(input);
assertThat(collectingLogger.getCollectedMessages())
.noneMatch(m -> m.getMessage().contains("does not have a custom cancel step"))
.anyMatch(m -> m.getMessage().contains("Running cancel step"));
assertEquals(1, CancelStep.callCount);
}
finally
{
QLogger.deactivateCollectingLoggerForClass(CancelProcessAction.class);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class CancelStep implements BackendStep
{
static int callCount = 0;
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
callCount++;
}
}
}

View File

@ -167,6 +167,36 @@ class QInstanceHelpContentManagerTest extends BaseTest
QInstance qInstance = QContext.getQInstance();
new HelpContentMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
HelpContent recordEntity = new HelpContent()
.withId(1)
.withKey("process:" + TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE + ";step:setup")
.withContent("v1")
.withRole(HelpContentRole.PROCESS_SCREEN.getId());
new InsertAction().execute(new InsertInput(HelpContent.TABLE_NAME).withRecordEntity(recordEntity));
///////////////////////////////////////////////////////////////////////////////////////////////
// now - post-insert customizer should have automatically added help content to the instance //
///////////////////////////////////////////////////////////////////////////////////////////////
List<QHelpContent> helpContents = qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE).getFrontendStep("setup").getHelpContents();
assertEquals(1, helpContents.size());
assertEquals("v1", helpContents.get(0).getContent());
assertEquals(Set.of(QHelpRole.PROCESS_SCREEN), helpContents.get(0).getRoles());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testProcessStep() throws QException
{
/////////////////////////////////////
// get the instance from base test //
/////////////////////////////////////
QInstance qInstance = QContext.getQInstance();
new HelpContentMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
//////////////////////////////////////////////////////////
// first, assert there's no help content on the section //
//////////////////////////////////////////////////////////

View File

@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
@ -69,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
@ -154,7 +156,7 @@ public class QInstanceValidatorTest extends BaseTest
@Test
public void test_validateEmptyBackends()
{
assertValidationFailureReasons((qInstance) -> qInstance.setBackends(new HashMap<>()),
assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.setBackends(new HashMap<>()),
"At least 1 backend must be defined");
}
@ -393,6 +395,26 @@ public class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void test_validateProcessCancelSteps()
{
assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData()),
"Cancel step is missing a code reference");
assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference())),
"missing a code reference name", "missing a code type");
assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(ValidAuthCustomizer.class))),
"CodeReference is not of the expected type");
assertValidationSuccess((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(CancelProcessActionTest.CancelStep.class))));
}
/*******************************************************************************
**
*******************************************************************************/
@ -537,7 +559,8 @@ public class QInstanceValidatorTest extends BaseTest
////////////////////////////////////////////////////
// make sure if remove all plugins, we don't fail //
////////////////////////////////////////////////////
assertValidationSuccess((qInstance) -> {});
assertValidationSuccess((qInstance) -> {
});
}
}

View File

@ -31,13 +31,18 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.data.QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS;
import static com.kingsrook.qqq.backend.core.model.data.QRecord.BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -250,4 +255,40 @@ class QRecordTest extends BaseTest
assertNotEquals(originalMap, cloneWithMapValue.getValue("myMap"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetErrorsAndWarningsAsString()
{
assertEquals("", new QRecord().getErrorsAsString());
assertEquals("one", new QRecord()
.withError(new BadInputStatusMessage("one"))
.getErrorsAsString());
assertEquals("one; two", new QRecord()
.withError(new BadInputStatusMessage("one"))
.withError(new SystemErrorStatusMessage("two"))
.getErrorsAsString());
assertEquals("", new QRecord().getWarningsAsString());
assertEquals("A", new QRecord()
.withWarning(new QWarningMessage("A"))
.getWarningsAsString());
assertEquals("A; B; C", new QRecord()
.withWarning(new QWarningMessage("A"))
.withWarning(new QWarningMessage("B"))
.withWarning(new QWarningMessage("C"))
.getWarningsAsString());
///////////////////////////////////////////////////////////////////////////////////
// make sure this AsString method doesn't get included in our json serialization //
///////////////////////////////////////////////////////////////////////////////////
String json = JsonUtils.toJson(new QRecord()
.withError(new BadInputStatusMessage("one")));
JSONObject jsonObject = new JSONObject(json);
assertFalse(jsonObject.has("errorsAsString"));
}
}

View File

@ -0,0 +1,381 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.locks;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.TimeUnit;
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;
/*******************************************************************************
** Unit test for ProcessLockUtils
*******************************************************************************/
class ProcessLockUtilsTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QInstance qInstance = QContext.getQInstance();
MetaDataProducerMultiOutput metaData = new ProcessLockMetaDataProducer().produce(qInstance);
for(QTableMetaData table : metaData.getEach(QTableMetaData.class))
{
table.setBackendName(TestUtils.MEMORY_BACKEND_NAME);
}
metaData.addSelfToInstance(qInstance);
new QInstanceValidator().revalidate(qInstance);
new InsertAction().execute(new InsertInput(ProcessLockType.TABLE_NAME).withRecordEntities(List.of(
new ProcessLockType()
.withName("typeA")
.withLabel("Type A"),
new ProcessLockType()
.withName("typeB")
.withLabel("Type B")
.withDefaultExpirationSeconds(1),
new ProcessLockType()
.withName("typeC")
.withLabel("Type C")
.withDefaultExpirationSeconds(10)
)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
/////////////////////////////////////////
// make sure that we can create a lock //
/////////////////////////////////////////
ProcessLock processLock = ProcessLockUtils.create("1", "typeA", "me");
assertNotNull(processLock.getId());
assertNotNull(processLock.getCheckInTimestamp());
assertNull(processLock.getExpiresAtTimestamp());
/////////////////////////////////////////////////////////
// make sure we can't create a second for the same key //
/////////////////////////////////////////////////////////
assertThatThrownBy(() -> ProcessLockUtils.create("1", "typeA", "you"))
.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 //
/////////////////////////////////////////////////////////
ProcessLockUtils.create("2", "typeA", "him");
/////////////////////////////////////////////////////////////////////
// make sure we can create another for a different type (same key) //
/////////////////////////////////////////////////////////////////////
ProcessLockUtils.create("1", "typeB", "her");
//////////////////////////////
// make sure we can release //
//////////////////////////////
ProcessLockUtils.release(processLock);
//////////////////////
// and then you can //
//////////////////////
processLock = ProcessLockUtils.create("1", "typeA", "you");
assertNotNull(processLock.getId());
assertEquals("you", processLock.getDetails());
assertThatThrownBy(() -> ProcessLockUtils.create("1", "notAType", "you"))
.isInstanceOf(QException.class)
.hasMessageContaining("Unrecognized process lock type");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSucceedWaitingForExpiration() throws QException
{
ProcessLock processLock = ProcessLockUtils.create("1", "typeB", "me");
assertNotNull(processLock.getId());
assertNotNull(processLock.getCheckInTimestamp());
assertNotNull(processLock.getExpiresAtTimestamp());
/////////////////////////////////////////////////////////////////////////
// make sure someone else can, if they wait longer than the expiration //
/////////////////////////////////////////////////////////////////////////
processLock = ProcessLockUtils.create("1", "typeB", "you", Duration.of(1, ChronoUnit.SECONDS), Duration.of(3, ChronoUnit.SECONDS));
assertNotNull(processLock.getId());
assertThat(processLock.getDetails()).endsWith("you");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFailWaitingForExpiration() throws QException
{
ProcessLock processLock = ProcessLockUtils.create("1", "typeC", "me");
assertNotNull(processLock.getId());
assertNotNull(processLock.getCheckInTimestamp());
assertNotNull(processLock.getExpiresAtTimestamp());
//////////////////////////////////////////////////////////////////
// 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)
.hasMessageContaining("Held by: " + QContext.getQSession().getUser().getIdReference())
.hasMessageContaining("with details: me")
.hasMessageContaining("expiring at: 20");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCheckInUpdatesExpiration() throws QException
{
ProcessLock processLock = ProcessLockUtils.create("1", "typeB", "me");
assertNotNull(processLock.getId());
Instant originalCheckIn = processLock.getCheckInTimestamp();
Instant originalExpiration = processLock.getExpiresAtTimestamp();
SleepUtils.sleep(5, TimeUnit.MILLISECONDS);
ProcessLockUtils.checkIn(processLock);
ProcessLock freshLock = ProcessLockUtils.getById(processLock.getId());
assertNotNull(freshLock);
assertNotEquals(originalCheckIn, freshLock.getCheckInTimestamp());
assertNotEquals(originalExpiration, freshLock.getExpiresAtTimestamp());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testReleaseById() throws QException
{
////////////////////////////////////////////
// assert no exceptions for these 2 cases //
////////////////////////////////////////////
ProcessLockUtils.releaseById(null);
ProcessLockUtils.releaseById(1);
ProcessLock processLock = ProcessLockUtils.create("1", "typeA", "me");
ProcessLockUtils.releaseById(processLock.getId());
assertNull(ProcessLockUtils.getById(processLock.getId()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUserAndSessionNullness() throws QException
{
{
QContext.getQSession().setUser(new QUser().withIdReference("me"));
ProcessLock processLock = ProcessLockUtils.create("1", "typeA", null);
assertNull(processLock.getDetails());
assertEquals("me", processLock.getUserId());
assertEquals(QContext.getQSession().getUuid(), processLock.getSessionUUID());
}
{
ProcessLock processLock = ProcessLockUtils.create("2", "typeA", "foo");
assertEquals("foo", processLock.getDetails());
assertEquals("me", processLock.getUserId());
assertEquals(QContext.getQSession().getUuid(), processLock.getSessionUUID());
}
{
QContext.getQSession().setUser(null);
ProcessLock processLock = ProcessLockUtils.create("3", "typeA", "bar");
assertEquals("bar", processLock.getDetails());
assertNull(processLock.getUserId());
assertEquals(QContext.getQSession().getUuid(), processLock.getSessionUUID());
}
{
QContext.getQSession().setUuid(null);
ProcessLock processLock = ProcessLockUtils.create("4", "typeA", "baz");
assertEquals("baz", processLock.getDetails());
assertNull(processLock.getUserId());
assertNull(processLock.getSessionUUID());
}
{
QContext.getQSession().setUuid(null);
ProcessLock processLock = ProcessLockUtils.create("5", "typeA", "");
assertEquals("", processLock.getDetails());
assertNull(processLock.getUserId());
assertNull(processLock.getSessionUUID());
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCheckInExpiresAtTimestampsWithNoDefault() throws QException
{
/////////////////////////////////////////
// this type has no default expiration //
/////////////////////////////////////////
ProcessLock processLock = ProcessLockUtils.create("1", "typeA", null);
assertNull(processLock.getExpiresAtTimestamp());
/////////////////////////////////////////////////////////////
// checkin w/o specifying an expires-time - leaves it null //
/////////////////////////////////////////////////////////////
ProcessLockUtils.checkIn(processLock);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNull(processLock.getExpiresAtTimestamp());
//////////////////////////////////////////////
// checkin specifying null - leaves it null //
//////////////////////////////////////////////
ProcessLockUtils.checkIn(processLock, null);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNull(processLock.getExpiresAtTimestamp());
//////////////////////////////////////////////
// checkin w/ a time - sets it to that time //
//////////////////////////////////////////////
Instant specifiedTime = Instant.now();
ProcessLockUtils.checkIn(processLock, specifiedTime);
processLock = ProcessLockUtils.getById(processLock.getId());
assertEquals(specifiedTime, processLock.getExpiresAtTimestamp());
///////////////////////////////////////////////////////////
// checkin w/o specifying time - leaves it previous time //
///////////////////////////////////////////////////////////
SleepUtils.sleep(1, TimeUnit.MILLISECONDS);
ProcessLockUtils.checkIn(processLock);
processLock = ProcessLockUtils.getById(processLock.getId());
assertEquals(specifiedTime, processLock.getExpiresAtTimestamp());
////////////////////////////////////////////////////
// checkin specifying null - puts it back to null //
////////////////////////////////////////////////////
ProcessLockUtils.checkIn(processLock, null);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNull(processLock.getExpiresAtTimestamp());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCheckInExpiresAtTimestampsWithSomeDefault() throws QException
{
/////////////////////////////////////////
// this type has a default expiration //
/////////////////////////////////////////
ProcessLock processLock = ProcessLockUtils.create("1", "typeB", null);
assertNotNull(processLock.getExpiresAtTimestamp());
Instant expiresAtTimestamp = processLock.getExpiresAtTimestamp();
///////////////////////////////////////////////////////////////
// checkin w/o specifying an expires-time - moves it forward //
///////////////////////////////////////////////////////////////
SleepUtils.sleep(1, TimeUnit.MILLISECONDS);
ProcessLockUtils.checkIn(processLock);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNotNull(processLock.getExpiresAtTimestamp());
assertNotEquals(expiresAtTimestamp, processLock.getExpiresAtTimestamp());
///////////////////////////////////////////////
// checkin specifying null - sets it to null //
///////////////////////////////////////////////
ProcessLockUtils.checkIn(processLock, null);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNull(processLock.getExpiresAtTimestamp());
//////////////////////////////////////////////
// checkin w/ a time - sets it to that time //
//////////////////////////////////////////////
Instant specifiedTime = Instant.now();
ProcessLockUtils.checkIn(processLock, specifiedTime);
processLock = ProcessLockUtils.getById(processLock.getId());
assertEquals(specifiedTime, processLock.getExpiresAtTimestamp());
/////////////////////////////////////////////////////////////////////////
// checkin w/o specifying time - uses the default and moves it forward //
/////////////////////////////////////////////////////////////////////////
SleepUtils.sleep(1, TimeUnit.MILLISECONDS);
ProcessLockUtils.checkIn(processLock);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNotEquals(specifiedTime, processLock.getExpiresAtTimestamp());
////////////////////////////////////////////////////
// checkin specifying null - puts it back to null //
////////////////////////////////////////////////////
ProcessLockUtils.checkIn(processLock, null);
processLock = ProcessLockUtils.getById(processLock.getId());
assertNull(processLock.getExpiresAtTimestamp());
}
}

View File

@ -119,6 +119,57 @@ class MemoizationTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMayNotStoreNull()
{
Memoization<String, String> memoization = new Memoization<>();
memoization.setMayStoreNullValues(false);
AtomicInteger callCounter = new AtomicInteger();
callCounter.set(0);
UnsafeFunction<String, String, Exception> supplier = name ->
{
callCounter.getAndIncrement();
if(name.equals("throw"))
{
throw (new Exception("You asked me to throw"));
}
else if(name.equals("null"))
{
return (null);
}
else
{
return (name);
}
};
assertThat(memoization.getResult("null", supplier)).isEmpty();
assertEquals(1, callCounter.get());
assertThat(memoization.getResult("null", supplier)).isEmpty();
assertEquals(2, callCounter.get()); // should re-run the supplier, incrementing the counter
assertThat(memoization.getResult("throw", supplier)).isEmpty();
assertEquals(3, callCounter.get());
assertThat(memoization.getResult("throw", supplier)).isEmpty();
assertEquals(4, callCounter.get()); // should re-run the supplier, incrementing the counter
//noinspection AssertBetweenInconvertibleTypes
assertThat(memoization.getResult("foo", supplier)).isPresent().get().isEqualTo("foo");
assertEquals(5, callCounter.get());
//noinspection AssertBetweenInconvertibleTypes
assertThat(memoization.getResult("foo", supplier)).isPresent().get().isEqualTo("foo");
assertEquals(5, callCounter.get()); // should NOT re-run the supplier, NOT incrementing the counter
}
/*******************************************************************************
**
*******************************************************************************/

98
qqq-bom/pom.xml Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ QQQ - Low-code Application Framework for Engineers.
~ Copyright (C) 2021-2024. Kingsrook, LLC
~ 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
~ contact@kingsrook.com
~ https://github.com/Kingsrook/
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as
~ published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>qqq-bom-pom</artifactId>
<packaging>pom</packaging>
<parent>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-parent-project</artifactId>
<version>${revision}</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-mongodb</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-filesystem</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-slack</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-picocli</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-language-support-javascript</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
<distributionManagement>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</distributionManagement>
</project>

View File

@ -30,6 +30,7 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -425,6 +426,7 @@ public class APILog extends QRecordEntity
/*******************************************************************************
** Getter for securityKeyValues
*******************************************************************************/
@QIgnore
public Map<String, Serializable> getSecurityKeyValues()
{
return (this.securityKeyValues);

View File

@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessAction;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
@ -130,6 +131,7 @@ public class QJavalinProcessHandler
post("/step/{step}", QJavalinProcessHandler::processStep);
get("/status/{jobUUID}", QJavalinProcessHandler::processStatus);
get("/records", QJavalinProcessHandler::processRecords);
get("/cancel", QJavalinProcessHandler::processCancel);
});
get("/possibleValues/{fieldName}", QJavalinProcessHandler::possibleValues);
@ -768,6 +770,32 @@ public class QJavalinProcessHandler
/*******************************************************************************
**
*******************************************************************************/
private static void processCancel(Context context)
{
try
{
RunProcessInput runProcessInput = new RunProcessInput();
QJavalinImplementation.setupSession(context, runProcessInput);
runProcessInput.setProcessName(context.pathParam("processName"));
runProcessInput.setProcessUUID(context.pathParam("processUUID"));
new CancelProcessAction().execute(runProcessInput);
Map<String, Object> resultForCaller = new HashMap<>();
context.result(JsonUtils.toJson(resultForCaller));
}
catch(Exception e)
{
QJavalinImplementation.handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.javalin;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
@ -604,4 +605,45 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
assertEquals(1, jsonObject.getJSONArray("options").getJSONObject(0).getInt("id"));
assertEquals("Darin Kelkhoff (1)", jsonObject.getJSONArray("options").getJSONObject(0).getString("label"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test_processCancel()
{
/////////////////////////
// 400s for bad inputs //
/////////////////////////
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/noSuchProcess/" + UUID.randomUUID() + "/cancel").asString();
assertEquals(400, response.getStatus());
assertThat(response.getBody()).contains("Process [noSuchProcess] is not defined in this instance");
}
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/" + UUID.randomUUID() + "/cancel").asString();
assertEquals(400, response.getStatus());
assertThat(response.getBody()).matches(".*State for process UUID.*not found.*");
}
///////////////////////////////////
// start a process, get its uuid //
///////////////////////////////////
String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
HttpResponse<String> response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT)
.header("Content-Type", "application/json").asString();
JSONObject jsonObject = assertProcessStepCompleteResponse(response);
String processUUID = jsonObject.getString("processUUID");
assertNotNull(processUUID, "Process UUID should not be null.");
/////////////////////////////////////////
// now cancel that, and expect success //
/////////////////////////////////////////
response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE + "/" + processUUID + "/cancel").asString();
assertEquals(200, response.getStatus());
}
}