diff --git a/.gitignore b/.gitignore
index 0310e0fb..dffbdad6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ target/
*.iml
.env
+.idea
#############################################
## Original contents from github template: ##
diff --git a/pom.xml b/pom.xml
index de49e349..906f638a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,7 @@
qqq-backend-module-api
qqq-backend-module-filesystem
qqq-backend-module-rdbms
+ qqq-language-support-javascript
qqq-middleware-picocli
qqq-middleware-javalin
qqq-middleware-lambda
@@ -41,7 +42,7 @@
- 0.6.0-SNAPSHOT
+ 0.6.0
UTF-8
UTF-8
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java
index 3defb074..1260ec09 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
@@ -51,6 +52,8 @@ public class QuickSightChartRenderer extends AbstractWidgetRenderer
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
+ ActionHelper.validateSession(input);
+
try
{
QuickSightChartMetaData quickSightMetaData = (QuickSightChartMetaData) input.getWidgetMetaData();
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java
index 00ba8fca..fe2ed2c1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
@@ -40,6 +41,8 @@ public class RenderWidgetAction
*******************************************************************************/
public RenderWidgetOutput execute(RenderWidgetInput input) throws QException
{
+ ActionHelper.validateSession(input);
+
AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference());
return (widgetRenderer.render(input));
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
new file mode 100644
index 00000000..258cd856
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
@@ -0,0 +1,40 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.interfaces;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+
+
+/*******************************************************************************
+ ** Interface for the Get action.
+ **
+ *******************************************************************************/
+public interface GetInterface
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ GetOutput execute(GetInput getInput) throws QException;
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java
index 7527556e..9cf25b4c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java
@@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
@@ -69,8 +70,10 @@ public class MetaDataAction
Map tables = new LinkedHashMap<>();
for(Map.Entry entry : metaDataInput.getInstance().getTables().entrySet())
{
- tables.put(entry.getKey(), new QFrontendTableMetaData(entry.getValue(), false));
- treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
+ String tableName = entry.getKey();
+ QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
+ tables.put(tableName, new QFrontendTableMetaData(backendForTable, entry.getValue(), false));
+ treeNodes.put(tableName, new AppTreeNode(entry.getValue()));
}
metaDataOutput.setTables(tables);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java
index 62623bdd..d67ec855 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java
@@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@@ -52,7 +53,8 @@ public class TableMetaDataAction
{
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
- tableMetaDataOutput.setTable(new QFrontendTableMetaData(table, true));
+ QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
+ tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true));
// todo post-customization - can do whatever w/ the result if you want
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java
index 562f2d8a..34e33336 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java
@@ -23,26 +23,43 @@ package com.kingsrook.qqq.backend.core.actions.processes;
import java.io.Serializable;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
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.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+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.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
+import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.commons.lang.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -55,6 +72,15 @@ public class RunProcessAction
{
private static final Logger LOG = LogManager.getLogger(RunProcessAction.class);
+ 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";
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // indicator that the timestamp field should be updated - e.g., the execute step is finished. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp";
+
/*******************************************************************************
@@ -84,6 +110,18 @@ public class RunProcessAction
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
ProcessState processState = primeProcessState(runProcessInput, stateKey, process);
+ /////////////////////////////////////////////////////////
+ // if process is 'basepull' style, keep track of 'now' //
+ /////////////////////////////////////////////////////////
+ BasepullConfiguration basepullConfiguration = process.getBasepullConfiguration();
+ if(basepullConfiguration != null)
+ {
+ ///////////////////////////////////////
+ // get the stored basepull timestamp //
+ ///////////////////////////////////////
+ persistLastRunTime(runProcessInput, process, basepullConfiguration);
+ }
+
try
{
String lastStepName = runProcessInput.getStartAfterStep();
@@ -151,6 +189,15 @@ public class RunProcessAction
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // if 'basepull' style process, update the stored basepull timestamp //
+ // but only when we've been signaled to do so - i.e., after an Execute step runs. //
+ ////////////////////////////////////////////////////////////////////////////////////
+ if(basepullConfiguration != null && BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD))))
+ {
+ storeLastRunTime(runProcessInput, process, basepullConfiguration);
+ }
}
catch(QException qe)
{
@@ -250,7 +297,17 @@ public class RunProcessAction
runBackendStepInput.setTableName(process.getTableName());
runBackendStepInput.setSession(runProcessInput.getSession());
runBackendStepInput.setCallback(runProcessInput.getCallback());
+ runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
+
+ ///////////////////////////////////////////////////////////////
+ // if 'basepull' values are in the inputs, add to step input //
+ ///////////////////////////////////////////////////////////////
+ if(runProcessInput.getValues().containsKey(BASEPULL_LAST_RUNTIME_KEY))
+ {
+ runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
+ }
+
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, lastFunctionResult.getProcessState());
@@ -368,4 +425,129 @@ public class RunProcessAction
return (getStateProvider().get(ProcessState.class, stateKey));
}
+
+
+ /*******************************************************************************
+ ** Insert or update the last runtime value for this basepull into the backend.
+ *******************************************************************************/
+ protected void storeLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
+ {
+ String basepullTableName = basepullConfiguration.getTableName();
+ String basepullKeyFieldName = basepullConfiguration.getKeyField();
+ String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
+ String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
+
+ ///////////////////////////////////////
+ // get the stored basepull timestamp //
+ ///////////////////////////////////////
+ QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
+ queryInput.setSession(runProcessInput.getSession());
+ queryInput.setTableName(basepullTableName);
+ queryInput.setFilter(new QQueryFilter().withCriteria(
+ new QFilterCriteria()
+ .withFieldName(basepullKeyFieldName)
+ .withOperator(QCriteriaOperator.EQUALS)
+ .withValues(List.of(basepullKeyValue))));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ //////////////////////////////////////////
+ // get the runtime for this process run //
+ //////////////////////////////////////////
+ Instant newRunTime = (Instant) runProcessInput.getValues().get(BASEPULL_THIS_RUNTIME_KEY);
+
+ /////////////////////////////////////////////////
+ // update if found, otherwise insert new value //
+ /////////////////////////////////////////////////
+ if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
+ {
+ ///////////////////////////////////////////////////////////////////////////////
+ // update the basepull table with 'now' (which is before original query ran) //
+ ///////////////////////////////////////////////////////////////////////////////
+ QRecord basepullRecord = queryOutput.getRecords().get(0);
+ basepullRecord.setValue(basepullLastRunTimeFieldName, newRunTime);
+
+ ////////////
+ // update //
+ ////////////
+ UpdateInput updateInput = new UpdateInput(runProcessInput.getInstance());
+ updateInput.setSession(runProcessInput.getSession());
+ updateInput.setTableName(basepullTableName);
+ updateInput.setRecords(List.of(basepullRecord));
+ new UpdateAction().execute(updateInput);
+ }
+ else
+ {
+ QRecord basepullRecord = new QRecord()
+ .withValue(basepullKeyFieldName, basepullKeyValue)
+ .withValue(basepullLastRunTimeFieldName, newRunTime);
+
+ ////////////////////////////////
+ // insert new basepull record //
+ ////////////////////////////////
+ InsertInput insertInput = new InsertInput(runProcessInput.getInstance());
+ insertInput.setSession(runProcessInput.getSession());
+ insertInput.setTableName(basepullTableName);
+ insertInput.setRecords(List.of(basepullRecord));
+ new InsertAction().execute(insertInput);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Lookup the last runtime for this basepull, and set it (plus now) in the process's
+ ** values.
+ *******************************************************************************/
+ protected void persistLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
+ {
+ ////////////////////////////////////////////////////////
+ // if these values were already computed, don't re-do //
+ ////////////////////////////////////////////////////////
+ if(runProcessInput.getValue(BASEPULL_THIS_RUNTIME_KEY) != null)
+ {
+ return;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // store 'now', which will be used to update basepull record if process completes successfully //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ Instant now = Instant.now();
+ runProcessInput.getValues().put(BASEPULL_THIS_RUNTIME_KEY, now);
+
+ String basepullTableName = basepullConfiguration.getTableName();
+ String basepullKeyFieldName = basepullConfiguration.getKeyField();
+ String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
+ Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
+ String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
+
+ ///////////////////////////////////////
+ // get the stored basepull timestamp //
+ ///////////////////////////////////////
+ QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
+ queryInput.setSession(runProcessInput.getSession());
+ queryInput.setTableName(basepullTableName);
+ queryInput.setFilter(new QQueryFilter().withCriteria(
+ new QFilterCriteria()
+ .withFieldName(basepullKeyFieldName)
+ .withOperator(QCriteriaOperator.EQUALS)
+ .withValues(List.of(basepullKeyValue))));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // get the stored time, if not, default to 'now' unless a number of hours to offset was provided //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ Instant lastRunTime = now;
+ if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
+ {
+ QRecord basepullRecord = queryOutput.getRecords().get(0);
+ lastRunTime = ValueUtils.getValueAsInstant(basepullRecord.getValue(basepullLastRunTimeFieldName));
+ }
+ else if(basepullHoursBackForInitialTimestamp != null)
+ {
+ lastRunTime = lastRunTime.minus(basepullHoursBackForInitialTimestamp, ChronoUnit.HOURS);
+ }
+
+ runProcessInput.getValues().put(BASEPULL_LAST_RUNTIME_KEY, lastRunTime);
+ runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java
new file mode 100644
index 00000000..44db3df9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java
@@ -0,0 +1,119 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+
+
+/*******************************************************************************
+ ** Action to execute user/runtime defined code.
+ **
+ ** This action is designed to support code in multiple languages, by using
+ ** executors, e.g., provided by additional runtime qqq dependencies. Initially
+ ** we are building qqq-language-support-javascript.
+ **
+ ** We also have a Java executor, to provide at least a little bit of testability
+ ** within qqq-backend-core. This executor is a candidate to be replaced in the
+ ** future with something that would do actual dynamic java (whether that's compiled
+ ** at runtime, or loaded from a plugin jar at runtime). In other words, the java
+ ** executor in place today is just meant to be a placeholder.
+ *******************************************************************************/
+public class ExecuteCodeAction
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @SuppressWarnings("checkstyle:indentation")
+ public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
+ {
+ QCodeReference codeReference = input.getCodeReference();
+
+ QCodeExecutionLoggerInterface executionLogger = input.getExecutionLogger();
+ if(executionLogger == null)
+ {
+ executionLogger = getDefaultExecutionLogger();
+ }
+ executionLogger.acceptExecutionStart(input);
+
+ try
+ {
+ String languageExecutor = switch(codeReference.getCodeType())
+ {
+ case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
+ case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
+ };
+
+ @SuppressWarnings("unchecked")
+ Class extends QCodeExecutor> executorClass = (Class extends QCodeExecutor>) Class.forName(languageExecutor);
+ QCodeExecutor qCodeExecutor = executorClass.getConstructor().newInstance();
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
+ // merge all of the input context, plus the input... input - into a context for the code executor //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
+ Map context = new HashMap<>();
+ if(input.getContext() != null)
+ {
+ context.putAll(input.getContext());
+ }
+ if(input.getInput() != null)
+ {
+ context.putAll(input.getInput());
+ }
+
+ Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger);
+ output.setOutput(codeOutput);
+ executionLogger.acceptExecutionEnd(codeOutput);
+ }
+ catch(QCodeException qCodeException)
+ {
+ executionLogger.acceptException(qCodeException);
+ throw (qCodeException);
+ }
+ catch(Exception e)
+ {
+ executionLogger.acceptException(e);
+ throw (new QException("Error executing code [" + codeReference + "]", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QCodeExecutionLoggerInterface getDefaultExecutionLogger()
+ {
+ return (new Log4jCodeExecutionLogger());
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java
new file mode 100644
index 00000000..e6cd808a
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java
@@ -0,0 +1,44 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+
+
+/*******************************************************************************
+ ** Interface to be implemented by language-specific code executors, e.g., in
+ ** qqq-language-support-${languageName} maven modules.
+ *******************************************************************************/
+public interface QCodeExecutor
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java
new file mode 100644
index 00000000..61451b38
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java
@@ -0,0 +1,68 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+
+
+/*******************************************************************************
+ ** Java
+ *******************************************************************************/
+public class QJavaExecutor implements QCodeExecutor
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
+ {
+ Map context = new HashMap<>(inputContext);
+ if(!context.containsKey("logger"))
+ {
+ context.put("logger", executionLogger);
+ }
+
+ Serializable output;
+ try
+ {
+ Function, Serializable> function = QCodeLoader.getFunction(codeReference);
+ output = function.apply(context);
+ }
+ catch(Exception e)
+ {
+ QCodeException qCodeException = new QCodeException("Error executing script", e);
+ throw (qCodeException);
+ }
+
+ return (output);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java
new file mode 100644
index 00000000..9a88b15a
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java
@@ -0,0 +1,152 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
+import com.kingsrook.qqq.backend.core.model.scripts.Script;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunAssociatedScriptAction
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void run(RunAssociatedScriptInput input, RunAssociatedScriptOutput output) throws QException
+ {
+ ActionHelper.validateSession(input);
+
+ Serializable scriptId = getScriptId(input);
+ if(scriptId == null)
+ {
+ throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ + "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
+ }
+
+ Script script = getScript(input, scriptId);
+ if(script.getCurrentScriptRevisionId() == null)
+ {
+ throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ + "] (scriptId=" + scriptId + ") does not have a current version."));
+ }
+
+ ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
+
+ ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
+ executeCodeInput.setSession(input.getSession());
+ executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
+ executeCodeInput.setContext(new HashMap<>());
+ if(input.getOutputObject() != null)
+ {
+ executeCodeInput.getContext().put("output", input.getOutputObject());
+ }
+ executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
+ executeCodeInput.setExecutionLogger(new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+
+ output.setOutput(executeCodeOutput.getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException
+ {
+ GetInput getInput = new GetInput(input.getInstance());
+ getInput.setSession(input.getSession());
+ getInput.setTableName("scriptRevision");
+ getInput.setPrimaryKey(scriptRevisionId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ if(getOutput.getRecord() == null)
+ {
+ throw (new QNotFoundException("The current revision of the script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ + input.getCodeReference().getFieldName() + "] (scriptRevisionId=" + scriptRevisionId + ") was not found."));
+ }
+
+ return (new ScriptRevision(getOutput.getRecord()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException
+ {
+ GetInput getInput = new GetInput(input.getInstance());
+ getInput.setSession(input.getSession());
+ getInput.setTableName("script");
+ getInput.setPrimaryKey(scriptId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+
+ if(getOutput.getRecord() == null)
+ {
+ throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ + input.getCodeReference().getFieldName() + "] (script id=" + scriptId + ") was not found."));
+ }
+
+ return (new Script(getOutput.getRecord()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Serializable getScriptId(RunAssociatedScriptInput input) throws QException
+ {
+ GetInput getInput = new GetInput(input.getInstance());
+ getInput.setSession(input.getSession());
+ getInput.setTableName(input.getCodeReference().getRecordTable());
+ getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey());
+ GetOutput getOutput = new GetAction().execute(getInput);
+ if(getOutput.getRecord() == null)
+ {
+ throw (new QNotFoundException("The requested record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "] was not found."));
+ }
+
+ return (getOutput.getRecord().getValue(input.getCodeReference().getFieldName()));
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java
new file mode 100644
index 00000000..c4bfe711
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java
@@ -0,0 +1,222 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+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.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+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.metadata.tables.AssociatedScript;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** Action to store a new version of a script, associated with a record.
+ **
+ ** If there's never been a script assigned to the record (for the specified field),
+ ** then a new Script record is first inserted.
+ **
+ ** The script referenced by the record is always updated to point at the new
+ ** scriptRevision record that is inserted.
+ **
+ *******************************************************************************/
+public class StoreAssociatedScriptAction
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void run(StoreAssociatedScriptInput input, StoreAssociatedScriptOutput output) throws QException
+ {
+ ActionHelper.validateSession(input);
+
+ QTableMetaData table = input.getTable();
+ Optional optAssociatedScript = table.getAssociatedScripts().stream().filter(as -> as.getFieldName().equals(input.getFieldName())).findFirst();
+ if(optAssociatedScript.isEmpty())
+ {
+ throw (new QException("Field to update associated script for is not an associated script field."));
+ }
+ AssociatedScript associatedScript = optAssociatedScript.get();
+
+ /////////////////////////////////////////////////////////////
+ // get the record that the script is to be associated with //
+ /////////////////////////////////////////////////////////////
+ QRecord associatedRecord;
+ {
+ GetInput getInput = new GetInput(input.getInstance());
+ getInput.setSession(input.getSession());
+ getInput.setTableName(input.getTableName());
+ getInput.setPrimaryKey(input.getRecordPrimaryKey());
+ getInput.setShouldGenerateDisplayValues(true);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ associatedRecord = getOutput.getRecord();
+ }
+ if(associatedRecord == null)
+ {
+ throw (new QException("Record to associated with script was not found."));
+ }
+
+ //////////////////////////////////////////////////////////////////
+ // check if there's currently a script referenced by the record //
+ //////////////////////////////////////////////////////////////////
+ Serializable existingScriptId = associatedRecord.getValueString(input.getFieldName());
+ QRecord script;
+ Integer nextSequenceNo = 1;
+ if(existingScriptId == null)
+ {
+ ////////////////////////////////////////////////////////////////////
+ // get the script type - that'll be part of the new script's name //
+ ////////////////////////////////////////////////////////////////////
+ GetInput getInput = new GetInput(input.getInstance());
+ getInput.setSession(input.getSession());
+ getInput.setTableName("scriptType");
+ getInput.setPrimaryKey(associatedScript.getScriptTypeId());
+ getInput.setShouldGenerateDisplayValues(true);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ QRecord scriptType = getOutput.getRecord();
+ if(scriptType == null)
+ {
+ throw (new QException("Script type [" + associatedScript.getScriptTypeId() + "] was not found."));
+ }
+
+ /////////////////////////
+ // insert a new script //
+ /////////////////////////
+ script = new QRecord();
+ script.setValue("scriptTypeId", associatedScript.getScriptTypeId());
+ script.setValue("name", associatedRecord.getRecordLabel() + " - " + scriptType.getRecordLabel());
+ InsertInput insertInput = new InsertInput(input.getInstance());
+ insertInput.setSession(input.getSession());
+ insertInput.setTableName("script");
+ insertInput.setRecords(List.of(script));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ script = insertOutput.getRecords().get(0);
+
+ /////////////////////////////////////////////////////////////
+ // update the associated record to point at the new script //
+ /////////////////////////////////////////////////////////////
+ UpdateInput updateInput = new UpdateInput(input.getInstance());
+ updateInput.setSession(input.getSession());
+ updateInput.setTableName(input.getTableName());
+ updateInput.setRecords(List.of(new QRecord()
+ .withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField()))
+ .withValue(input.getFieldName(), script.getValue("id"))
+ ));
+ new UpdateAction().execute(updateInput);
+ }
+ else
+ {
+ ////////////////////////////////////////
+ // get the existing script, to update //
+ ////////////////////////////////////////
+ GetInput getInput = new GetInput(input.getInstance());
+ getInput.setSession(input.getSession());
+ getInput.setTableName("script");
+ getInput.setPrimaryKey(existingScriptId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ script = getOutput.getRecord();
+
+ QueryInput queryInput = new QueryInput(input.getInstance());
+ queryInput.setSession(input.getSession());
+ queryInput.setTableName("scriptRevision");
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
+ .withOrderBy(new QFilterOrderBy("sequenceNo", false))
+ );
+ queryInput.setLimit(1);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ if(!queryOutput.getRecords().isEmpty())
+ {
+ nextSequenceNo = queryOutput.getRecords().get(0).getValueInteger("sequenceNo") + 1;
+ }
+ }
+
+ //////////////////////////////////
+ // insert a new script revision //
+ //////////////////////////////////
+ String commitMessage = input.getCommitMessage();
+ if(!StringUtils.hasContent(commitMessage))
+ {
+ if(nextSequenceNo == 1)
+ {
+ commitMessage = "Initial version";
+ }
+ else
+ {
+ commitMessage = "No commit message given";
+ }
+ }
+
+ QRecord scriptRevision = new QRecord()
+ .withValue("scriptId", script.getValue("id"))
+ .withValue("contents", input.getCode())
+ .withValue("commitMessage", commitMessage)
+ .withValue("sequenceNo", nextSequenceNo);
+
+ try
+ {
+ scriptRevision.setValue("author", input.getSession().getUser().getFullName());
+ }
+ catch(Exception e)
+ {
+ scriptRevision.setValue("author", "Unknown");
+ }
+
+ InsertInput insertInput = new InsertInput(input.getInstance());
+ insertInput.setSession(input.getSession());
+ insertInput.setTableName("scriptRevision");
+ insertInput.setRecords(List.of(scriptRevision));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ scriptRevision = insertOutput.getRecords().get(0);
+
+ ////////////////////////////////////////////////////
+ // update the script to point at the new revision //
+ ////////////////////////////////////////////////////
+ script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
+ UpdateInput updateInput = new UpdateInput(input.getInstance());
+ updateInput.setSession(input.getSession());
+ updateInput.setTableName("script");
+ updateInput.setRecords(List.of(script));
+ new UpdateAction().execute(updateInput);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptAction.java
new file mode 100644
index 00000000..7e9e1e5d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptAction.java
@@ -0,0 +1,65 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.util.HashMap;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ ** Class for running a test of a script - e.g., maybe before it is saved.
+ *******************************************************************************/
+public class TestScriptAction
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void run(TestScriptInput input, TestScriptOutput output) throws QException
+ {
+ QTableMetaData table = input.getTable();
+
+ ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
+ executeCodeInput.setSession(input.getSession());
+ executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
+ executeCodeInput.setContext(new HashMap<>());
+ // todo! if(input.getOutputObject() != null)
+ // todo! {
+ // todo! executeCodeInput.getContext().put("output", input.getOutputObject());
+ // todo! }
+ executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(input.getCode()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
+ executeCodeInput.setExecutionLogger(new BuildScriptLogAndScriptLogLineExecutionLogger());
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+
+ // todo! output.setOutput(executeCodeOutput.getOutput());
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLogger.java
new file mode 100644
index 00000000..7dc4b45e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLogger.java
@@ -0,0 +1,221 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ ** Implementation of a code execution logger that builds a scriptLog and 0 or more
+ ** scriptLogLine records - but doesn't insert them. e.g., useful for testing
+ ** (both in junit, and for users in-app).
+ *******************************************************************************/
+public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface
+{
+ private static final Logger LOG = LogManager.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
+
+ private QRecord scriptLog;
+ private List scriptLogLines = new ArrayList<>();
+ protected ExecuteCodeInput executeCodeInput;
+
+ private Serializable scriptId;
+ private Serializable scriptRevisionId;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public BuildScriptLogAndScriptLogLineExecutionLogger()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public BuildScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
+ {
+ this.scriptId = scriptId;
+ this.scriptRevisionId = scriptRevisionId;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QRecord buildHeaderRecord(ExecuteCodeInput executeCodeInput)
+ {
+ return (new QRecord()
+ .withValue("scriptId", scriptId)
+ .withValue("scriptRevisionId", scriptRevisionId)
+ .withValue("startTimestamp", Instant.now())
+ .withValue("input", truncate(executeCodeInput.getInput())));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected QRecord buildDetailLogRecord(String logLine)
+ {
+ return (new QRecord()
+ .withValue("scriptLogId", scriptLog.getValue("id"))
+ .withValue("timestamp", Instant.now())
+ .withValue("text", truncate(logLine)));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String truncate(Object o)
+ {
+ return StringUtils.safeTruncate(ValueUtils.getValueAsString(o), 1000, "...");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected void updateHeaderAtEnd(Serializable output, Exception exception)
+ {
+ Instant startTimestamp = (Instant) scriptLog.getValue("startTimestamp");
+ Instant endTimestamp = Instant.now();
+ scriptLog.setValue("endTimestamp", endTimestamp);
+ scriptLog.setValue("runTimeMillis", startTimestamp.until(endTimestamp, ChronoUnit.MILLIS));
+
+ if(exception != null)
+ {
+ scriptLog.setValue("hadError", true);
+ scriptLog.setValue("error", exception.getMessage());
+ }
+ else
+ {
+ scriptLog.setValue("hadError", false);
+ scriptLog.setValue("output", truncate(output));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
+ {
+ try
+ {
+ this.executeCodeInput = executeCodeInput;
+ this.scriptLog = buildHeaderRecord(executeCodeInput);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error starting storage of script log", e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptLogLine(String logLine)
+ {
+ scriptLogLines.add(buildDetailLogRecord(logLine));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptException(Exception exception)
+ {
+ updateHeaderAtEnd(null, exception);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionEnd(Serializable output)
+ {
+ updateHeaderAtEnd(output, null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptLog
+ **
+ *******************************************************************************/
+ public QRecord getScriptLog()
+ {
+ return scriptLog;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptLogLines
+ **
+ *******************************************************************************/
+ public List getScriptLogLines()
+ {
+ return scriptLogLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptLog
+ **
+ *******************************************************************************/
+ protected void setScriptLog(QRecord scriptLog)
+ {
+ this.scriptLog = scriptLog;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/Log4jCodeExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/Log4jCodeExecutionLogger.java
new file mode 100644
index 00000000..498652ef
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/Log4jCodeExecutionLogger.java
@@ -0,0 +1,93 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.io.Serializable;
+import java.util.UUID;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ ** Implementation of a code execution logger that logs to LOG 4j
+ *******************************************************************************/
+public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
+{
+ private static final Logger LOG = LogManager.getLogger(Log4jCodeExecutionLogger.class);
+
+ private QCodeReference qCodeReference;
+ private String uuid = UUID.randomUUID().toString();
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
+ {
+ this.qCodeReference = executeCodeInput.getCodeReference();
+
+ String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
+ LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptLogLine(String logLine)
+ {
+ LOG.info("Script log: " + uuid + ": " + logLine);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptException(Exception exception)
+ {
+ LOG.info("Script Exception: " + uuid, exception);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionEnd(Serializable output)
+ {
+ String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "...");
+ LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/NoopCodeExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/NoopCodeExecutionLogger.java
new file mode 100644
index 00000000..d2e9e56d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/NoopCodeExecutionLogger.java
@@ -0,0 +1,74 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+
+
+/*******************************************************************************
+ ** Implementation of a code execution logger that just noop's every action.
+ *******************************************************************************/
+public class NoopCodeExecutionLogger implements QCodeExecutionLoggerInterface
+{
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptLogLine(String logLine)
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptException(Exception exception)
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionEnd(Serializable output)
+ {
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/QCodeExecutionLoggerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/QCodeExecutionLoggerInterface.java
new file mode 100644
index 00000000..5277084f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/QCodeExecutionLoggerInterface.java
@@ -0,0 +1,64 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+
+
+/*******************************************************************************
+ ** Interface to provide logging functionality to QCodeExecution (e.g., scripts)
+ *******************************************************************************/
+public interface QCodeExecutionLoggerInterface
+{
+
+ /*******************************************************************************
+ ** Called when the execution starts - takes the execution's input object.
+ *******************************************************************************/
+ void acceptExecutionStart(ExecuteCodeInput executeCodeInput);
+
+ /*******************************************************************************
+ ** Called to log a line, a message.
+ *******************************************************************************/
+ void acceptLogLine(String logLine);
+
+ /*******************************************************************************
+ ** In case the loggerInterface object is provided to the script as context,
+ ** this method gives a clean interface for the script to log a line.
+ *******************************************************************************/
+ default void log(String message)
+ {
+ acceptLogLine(message);
+ }
+
+ /*******************************************************************************
+ ** Called if the script fails with an exception.
+ *******************************************************************************/
+ void acceptException(Exception exception);
+
+ /*******************************************************************************
+ ** Called if the script completes without exception.
+ *******************************************************************************/
+ void acceptExecutionEnd(Serializable output);
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLogger.java
new file mode 100644
index 00000000..6e0b84fc
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLogger.java
@@ -0,0 +1,136 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.io.Serializable;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ ** Implementation of a code execution logger that logs into scriptLog and scriptLogLine
+ ** tables - e.g., as defined in ScriptMetaDataProvider.
+ *******************************************************************************/
+public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger
+{
+ private static final Logger LOG = LogManager.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public StoreScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
+ {
+ super(scriptId, scriptRevisionId);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
+ {
+ try
+ {
+ super.acceptExecutionStart(executeCodeInput);
+
+ InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
+ insertInput.setSession(executeCodeInput.getSession());
+ insertInput.setTableName("scriptLog");
+ insertInput.setRecords(List.of(getScriptLog()));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+
+ setScriptLog(insertOutput.getRecords().get(0));
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error starting storage of script log", e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptException(Exception exception)
+ {
+ store(null, exception);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void acceptExecutionEnd(Serializable output)
+ {
+ store(output, null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void store(Serializable output, Exception exception)
+ {
+ try
+ {
+ updateHeaderAtEnd(output, exception);
+ UpdateInput updateInput = new UpdateInput(executeCodeInput.getInstance());
+ updateInput.setSession(executeCodeInput.getSession());
+ updateInput.setTableName("scriptLog");
+ updateInput.setRecords(List.of(getScriptLog()));
+ new UpdateAction().execute(updateInput);
+
+ if(CollectionUtils.nullSafeHasContents(getScriptLogLines()))
+ {
+ InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
+ insertInput.setSession(executeCodeInput.getSession());
+ insertInput.setTableName("scriptLogLine");
+ insertInput.setRecords(getScriptLogLines());
+ new InsertAction().execute(insertInput);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error storing script log", e);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
new file mode 100644
index 00000000..da923b7b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
@@ -0,0 +1,161 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.tables;
+
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
+import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
+import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
+import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+
+
+/*******************************************************************************
+ ** Action to run a get against a table.
+ **
+ *******************************************************************************/
+public class GetAction
+{
+ private Optional> postGetRecordCustomizer;
+
+ private GetInput getInput;
+ private QValueFormatter qValueFormatter;
+ private QPossibleValueTranslator qPossibleValueTranslator;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetOutput execute(GetInput getInput) throws QException
+ {
+ ActionHelper.validateSession(getInput);
+
+ postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
+ this.getInput = getInput;
+
+ QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
+ QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(getInput.getBackend());
+ // todo pre-customization - just get to modify the request?
+
+ GetInterface getInterface = null;
+ try
+ {
+ getInterface = qModule.getGetInterface();
+ }
+ catch(IllegalStateException ise)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // if a module doesn't implement Get directly - try to do a Get by a Query by the primary key //
+ // see below. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ }
+
+ GetOutput getOutput;
+ if(getInterface != null)
+ {
+ getOutput = getInterface.execute(getInput);
+ }
+ else
+ {
+ getOutput = performGetViaQuery(getInput);
+ }
+
+ if(getOutput.getRecord() != null)
+ {
+ getOutput.setRecord(postRecordActions(getOutput.getRecord()));
+ }
+
+ return getOutput;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private GetOutput performGetViaQuery(GetInput getInput) throws QException
+ {
+ QueryInput queryInput = new QueryInput(getInput.getInstance());
+ queryInput.setSession(getInput.getSession());
+ queryInput.setTableName(getInput.getTableName());
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey()))));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ GetOutput getOutput = new GetOutput();
+ if(!queryOutput.getRecords().isEmpty())
+ {
+ getOutput.setRecord(queryOutput.getRecords().get(0));
+ }
+ return (getOutput);
+ }
+
+
+
+ /*******************************************************************************
+ ** Run the necessary actions on a record. This may include setting display values,
+ ** translating possible values, and running post-record customizations.
+ *******************************************************************************/
+ public QRecord postRecordActions(QRecord record)
+ {
+ QRecord returnRecord = record;
+ if(this.postGetRecordCustomizer.isPresent())
+ {
+ returnRecord = postGetRecordCustomizer.get().apply(record);
+ }
+
+ if(getInput.getShouldTranslatePossibleValues())
+ {
+ if(qPossibleValueTranslator == null)
+ {
+ qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession());
+ }
+ qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord));
+ }
+
+ if(getInput.getShouldGenerateDisplayValues())
+ {
+ if(qValueFormatter == null)
+ {
+ qValueFormatter = new QValueFormatter();
+ }
+ qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
+ }
+
+ return (returnRecord);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
index 31ccea38..31c3f315 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
@@ -406,8 +406,11 @@ public class QPossibleValueTranslator
/////////////////////////////////////////////////////////////////////////////////////////
// this is needed to get record labels, which are what we use here... unclear if best! //
/////////////////////////////////////////////////////////////////////////////////////////
- queryInput.setShouldTranslatePossibleValues(true);
- queryInput.setShouldGenerateDisplayValues(true);
+ if(notTooDeep())
+ {
+ queryInput.setShouldTranslatePossibleValues(true);
+ queryInput.setShouldGenerateDisplayValues(true);
+ }
QueryOutput queryOutput = new QueryAction().execute(queryInput);
@@ -428,4 +431,24 @@ public class QPossibleValueTranslator
}
}
+
+
+ /*******************************************************************************
+ ** Avoid infinite recursion, for where one field's PVS depends on another's...
+ ** not too smart, just breaks at 5...
+ *******************************************************************************/
+ private boolean notTooDeep()
+ {
+ int count = 0;
+ for(StackTraceElement stackTraceElement : new Throwable().getStackTrace())
+ {
+ if(stackTraceElement.getMethodName().equals("translatePossibleValuesInRecords"))
+ {
+ count++;
+ }
+ }
+
+ return (count < 5);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java
index 791986a2..e6772489 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java
@@ -184,6 +184,7 @@ public class QValueFormatter
}
catch(Exception e)
{
+ LOG.debug("Error formatting record label", e);
return (formatRecordLabelExceptionalCases(table, record));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QCodeException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QCodeException.java
new file mode 100644
index 00000000..12e16fb5
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QCodeException.java
@@ -0,0 +1,79 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.exceptions;
+
+
+/*******************************************************************************
+ ** Exception thrown while executing custom code in QQQ.
+ **
+ ** Context field is meant to give the user "context" for where the error occurred
+ ** - e.g., a line number or word that was bad.
+ *******************************************************************************/
+public class QCodeException extends QException
+{
+ private String context;
+
+
+
+ /*******************************************************************************
+ ** Constructor of message
+ **
+ *******************************************************************************/
+ public QCodeException(String message)
+ {
+ super(message);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor of message & cause
+ **
+ *******************************************************************************/
+ public QCodeException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for context
+ **
+ *******************************************************************************/
+ public String getContext()
+ {
+ return context;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for context
+ **
+ *******************************************************************************/
+ public void setContext(String context)
+ {
+ this.context = context;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
index 573b8ba2..078c7843 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
@@ -316,6 +316,24 @@ public class QInstanceEnricher
{
generateAppSections(app);
}
+
+ for(QAppSection section : CollectionUtils.nonNullList(app.getSections()))
+ {
+ enrichAppSection(section);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void enrichAppSection(QAppSection section)
+ {
+ if(!StringUtils.hasContent(section.getLabel()))
+ {
+ section.setLabel(nameToLabel(section.getName()));
+ }
}
@@ -590,7 +608,7 @@ public class QInstanceEnricher
** TLAAndAnotherTLA -> tla_and_another_tla
**
*******************************************************************************/
- static String inferBackendName(String fieldName)
+ public static String inferBackendName(String fieldName)
{
////////////////////////////////////////////////////////////////////////////////////////
// build a list of words in the name, then join them with _ and lower-case the result //
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index 6f344f10..88bf8272 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -31,13 +31,16 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
@@ -50,6 +53,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
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;
@@ -118,6 +123,7 @@ public class QInstanceValidator
validateAutomationProviders(qInstance);
validateTables(qInstance);
validateProcesses(qInstance);
+ validateReports(qInstance);
validateApps(qInstance);
validatePossibleValueSources(qInstance);
validateQueuesAndProviders(qInstance);
@@ -186,6 +192,8 @@ public class QInstanceValidator
qInstance.getBackends().forEach((backendName, backend) ->
{
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
+
+ backend.performValidation(this);
});
}
}
@@ -258,6 +266,8 @@ public class QInstanceValidator
//////////////////////////////////////////
Set fieldNamesInSections = new HashSet<>();
QFieldSection tier1Section = null;
+ Set usedSectionNames = new HashSet<>();
+ Set usedSectionLabels = new HashSet<>();
if(table.getSections() != null)
{
for(QFieldSection section : table.getSections())
@@ -268,6 +278,12 @@ public class QInstanceValidator
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
tier1Section = section;
}
+
+ assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName());
+ usedSectionNames.add(section.getName());
+
+ assertCondition(!usedSectionLabels.contains(section.getLabel()), "Table " + tableName + " has more than 1 section labeled " + section.getLabel());
+ usedSectionLabels.add(section.getLabel());
}
}
@@ -716,6 +732,133 @@ public class QInstanceValidator
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void validateReports(QInstance qInstance)
+ {
+ if(CollectionUtils.nullSafeHasContents(qInstance.getReports()))
+ {
+ qInstance.getReports().forEach((reportName, report) ->
+ {
+ assertCondition(Objects.equals(reportName, report.getName()), "Inconsistent naming for report: " + reportName + "/" + report.getName() + ".");
+ validateAppChildHasValidParentAppName(qInstance, report);
+
+ ////////////////////////////////////////
+ // validate dataSources in the report //
+ ////////////////////////////////////////
+ Set usedDataSourceNames = new HashSet<>();
+ if(assertCondition(CollectionUtils.nullSafeHasContents(report.getDataSources()), "At least 1 data source must be defined in report " + reportName + "."))
+ {
+ int index = 0;
+ for(QReportDataSource dataSource : report.getDataSources())
+ {
+ assertCondition(StringUtils.hasContent(dataSource.getName()), "Missing name for a dataSource at index " + index + " in report " + reportName);
+ index++;
+
+ assertCondition(!usedDataSourceNames.contains(dataSource.getName()), "More than one dataSource with name " + dataSource.getName() + " in report " + reportName);
+ usedDataSourceNames.add(dataSource.getName());
+
+ String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " ";
+
+ if(StringUtils.hasContent(dataSource.getSourceTable()))
+ {
+ assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required).");
+ if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance."))
+ {
+ if(dataSource.getQueryFilter() != null)
+ {
+ validateQueryFilter("In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter());
+ }
+ }
+ }
+ else if(dataSource.getStaticDataSupplier() != null)
+ {
+ validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class);
+ }
+ else
+ {
+ errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required).");
+ }
+ }
+ }
+
+ ////////////////////////////////////////
+ // validate dataSources in the report //
+ ////////////////////////////////////////
+ if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + "."))
+ {
+ int index = 0;
+ Set usedViewNames = new HashSet<>();
+ for(QReportView view : report.getViews())
+ {
+ assertCondition(StringUtils.hasContent(view.getName()), "Missing name for a view at index " + index + " in report " + reportName);
+ index++;
+
+ assertCondition(!usedViewNames.contains(view.getName()), "More than one view with name " + view.getName() + " in report " + reportName);
+ usedViewNames.add(view.getName());
+
+ String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " ";
+ assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type.");
+ if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName"))
+ {
+ assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + " has an unrecognized dataSourceName: " + view.getDataSourceName());
+ }
+
+ if(StringUtils.hasContent(view.getVarianceDataSourceName()))
+ {
+ assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + " has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName());
+ }
+
+ // actually, this is okay if there's a customizer, so...
+ assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns.");
+
+ // todo - all these too...
+ // view.getPivotFields();
+ // view.getViewCustomizer(); // validate code ref
+ // view.getRecordTransformStep(); // validate code ref
+ // view.getOrderByFields(); // make sure valid field names?
+ // view.getIncludePivotSubTotals(); // only for pivot type
+ // view.getTitleFormat(); view.getTitleFields(); // validate these match?
+ }
+ }
+ });
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void validateQueryFilter(String context, QTableMetaData table, QQueryFilter queryFilter)
+ {
+ for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
+ {
+ if(assertCondition(StringUtils.hasContent(criterion.getFieldName()), context + "Missing fieldName for a criteria"))
+ {
+ assertNoException(() -> table.getField(criterion.getFieldName()), context + "Criteria fieldName " + criterion.getFieldName() + " is not a field in this table.");
+ }
+ assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + criterion.getFieldName());
+ assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK)
+ }
+
+ for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
+ {
+ if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy"))
+ {
+ assertNoException(() -> table.getField(orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table.");
+ }
+ }
+
+ for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
+ {
+ validateQueryFilter(context, table, subFilter);
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -978,7 +1121,7 @@ public class QInstanceValidator
** But if it's false, add the provided message to the list of errors (and return false,
** e.g., in case you need to stop evaluating rules to avoid exceptions).
*******************************************************************************/
- private boolean assertCondition(boolean condition, String message)
+ public boolean assertCondition(boolean condition, String message)
{
if(!condition)
{
@@ -1035,4 +1178,15 @@ public class QInstanceValidator
LOG.info("Validation warning: " + message);
}
}
+
+
+
+ /*******************************************************************************
+ ** Getter for errors
+ **
+ *******************************************************************************/
+ public List getErrors()
+ {
+ return errors;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java
index 69cf5787..0ffbd612 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
+import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@@ -44,12 +45,14 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class RunBackendStepInput extends AbstractActionInput
{
- private ProcessState processState;
- private String processName;
- private String tableName;
- private String stepName;
- private QProcessCallback callback;
- private AsyncJobCallback asyncJobCallback;
+ private ProcessState processState;
+ private String processName;
+ private String tableName;
+ private String stepName;
+ private QProcessCallback callback;
+ private AsyncJobCallback asyncJobCallback;
+ private RunProcessInput.FrontendStepBehavior frontendStepBehavior;
+ private Instant basepullLastRunTime;
////////////////////////////////////////////////////////////////////////////
// note - new fields should generally be added in method: cloneFieldsInto //
@@ -416,6 +419,17 @@ public class RunBackendStepInput extends AbstractActionInput
+ /*******************************************************************************
+ ** Getter for a single field's value
+ **
+ *******************************************************************************/
+ public Instant getValueInstant(String fieldName)
+ {
+ return (ValueUtils.getValueAsInstant(getValue(fieldName)));
+ }
+
+
+
/*******************************************************************************
** Accessor for processState - protected, because we generally want to access
** its members through wrapper methods, we think
@@ -453,4 +467,72 @@ public class RunBackendStepInput extends AbstractActionInput
return (asyncJobCallback);
}
+
+
+ /*******************************************************************************
+ ** Getter for frontendStepBehavior
+ **
+ *******************************************************************************/
+ public RunProcessInput.FrontendStepBehavior getFrontendStepBehavior()
+ {
+ return frontendStepBehavior;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for frontendStepBehavior
+ **
+ *******************************************************************************/
+ public void setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
+ {
+ this.frontendStepBehavior = frontendStepBehavior;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendStepBehavior
+ **
+ *******************************************************************************/
+ public RunBackendStepInput withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
+ {
+ this.frontendStepBehavior = frontendStepBehavior;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for basepullLastRunTime
+ **
+ *******************************************************************************/
+ public Instant getBasepullLastRunTime()
+ {
+ return basepullLastRunTime;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for basepullLastRunTime
+ **
+ *******************************************************************************/
+ public void setBasepullLastRunTime(Instant basepullLastRunTime)
+ {
+ this.basepullLastRunTime = basepullLastRunTime;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for basepullLastRunTime
+ **
+ *******************************************************************************/
+ public RunBackendStepInput withBasepullLastRunTime(Instant basepullLastRunTime)
+ {
+ this.basepullLastRunTime = basepullLastRunTime;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeInput.java
new file mode 100644
index 00000000..6bb4df7d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeInput.java
@@ -0,0 +1,223 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ExecuteCodeInput extends AbstractActionInput
+{
+ private QCodeReference codeReference;
+ private Map input;
+ private Map context;
+ private QCodeExecutionLoggerInterface executionLogger;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput(QInstance qInstance)
+ {
+ super(qInstance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for codeReference
+ **
+ *******************************************************************************/
+ public QCodeReference getCodeReference()
+ {
+ return codeReference;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for codeReference
+ **
+ *******************************************************************************/
+ public void setCodeReference(QCodeReference codeReference)
+ {
+ this.codeReference = codeReference;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for codeReference
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput withCodeReference(QCodeReference codeReference)
+ {
+ this.codeReference = codeReference;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for input
+ **
+ *******************************************************************************/
+ public Map getInput()
+ {
+ return input;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for input
+ **
+ *******************************************************************************/
+ public void setInput(Map input)
+ {
+ this.input = input;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for input
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput withInput(Map input)
+ {
+ this.input = input;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for input
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput withInput(String key, Serializable value)
+ {
+ if(this.input == null)
+ {
+ input = new HashMap<>();
+ }
+ this.input.put(key, value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for context
+ **
+ *******************************************************************************/
+ public Map getContext()
+ {
+ return context;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for context
+ **
+ *******************************************************************************/
+ public void setContext(Map context)
+ {
+ this.context = context;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for context
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput withContext(Map context)
+ {
+ this.context = context;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for context
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput withContext(String key, Serializable value)
+ {
+ if(this.context == null)
+ {
+ context = new HashMap<>();
+ }
+ this.context.put(key, value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for executionLogger
+ **
+ *******************************************************************************/
+ public QCodeExecutionLoggerInterface getExecutionLogger()
+ {
+ return executionLogger;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for executionLogger
+ **
+ *******************************************************************************/
+ public void setExecutionLogger(QCodeExecutionLoggerInterface executionLogger)
+ {
+ this.executionLogger = executionLogger;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for executionLogger
+ **
+ *******************************************************************************/
+ public ExecuteCodeInput withExecutionLogger(QCodeExecutionLoggerInterface executionLogger)
+ {
+ this.executionLogger = executionLogger;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeOutput.java
new file mode 100644
index 00000000..bb66522f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeOutput.java
@@ -0,0 +1,69 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ExecuteCodeOutput
+{
+ private Serializable output;
+
+
+
+ /*******************************************************************************
+ ** Getter for output
+ **
+ *******************************************************************************/
+ public Serializable getOutput()
+ {
+ return output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for output
+ **
+ *******************************************************************************/
+ public void setOutput(Serializable output)
+ {
+ this.output = output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for output
+ **
+ *******************************************************************************/
+ public ExecuteCodeOutput withOutput(Serializable output)
+ {
+ this.output = output;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java
new file mode 100644
index 00000000..77acfe43
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java
@@ -0,0 +1,154 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunAssociatedScriptInput extends AbstractTableActionInput
+{
+ private AssociatedScriptCodeReference codeReference;
+ private Map inputValues;
+
+ private Serializable outputObject;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public RunAssociatedScriptInput(QInstance qInstance)
+ {
+ super(qInstance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for codeReference
+ **
+ *******************************************************************************/
+ public AssociatedScriptCodeReference getCodeReference()
+ {
+ return codeReference;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for codeReference
+ **
+ *******************************************************************************/
+ public void setCodeReference(AssociatedScriptCodeReference codeReference)
+ {
+ this.codeReference = codeReference;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for codeReference
+ **
+ *******************************************************************************/
+ public RunAssociatedScriptInput withCodeReference(AssociatedScriptCodeReference codeReference)
+ {
+ this.codeReference = codeReference;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for inputValues
+ **
+ *******************************************************************************/
+ public Map getInputValues()
+ {
+ return inputValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inputValues
+ **
+ *******************************************************************************/
+ public void setInputValues(Map inputValues)
+ {
+ this.inputValues = inputValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inputValues
+ **
+ *******************************************************************************/
+ public RunAssociatedScriptInput withInputValues(Map inputValues)
+ {
+ this.inputValues = inputValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for outputObject
+ **
+ *******************************************************************************/
+ public Serializable getOutputObject()
+ {
+ return outputObject;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for outputObject
+ **
+ *******************************************************************************/
+ public void setOutputObject(Serializable outputObject)
+ {
+ this.outputObject = outputObject;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for outputObject
+ **
+ *******************************************************************************/
+ public RunAssociatedScriptInput withOutputObject(Serializable outputObject)
+ {
+ this.outputObject = outputObject;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptOutput.java
new file mode 100644
index 00000000..1b80a54f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptOutput.java
@@ -0,0 +1,70 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunAssociatedScriptOutput extends AbstractActionOutput
+{
+ private Serializable output;
+
+
+
+ /*******************************************************************************
+ ** Getter for output
+ **
+ *******************************************************************************/
+ public Serializable getOutput()
+ {
+ return output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for output
+ **
+ *******************************************************************************/
+ public void setOutput(Serializable output)
+ {
+ this.output = output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for output
+ **
+ *******************************************************************************/
+ public RunAssociatedScriptOutput withOutput(Serializable output)
+ {
+ this.output = output;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java
new file mode 100644
index 00000000..4f3915b4
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java
@@ -0,0 +1,188 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class StoreAssociatedScriptInput extends AbstractTableActionInput
+{
+ private String fieldName;
+ private Serializable recordPrimaryKey;
+
+ private String code;
+ private String commitMessage;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public StoreAssociatedScriptInput(QInstance instance)
+ {
+ super(instance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ **
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ **
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ **
+ *******************************************************************************/
+ public StoreAssociatedScriptInput withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public Serializable getRecordPrimaryKey()
+ {
+ return recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public void setRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public StoreAssociatedScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for code
+ **
+ *******************************************************************************/
+ public String getCode()
+ {
+ return code;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for code
+ **
+ *******************************************************************************/
+ public void setCode(String code)
+ {
+ this.code = code;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for code
+ **
+ *******************************************************************************/
+ public StoreAssociatedScriptInput withCode(String code)
+ {
+ this.code = code;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for commitMessage
+ **
+ *******************************************************************************/
+ public String getCommitMessage()
+ {
+ return commitMessage;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for commitMessage
+ **
+ *******************************************************************************/
+ public void setCommitMessage(String commitMessage)
+ {
+ this.commitMessage = commitMessage;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for commitMessage
+ **
+ *******************************************************************************/
+ public StoreAssociatedScriptInput withCommitMessage(String commitMessage)
+ {
+ this.commitMessage = commitMessage;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptOutput.java
new file mode 100644
index 00000000..12549e0d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptOutput.java
@@ -0,0 +1,33 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class StoreAssociatedScriptOutput extends AbstractActionOutput
+{
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java
new file mode 100644
index 00000000..33871f38
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java
@@ -0,0 +1,187 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TestScriptInput extends AbstractTableActionInput
+{
+ private Serializable recordPrimaryKey;
+ private String code;
+ private Serializable scriptTypeId;
+ private Map inputValues;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public TestScriptInput(QInstance qInstance)
+ {
+ super(qInstance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public Serializable getRecordPrimaryKey()
+ {
+ return recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public void setRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public TestScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for inputValues
+ **
+ *******************************************************************************/
+ public Map getInputValues()
+ {
+ return inputValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inputValues
+ **
+ *******************************************************************************/
+ public void setInputValues(Map inputValues)
+ {
+ this.inputValues = inputValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inputValues
+ **
+ *******************************************************************************/
+ public TestScriptInput withInputValues(Map inputValues)
+ {
+ this.inputValues = inputValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for code
+ **
+ *******************************************************************************/
+ public String getCode()
+ {
+ return code;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for code
+ **
+ *******************************************************************************/
+ public void setCode(String code)
+ {
+ this.code = code;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for code
+ **
+ *******************************************************************************/
+ public TestScriptInput withCode(String code)
+ {
+ this.code = code;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptTypeId
+ **
+ *******************************************************************************/
+ public Serializable getScriptTypeId()
+ {
+ return scriptTypeId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptTypeId
+ **
+ *******************************************************************************/
+ public void setScriptTypeId(Serializable scriptTypeId)
+ {
+ this.scriptTypeId = scriptTypeId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptTypeId
+ **
+ *******************************************************************************/
+ public TestScriptInput withScriptTypeId(Serializable scriptTypeId)
+ {
+ this.scriptTypeId = scriptTypeId;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptOutput.java
new file mode 100644
index 00000000..d6da9222
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptOutput.java
@@ -0,0 +1,33 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TestScriptOutput extends AbstractActionOutput
+{
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
new file mode 100644
index 00000000..e08e3f22
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
@@ -0,0 +1,186 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.tables.get;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+
+
+/*******************************************************************************
+ ** Input data for the Get action
+ **
+ *******************************************************************************/
+public class GetInput extends AbstractTableActionInput
+{
+ private QBackendTransaction transaction;
+ private Serializable primaryKey;
+
+ private boolean shouldTranslatePossibleValues = false;
+ private boolean shouldGenerateDisplayValues = false;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetInput()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetInput(QInstance instance)
+ {
+ super(instance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetInput(QInstance instance, QSession session)
+ {
+ super(instance);
+ setSession(session);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for primaryKey
+ **
+ *******************************************************************************/
+ public Serializable getPrimaryKey()
+ {
+ return primaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for primaryKey
+ **
+ *******************************************************************************/
+ public void setPrimaryKey(Serializable primaryKey)
+ {
+ this.primaryKey = primaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for primaryKey
+ **
+ *******************************************************************************/
+ public GetInput withPrimaryKey(Serializable primaryKey)
+ {
+ this.primaryKey = primaryKey;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for shouldTranslatePossibleValues
+ **
+ *******************************************************************************/
+ public boolean getShouldTranslatePossibleValues()
+ {
+ return shouldTranslatePossibleValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for shouldTranslatePossibleValues
+ **
+ *******************************************************************************/
+ public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues)
+ {
+ this.shouldTranslatePossibleValues = shouldTranslatePossibleValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for shouldGenerateDisplayValues
+ **
+ *******************************************************************************/
+ public boolean getShouldGenerateDisplayValues()
+ {
+ return shouldGenerateDisplayValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for shouldGenerateDisplayValues
+ **
+ *******************************************************************************/
+ public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues)
+ {
+ this.shouldGenerateDisplayValues = shouldGenerateDisplayValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for transaction
+ **
+ *******************************************************************************/
+ public QBackendTransaction getTransaction()
+ {
+ return transaction;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for transaction
+ **
+ *******************************************************************************/
+ public void setTransaction(QBackendTransaction transaction)
+ {
+ this.transaction = transaction;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for transaction
+ **
+ *******************************************************************************/
+ public GetInput withTransaction(QBackendTransaction transaction)
+ {
+ this.transaction = transaction;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java
new file mode 100644
index 00000000..2f9cada6
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java
@@ -0,0 +1,72 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.tables.get;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+
+
+/*******************************************************************************
+ ** Output for a Get action
+ **
+ *******************************************************************************/
+public class GetOutput extends AbstractActionOutput implements Serializable
+{
+ private QRecord record;
+
+
+
+ /*******************************************************************************
+ ** Getter for record
+ **
+ *******************************************************************************/
+ public QRecord getRecord()
+ {
+ return record;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for record
+ **
+ *******************************************************************************/
+ public void setRecord(QRecord record)
+ {
+ this.record = record;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for record
+ **
+ *******************************************************************************/
+ public GetOutput withRecord(QRecord record)
+ {
+ this.record = record;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
index 51653a6b..f83e959d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
@@ -25,6 +25,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
/*******************************************************************************
@@ -33,6 +36,8 @@ import java.util.List;
*******************************************************************************/
public class QFilterCriteria implements Serializable, Cloneable
{
+ private static final Logger LOG = LogManager.getLogger(QFilterCriteria.class);
+
private String fieldName;
private QCriteriaOperator operator;
private List values;
@@ -183,4 +188,46 @@ public class QFilterCriteria implements Serializable, Cloneable
this.values = values;
return this;
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ StringBuilder rs = new StringBuilder(fieldName);
+ try
+ {
+ rs.append(" ").append(operator).append(" ");
+ if(CollectionUtils.nullSafeHasContents(values))
+ {
+ if(values.size() == 1)
+ {
+ rs.append(values.get(0));
+ }
+ else
+ {
+ int index = 0;
+ for(Serializable value : values)
+ {
+ if(index++ > 9)
+ {
+ rs.append("and ").append(values.size() - index).append(" more");
+ break;
+ }
+ rs.append(value).append(",");
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error in toString", e);
+ rs.append("Error generating toString...");
+ }
+
+ return (rs.toString());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java
index 4cfa6395..52eca98c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java
@@ -151,4 +151,14 @@ public class QFilterOrderBy implements Serializable, Cloneable
return (this);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return (fieldName + " " + (isAscending ? "ASC" : "DESC"));
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
index f39403de..6d1fc376 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
@@ -26,6 +26,8 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
/*******************************************************************************
@@ -34,6 +36,8 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*******************************************************************************/
public class QQueryFilter implements Serializable, Cloneable
{
+ private static final Logger LOG = LogManager.getLogger(QQueryFilter.class);
+
private List criteria = new ArrayList<>();
private List orderBys = new ArrayList<>();
@@ -301,4 +305,41 @@ public class QQueryFilter implements Serializable, Cloneable
subFilters.add(subFilter);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ StringBuilder rs = new StringBuilder("(");
+ try
+ {
+ for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
+ {
+ rs.append(criterion).append(" ").append(getBooleanOperator());
+ }
+
+ for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
+ {
+ rs.append(subFilter);
+ }
+ rs.append(")");
+
+ rs.append("OrderBy[");
+ for(QFilterOrderBy orderBy : orderBys)
+ {
+ rs.append(orderBy).append(",");
+ }
+ rs.append("]");
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error in toString", e);
+ rs.append("Error generating toString...");
+ }
+
+ return (rs.toString());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java
index 81c39178..5bde561c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.update;
+import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@@ -54,4 +55,18 @@ public class UpdateOutput extends AbstractActionOutput
{
this.records = records;
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void addRecord(QRecord record)
+ {
+ if(this.records == null)
+ {
+ this.records = new ArrayList<>();
+ }
+ this.records.add(record);
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
index 5e3772e1..8a5bb58e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
@@ -64,6 +64,8 @@ public class QRecord implements Serializable
private Map backendDetails = new LinkedHashMap<>();
private List errors = new ArrayList<>();
+ public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject";
+
/*******************************************************************************
@@ -455,6 +457,11 @@ public class QRecord implements Serializable
*******************************************************************************/
public Serializable getBackendDetail(String key)
{
+ if(!this.backendDetails.containsKey(key))
+ {
+ return (null);
+ }
+
return this.backendDetails.get(key);
}
@@ -466,7 +473,7 @@ public class QRecord implements Serializable
*******************************************************************************/
public String getBackendDetailString(String key)
{
- return (String) this.backendDetails.get(key);
+ return (String) getBackendDetail(key);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java
new file mode 100644
index 00000000..28b6198c
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java
@@ -0,0 +1,175 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.data;
+
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.utils.ListingHash;
+
+
+/*******************************************************************************
+ ** Base class for enums that are interoperable with QRecords.
+ *******************************************************************************/
+public interface QRecordEnum
+{
+ ListingHash, QRecordEntityField> fieldMapping = new ListingHash<>();
+
+
+ /*******************************************************************************
+ ** Convert this entity to a QRecord.
+ **
+ *******************************************************************************/
+ default QRecord toQRecord() throws QException
+ {
+ try
+ {
+ QRecord qRecord = new QRecord();
+
+ List fieldList = getFieldList(this.getClass());
+ for(QRecordEntityField qRecordEntityField : fieldList)
+ {
+ qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
+ }
+
+ return (qRecord);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error building qRecord from entity.", e));
+ }
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static List getFieldList(Class extends QRecordEnum> c)
+ {
+ if(!fieldMapping.containsKey(c))
+ {
+ List fieldList = new ArrayList<>();
+ for(Method possibleGetter : c.getMethods())
+ {
+ if(isGetter(possibleGetter))
+ {
+ String fieldName = getFieldNameFromGetter(possibleGetter);
+ Optional fieldAnnotation = getQFieldAnnotation(c, fieldName);
+ fieldList.add(new QRecordEntityField(fieldName, possibleGetter, null, possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
+ }
+ }
+ fieldMapping.put(c, fieldList);
+ }
+ return (fieldMapping.get(c));
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Optional getQFieldAnnotation(Class extends QRecordEnum> c, String fieldName)
+ {
+ try
+ {
+ Field field = c.getDeclaredField(fieldName);
+ return (Optional.ofNullable(field.getAnnotation(QField.class)));
+ }
+ catch(NoSuchFieldException e)
+ {
+ //////////////////////////////////////////
+ // ok, we just won't have an annotation //
+ //////////////////////////////////////////
+ }
+ return (Optional.empty());
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String getFieldNameFromGetter(Method getter)
+ {
+ String nameWithoutGet = getter.getName().replaceFirst("^get", "");
+ if(nameWithoutGet.length() == 1)
+ {
+ return (nameWithoutGet.toLowerCase(Locale.ROOT));
+ }
+ return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1));
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean isGetter(Method method)
+ {
+ if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
+ {
+ if(isSupportedFieldType(method.getReturnType()))
+ {
+ return (true);
+ }
+ else
+ {
+ if(!method.getName().equals("getClass"))
+ {
+ System.err.println("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
+ }
+ }
+ }
+ return (false);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean isSupportedFieldType(Class> returnType)
+ {
+ // todo - more types!!
+ return (returnType.equals(String.class)
+ || returnType.equals(Integer.class)
+ || returnType.equals(int.class)
+ || returnType.equals(Boolean.class)
+ || returnType.equals(boolean.class)
+ || returnType.equals(BigDecimal.class)
+ || returnType.equals(Instant.class)
+ || returnType.equals(LocalDate.class)
+ || returnType.equals(LocalTime.class));
+ /////////////////////////////////////////////
+ // note - this list has implications upon: //
+ // - QFieldType.fromClass //
+ // - QRecordEntityField.convertValueType //
+ /////////////////////////////////////////////
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java
index ea4cd840..6c5d6985 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java
@@ -22,8 +22,13 @@
package com.kingsrook.qqq.backend.core.model.metadata;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.serialization.QBackendMetaDataDeserializer;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@@ -38,6 +43,9 @@ public class QBackendMetaData
private String name;
private String backendType;
+ private Set enabledCapabilities = new HashSet<>();
+ private Set disabledCapabilities = new HashSet<>();
+
// todo - at some point, we may want to apply this to secret properties on subclasses?
// @JsonFilter("secretsFilter")
@@ -157,4 +165,172 @@ public class QBackendMetaData
// noop in base class //
////////////////////////
}
+
+
+
+ /*******************************************************************************
+ ** Getter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public Set getEnabledCapabilities()
+ {
+ return enabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public void setEnabledCapabilities(Set enabledCapabilities)
+ {
+ this.enabledCapabilities = enabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withEnabledCapabilities(Set enabledCapabilities)
+ {
+ this.enabledCapabilities = enabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withCapabilities(Set enabledCapabilities)
+ {
+ this.enabledCapabilities = enabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for a single enabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withCapability(Capability capability)
+ {
+ if(this.enabledCapabilities == null)
+ {
+ this.enabledCapabilities = new HashSet<>();
+ }
+ this.enabledCapabilities.add(capability);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withCapabilities(Capability... enabledCapabilities)
+ {
+ if(this.enabledCapabilities == null)
+ {
+ this.enabledCapabilities = new HashSet<>();
+ }
+ this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList());
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public Set getDisabledCapabilities()
+ {
+ return disabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public void setDisabledCapabilities(Set disabledCapabilities)
+ {
+ this.disabledCapabilities = disabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withDisabledCapabilities(Set disabledCapabilities)
+ {
+ this.disabledCapabilities = disabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withoutCapabilities(Capability... disabledCapabilities)
+ {
+ if(this.disabledCapabilities == null)
+ {
+ this.disabledCapabilities = new HashSet<>();
+ }
+ this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList());
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withoutCapabilities(Set disabledCapabilities)
+ {
+ this.disabledCapabilities = disabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for a single disabledCapabilities
+ **
+ *******************************************************************************/
+ public QBackendMetaData withoutCapability(Capability capability)
+ {
+ if(this.disabledCapabilities == null)
+ {
+ this.disabledCapabilities = new HashSet<>();
+ }
+ this.disabledCapabilities.add(capability);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void performValidation(QInstanceValidator qInstanceValidator)
+ {
+ ////////////////////////
+ // noop in base class //
+ ////////////////////////
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AssociatedScriptCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AssociatedScriptCodeReference.java
new file mode 100644
index 00000000..716b4063
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AssociatedScriptCodeReference.java
@@ -0,0 +1,139 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.code;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AssociatedScriptCodeReference extends QCodeReference
+{
+ private String recordTable;
+ private Serializable recordPrimaryKey;
+ private String fieldName;
+
+
+
+ /*******************************************************************************
+ ** Getter for recordTable
+ **
+ *******************************************************************************/
+ public String getRecordTable()
+ {
+ return recordTable;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for recordTable
+ **
+ *******************************************************************************/
+ public void setRecordTable(String recordTable)
+ {
+ this.recordTable = recordTable;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for recordTable
+ **
+ *******************************************************************************/
+ public AssociatedScriptCodeReference withRecordTable(String recordTable)
+ {
+ this.recordTable = recordTable;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public Serializable getRecordPrimaryKey()
+ {
+ return recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public void setRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for recordPrimaryKey
+ **
+ *******************************************************************************/
+ public AssociatedScriptCodeReference withRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ **
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ **
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ **
+ *******************************************************************************/
+ public AssociatedScriptCodeReference withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java
index 62f7f7d2..4e7998e5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java
@@ -38,6 +38,8 @@ public class QCodeReference implements Serializable
private QCodeType codeType;
private QCodeUsage codeUsage;
+ private String inlineCode;
+
/*******************************************************************************
@@ -212,4 +214,38 @@ public class QCodeReference implements Serializable
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for inlineCode
+ **
+ *******************************************************************************/
+ public String getInlineCode()
+ {
+ return inlineCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inlineCode
+ **
+ *******************************************************************************/
+ public void setInlineCode(String inlineCode)
+ {
+ this.inlineCode = inlineCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inlineCode
+ **
+ *******************************************************************************/
+ public QCodeReference withInlineCode(String inlineCode)
+ {
+ this.inlineCode = inlineCode;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java
index 6aa9099b..e3f98270 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java
@@ -28,5 +28,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.code;
*******************************************************************************/
public enum QCodeType
{
- JAVA
+ JAVA,
+ JAVA_SCRIPT
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java
index b3c481a1..45da4911 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java
@@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
@@ -38,6 +41,8 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
protected Integer gridColumns;
protected QCodeReference codeReference;
+ protected Map defaultValues = new LinkedHashMap<>();
+
/*******************************************************************************
@@ -242,4 +247,56 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for defaultValues
+ **
+ *******************************************************************************/
+ public Map getDefaultValues()
+ {
+ return defaultValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for defaultValues
+ **
+ *******************************************************************************/
+ public void setDefaultValues(Map defaultValues)
+ {
+ this.defaultValues = defaultValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for defaultValues
+ **
+ *******************************************************************************/
+ public QWidgetMetaData withDefaultValues(Map defaultValues)
+ {
+ this.defaultValues = defaultValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for a single defaultValue
+ **
+ *******************************************************************************/
+ public QWidgetMetaData withDefaultValue(String key, Serializable value)
+ {
+ if(this.defaultValues == null)
+ {
+ this.defaultValues = new LinkedHashMap<>();
+ }
+
+ this.defaultValues.put(key, value);
+
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java
index 62abd7db..ad5f3c53 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java
@@ -34,7 +34,11 @@ public enum AdornmentType
{
LINK,
CHIP,
- SIZE;
+ SIZE,
+ CODE_EDITOR;
+ //////////////////////////////////////////////////////////////////////////
+ // keep these values in sync with AdornmentType.ts in qqq-frontend-core //
+ //////////////////////////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
index c5c18d7b..b52ed802 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
@@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
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.utils.CollectionUtils;
@@ -53,6 +58,8 @@ public class QFrontendTableMetaData
private List widgets;
+ private Set capabilities;
+
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
//////////////////////////////////////////////////////////////////////////////////
@@ -62,7 +69,7 @@ public class QFrontendTableMetaData
/*******************************************************************************
**
*******************************************************************************/
- public QFrontendTableMetaData(QTableMetaData tableMetaData, boolean includeFields)
+ public QFrontendTableMetaData(QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields)
{
this.name = tableMetaData.getName();
this.label = tableMetaData.getLabel();
@@ -89,6 +96,62 @@ public class QFrontendTableMetaData
{
this.widgets = tableMetaData.getWidgets();
}
+
+ setCapabilities(backendForTable, tableMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void setCapabilities(QBackendMetaData backend, QTableMetaData table)
+ {
+ Set enabledCapabilities = new HashSet<>();
+ for(Capability capability : Capability.values())
+ {
+ ///////////////////////////////////////////////
+ // by default, every table can do everything //
+ ///////////////////////////////////////////////
+ boolean hasCapability = true;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the table's backend says the capability is disabled, then by default, then the capability is disabled... //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(backend.getDisabledCapabilities().contains(capability))
+ {
+ hasCapability = false;
+
+ /////////////////////////////////////////////////////////////////
+ // unless the table overrides that and says that it IS enabled //
+ /////////////////////////////////////////////////////////////////
+ if(table.getEnabledCapabilities().contains(capability))
+ {
+ hasCapability = true;
+ }
+ }
+ else
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // if the backend doesn't specify the capability, then disable it if the table says so //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ if(table.getDisabledCapabilities().contains(capability))
+ {
+ hasCapability = false;
+ }
+ }
+
+ if(hasCapability)
+ {
+ ///////////////////////////////////////
+ // todo - check if user is allowed!! //
+ ///////////////////////////////////////
+
+ enabledCapabilities.add(capability);
+ }
+ }
+
+ this.capabilities = enabledCapabilities.stream().map(Enum::name).collect(Collectors.toSet());
}
@@ -178,4 +241,16 @@ public class QFrontendTableMetaData
{
return widgets;
}
+
+
+
+ /*******************************************************************************
+ ** Getter for capabilities
+ **
+ *******************************************************************************/
+ public Set getCapabilities()
+ {
+ return capabilities;
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java
index 36f2a0a2..542d2a79 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java
@@ -24,6 +24,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
import java.util.ArrayList;
import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -340,4 +343,38 @@ public class QAppMetaData implements QAppChildMetaData
this.addSection(section);
return (this);
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QAppMetaData withSectionOfChildren(QAppSection section, QAppChildMetaData... children)
+ {
+ this.addSection(section);
+
+ for(QAppChildMetaData child : children)
+ {
+ withChild(child);
+ if(child instanceof QTableMetaData)
+ {
+ section.withTable(child.getName());
+ }
+ else if(child instanceof QProcessMetaData)
+ {
+ section.withProcess(child.getName());
+ }
+ else if(child instanceof QReportMetaData)
+ {
+ section.withReport(child.getName());
+ }
+ else
+ {
+ throw new IllegalArgumentException("Unrecognized child type: " + child.getName());
+ }
+ }
+
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java
index 461871b0..b1d0c5b5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.layout;
+import java.util.ArrayList;
import java.util.List;
@@ -166,6 +167,22 @@ public class QAppSection
+ /*******************************************************************************
+ ** Fluent setter for tables
+ **
+ *******************************************************************************/
+ public QAppSection withTable(String tableName)
+ {
+ if(this.tables == null)
+ {
+ this.tables = new ArrayList<>();
+ }
+ this.tables.add(tableName);
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for processes
**
@@ -200,6 +217,22 @@ public class QAppSection
+ /*******************************************************************************
+ ** Fluent setter for processes
+ **
+ *******************************************************************************/
+ public QAppSection withProcess(String processName)
+ {
+ if(this.processes == null)
+ {
+ this.processes = new ArrayList<>();
+ }
+ this.processes.add(processName);
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for reports
**
@@ -234,6 +267,22 @@ public class QAppSection
+ /*******************************************************************************
+ ** Fluent setter for reports
+ **
+ *******************************************************************************/
+ public QAppSection withReport(String reportName)
+ {
+ if(this.reports == null)
+ {
+ this.reports = new ArrayList<>();
+ }
+ this.reports.add(reportName);
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for icon
**
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java
index 92e374c6..cfc2aa40 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java
@@ -34,6 +34,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
public class QIcon
{
private String name;
+ private String path;
@@ -88,4 +89,38 @@ public class QIcon
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for path
+ **
+ *******************************************************************************/
+ public String getPath()
+ {
+ return path;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for path
+ **
+ *******************************************************************************/
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for path
+ **
+ *******************************************************************************/
+ public QIcon withPath(String path)
+ {
+ this.path = path;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java
index accce041..280c3c4e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java
@@ -289,6 +289,7 @@ public class QPossibleValueSource
*******************************************************************************/
public void setTableName(String tableName)
{
+ this.type = QPossibleValueSourceType.TABLE;
this.tableName = tableName;
}
@@ -300,7 +301,7 @@ public class QPossibleValueSource
*******************************************************************************/
public QPossibleValueSource withTableName(String tableName)
{
- this.tableName = tableName;
+ setTableName(tableName);
return (this);
}
@@ -446,6 +447,7 @@ public class QPossibleValueSource
public void setEnumValues(List> enumValues)
{
this.enumValues = enumValues;
+ setType(QPossibleValueSourceType.ENUM);
}
@@ -456,7 +458,7 @@ public class QPossibleValueSource
*******************************************************************************/
public QPossibleValueSource withEnumValues(List> enumValues)
{
- this.enumValues = enumValues;
+ setEnumValues(enumValues);
return this;
}
@@ -472,6 +474,7 @@ public class QPossibleValueSource
this.enumValues = new ArrayList<>();
}
this.enumValues.add(possibleValue);
+ setType(QPossibleValueSourceType.ENUM);
}
@@ -512,6 +515,7 @@ public class QPossibleValueSource
public void setCustomCodeReference(QCodeReference customCodeReference)
{
this.customCodeReference = customCodeReference;
+ setType(QPossibleValueSourceType.CUSTOM);
}
@@ -522,7 +526,7 @@ public class QPossibleValueSource
*******************************************************************************/
public QPossibleValueSource withCustomCodeReference(QCodeReference customCodeReference)
{
- this.customCodeReference = customCodeReference;
+ setCustomCodeReference(customCodeReference);
return (this);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java
new file mode 100644
index 00000000..cc99b14d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java
@@ -0,0 +1,68 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.processes;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AbstractProcessMetaDataBuilder
+{
+ protected QProcessMetaData processMetaData;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public AbstractProcessMetaDataBuilder(QProcessMetaData processMetaData)
+ {
+ this.processMetaData = processMetaData;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processMetaData
+ **
+ *******************************************************************************/
+ public QProcessMetaData getProcessMetaData()
+ {
+ return processMetaData;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected void setInputFieldDefaultValue(String fieldName, Serializable value)
+ {
+ processMetaData.getInputFields().stream()
+ .filter(f -> f.getName().equals(fieldName)).findFirst()
+ .ifPresent(f -> f.setDefaultValue(value));
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
index 83435da8..cc54e526 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
@@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
+import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
/*******************************************************************************
@@ -41,10 +42,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
*******************************************************************************/
public class QProcessMetaData implements QAppChildMetaData
{
- private String name;
- private String label;
- private String tableName;
- private boolean isHidden = false;
+ private String name;
+ private String label;
+ private String tableName;
+ private boolean isHidden = false;
+ private BasepullConfiguration basepullConfiguration;
private List stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map steps; // this is the full map of possible steps
@@ -473,4 +475,38 @@ public class QProcessMetaData implements QAppChildMetaData
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for basepullConfiguration
+ **
+ *******************************************************************************/
+ public BasepullConfiguration getBasepullConfiguration()
+ {
+ return basepullConfiguration;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for basepullConfiguration
+ **
+ *******************************************************************************/
+ public void setBasepullConfiguration(BasepullConfiguration basepullConfiguration)
+ {
+ this.basepullConfiguration = basepullConfiguration;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for basepullConfiguration
+ **
+ *******************************************************************************/
+ public QProcessMetaData withBasepullConfiguration(BasepullConfiguration basepullConfiguration)
+ {
+ this.basepullConfiguration = basepullConfiguration;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/AssociatedScript.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/AssociatedScript.java
new file mode 100644
index 00000000..1645e470
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/AssociatedScript.java
@@ -0,0 +1,104 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.tables;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AssociatedScript implements Serializable
+{
+ private String fieldName;
+ private Serializable scriptTypeId;
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ **
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ **
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ **
+ *******************************************************************************/
+ public AssociatedScript withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptTypeId
+ **
+ *******************************************************************************/
+ public Serializable getScriptTypeId()
+ {
+ return scriptTypeId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptTypeId
+ **
+ *******************************************************************************/
+ public void setScriptTypeId(Serializable scriptTypeId)
+ {
+ this.scriptTypeId = scriptTypeId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptTypeId
+ **
+ *******************************************************************************/
+ public AssociatedScript withScriptTypeId(Serializable scriptTypeId)
+ {
+ this.scriptTypeId = scriptTypeId;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Capability.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Capability.java
new file mode 100644
index 00000000..e8321397
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Capability.java
@@ -0,0 +1,41 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.tables;
+
+
+/*******************************************************************************
+ ** Things that can be done to tables, fields.
+ **
+ *******************************************************************************/
+public enum Capability
+{
+ TABLE_QUERY,
+ TABLE_GET,
+ TABLE_COUNT,
+ TABLE_INSERT,
+ TABLE_UPDATE,
+ TABLE_DELETE
+ //////////////////////////////////////////////////////////////////////////
+ // keep these values in sync with AdornmentType.ts in qqq-frontend-core //
+ //////////////////////////////////////////////////////////////////////////
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java
index c1369e65..f62140f4 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java
@@ -38,6 +38,8 @@ public class QFieldSection
private List fieldNames;
private QIcon icon;
+ private boolean isHidden = false;
+
/*******************************************************************************
@@ -244,4 +246,38 @@ public class QFieldSection
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for isHidden
+ **
+ *******************************************************************************/
+ public boolean getIsHidden()
+ {
+ return (isHidden);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for isHidden
+ **
+ *******************************************************************************/
+ public void setIsHidden(boolean isHidden)
+ {
+ this.isHidden = isHidden;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent Setter for isHidden
+ **
+ *******************************************************************************/
+ public QFieldSection withIsHidden(boolean isHidden)
+ {
+ this.isHidden = isHidden;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
index 886fc5bc..c7f20b48 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
@@ -26,10 +26,12 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
@@ -78,7 +80,11 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
private List sections;
- private List widgets;
+ private List widgets;
+ private List associatedScripts;
+
+ private Set enabledCapabilities = new HashSet<>();
+ private Set disabledCapabilities = new HashSet<>();
@@ -754,6 +760,56 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
+ /*******************************************************************************
+ ** Getter for associatedScripts
+ **
+ *******************************************************************************/
+ public List getAssociatedScripts()
+ {
+ return associatedScripts;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for associatedScripts
+ **
+ *******************************************************************************/
+ public void setAssociatedScripts(List associatedScripts)
+ {
+ this.associatedScripts = associatedScripts;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for associatedScripts
+ **
+ *******************************************************************************/
+ public QTableMetaData withAssociatedScripts(List associatedScripts)
+ {
+ this.associatedScripts = associatedScripts;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for associatedScripts
+ **
+ *******************************************************************************/
+ public QTableMetaData withAssociatedScript(AssociatedScript associatedScript)
+ {
+ if(this.associatedScripts == null)
+ {
+ this.associatedScripts = new ArrayList();
+ }
+ this.associatedScripts.add(associatedScript);
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for uniqueKeys
**
@@ -802,4 +858,181 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
return (this);
}
+
+
+ /*******************************************************************************
+ ** Fluently add a section and fields in that section.
+ *******************************************************************************/
+ public QTableMetaData withSectionOfFields(QFieldSection fieldSection, QFieldMetaData... fields)
+ {
+ withSection(fieldSection);
+
+ List fieldNames = new ArrayList<>();
+ for(QFieldMetaData field : fields)
+ {
+ withField(field);
+ fieldNames.add(field.getName());
+ }
+
+ fieldSection.setFieldNames(fieldNames);
+
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public Set getEnabledCapabilities()
+ {
+ return enabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public void setEnabledCapabilities(Set enabledCapabilities)
+ {
+ this.enabledCapabilities = enabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withEnabledCapabilities(Set enabledCapabilities)
+ {
+ this.enabledCapabilities = enabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withCapabilities(Set enabledCapabilities)
+ {
+ this.enabledCapabilities = enabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for a single enabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withCapability(Capability capability)
+ {
+ if(this.enabledCapabilities == null)
+ {
+ this.enabledCapabilities = new HashSet<>();
+ }
+ this.enabledCapabilities.add(capability);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for enabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withCapabilities(Capability... enabledCapabilities)
+ {
+ if(this.enabledCapabilities == null)
+ {
+ this.enabledCapabilities = new HashSet<>();
+ }
+ this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList());
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public Set getDisabledCapabilities()
+ {
+ return disabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public void setDisabledCapabilities(Set disabledCapabilities)
+ {
+ this.disabledCapabilities = disabledCapabilities;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withDisabledCapabilities(Set disabledCapabilities)
+ {
+ this.disabledCapabilities = disabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withoutCapabilities(Capability... disabledCapabilities)
+ {
+ if(this.disabledCapabilities == null)
+ {
+ this.disabledCapabilities = new HashSet<>();
+ }
+ this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList());
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for disabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withoutCapabilities(Set disabledCapabilities)
+ {
+ this.disabledCapabilities = disabledCapabilities;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Alternative fluent setter for a single disabledCapabilities
+ **
+ *******************************************************************************/
+ public QTableMetaData withoutCapability(Capability capability)
+ {
+ if(this.disabledCapabilities == null)
+ {
+ this.disabledCapabilities = new HashSet<>();
+ }
+ this.disabledCapabilities.add(capability);
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/Script.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/Script.java
new file mode 100644
index 00000000..42fb6fe9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/Script.java
@@ -0,0 +1,282 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.scripts;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+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;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class Script extends QRecordEntity
+{
+ public static final String TABLE_NAME = "script";
+
+ @QField()
+ private Integer id;
+
+ @QField()
+ private Instant createDate;
+
+ @QField()
+ private Instant modifyDate;
+
+ @QField()
+ private String name;
+
+ @QField(possibleValueSourceName = "scriptType")
+ private Integer scriptTypeId;
+
+ @QField(possibleValueSourceName = "scriptRevision")
+ private Integer currentScriptRevisionId;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Script()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Script(QRecord qRecord) throws QException
+ {
+ populateFromQRecord(qRecord);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ **
+ *******************************************************************************/
+ public Script withId(Integer id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for createDate
+ **
+ *******************************************************************************/
+ public Script withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for modifyDate
+ **
+ *******************************************************************************/
+ public Script withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for name
+ **
+ *******************************************************************************/
+ public String getName()
+ {
+ return name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for name
+ **
+ *******************************************************************************/
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ **
+ *******************************************************************************/
+ public Script withName(String name)
+ {
+ this.name = name;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptTypeId
+ **
+ *******************************************************************************/
+ public Integer getScriptTypeId()
+ {
+ return scriptTypeId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptTypeId
+ **
+ *******************************************************************************/
+ public void setScriptTypeId(Integer scriptTypeId)
+ {
+ this.scriptTypeId = scriptTypeId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptTypeId
+ **
+ *******************************************************************************/
+ public Script withScriptTypeId(Integer scriptTypeId)
+ {
+ this.scriptTypeId = scriptTypeId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for currentScriptRevisionId
+ **
+ *******************************************************************************/
+ public Integer getCurrentScriptRevisionId()
+ {
+ return currentScriptRevisionId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for currentScriptRevisionId
+ **
+ *******************************************************************************/
+ public void setCurrentScriptRevisionId(Integer currentScriptRevisionId)
+ {
+ this.currentScriptRevisionId = currentScriptRevisionId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for currentScriptRevisionId
+ **
+ *******************************************************************************/
+ public Script withCurrentScriptRevisionId(Integer currentScriptRevisionId)
+ {
+ this.currentScriptRevisionId = currentScriptRevisionId;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLog.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLog.java
new file mode 100644
index 00000000..5949f326
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLog.java
@@ -0,0 +1,482 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.scripts;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ScriptLog extends QRecordEntity
+{
+ public static final String TABLE_NAME = "scriptLog";
+
+ @QField()
+ private Integer id;
+
+ @QField()
+ private Instant createDate;
+
+ @QField()
+ private Instant modifyDate;
+
+ @QField(possibleValueSourceName = "script")
+ private Integer scriptId;
+
+ @QField(possibleValueSourceName = "scriptRevision")
+ private Integer scriptRevisionId;
+
+ @QField()
+ private Instant startTimestamp;
+
+ @QField()
+ private Instant endTimestamp;
+
+ @QField(displayFormat = DisplayFormat.COMMAS)
+ private Integer runTimeMillis;
+
+ @QField()
+ private Boolean hadError;
+
+ @QField()
+ private String input;
+
+ @QField()
+ private String output;
+
+ @QField()
+ private String error;
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ **
+ *******************************************************************************/
+ public ScriptLog withId(Integer id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for createDate
+ **
+ *******************************************************************************/
+ public ScriptLog withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for modifyDate
+ **
+ *******************************************************************************/
+ public ScriptLog withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptId
+ **
+ *******************************************************************************/
+ public Integer getScriptId()
+ {
+ return scriptId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptId
+ **
+ *******************************************************************************/
+ public void setScriptId(Integer scriptId)
+ {
+ this.scriptId = scriptId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptId
+ **
+ *******************************************************************************/
+ public ScriptLog withScriptId(Integer scriptId)
+ {
+ this.scriptId = scriptId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptRevisionId
+ **
+ *******************************************************************************/
+ public Integer getScriptRevisionId()
+ {
+ return scriptRevisionId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptRevisionId
+ **
+ *******************************************************************************/
+ public void setScriptRevisionId(Integer scriptRevisionId)
+ {
+ this.scriptRevisionId = scriptRevisionId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptRevisionId
+ **
+ *******************************************************************************/
+ public ScriptLog withScriptRevisionId(Integer scriptRevisionId)
+ {
+ this.scriptRevisionId = scriptRevisionId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for startTimestamp
+ **
+ *******************************************************************************/
+ public Instant getStartTimestamp()
+ {
+ return startTimestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for startTimestamp
+ **
+ *******************************************************************************/
+ public void setStartTimestamp(Instant startTimestamp)
+ {
+ this.startTimestamp = startTimestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for startTimestamp
+ **
+ *******************************************************************************/
+ public ScriptLog withStartTimestamp(Instant startTimestamp)
+ {
+ this.startTimestamp = startTimestamp;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for endTimestamp
+ **
+ *******************************************************************************/
+ public Instant getEndTimestamp()
+ {
+ return endTimestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for endTimestamp
+ **
+ *******************************************************************************/
+ public void setEndTimestamp(Instant endTimestamp)
+ {
+ this.endTimestamp = endTimestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for endTimestamp
+ **
+ *******************************************************************************/
+ public ScriptLog withEndTimestamp(Instant endTimestamp)
+ {
+ this.endTimestamp = endTimestamp;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for runTimeMillis
+ **
+ *******************************************************************************/
+ public Integer getRunTimeMillis()
+ {
+ return runTimeMillis;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for runTimeMillis
+ **
+ *******************************************************************************/
+ public void setRunTimeMillis(Integer runTimeMillis)
+ {
+ this.runTimeMillis = runTimeMillis;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for runTimeMillis
+ **
+ *******************************************************************************/
+ public ScriptLog withRunTimeMillis(Integer runTimeMillis)
+ {
+ this.runTimeMillis = runTimeMillis;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for hadError
+ **
+ *******************************************************************************/
+ public Boolean getHadError()
+ {
+ return hadError;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for hadError
+ **
+ *******************************************************************************/
+ public void setHadError(Boolean hadError)
+ {
+ this.hadError = hadError;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for hadError
+ **
+ *******************************************************************************/
+ public ScriptLog withHadError(Boolean hadError)
+ {
+ this.hadError = hadError;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for input
+ **
+ *******************************************************************************/
+ public String getInput()
+ {
+ return input;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for input
+ **
+ *******************************************************************************/
+ public void setInput(String input)
+ {
+ this.input = input;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for input
+ **
+ *******************************************************************************/
+ public ScriptLog withInput(String input)
+ {
+ this.input = input;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for output
+ **
+ *******************************************************************************/
+ public String getOutput()
+ {
+ return output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for output
+ **
+ *******************************************************************************/
+ public void setOutput(String output)
+ {
+ this.output = output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for output
+ **
+ *******************************************************************************/
+ public ScriptLog withOutput(String output)
+ {
+ this.output = output;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for error
+ **
+ *******************************************************************************/
+ public String getError()
+ {
+ return error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for error
+ **
+ *******************************************************************************/
+ public void setError(String error)
+ {
+ this.error = error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for error
+ **
+ *******************************************************************************/
+ public ScriptLog withError(String error)
+ {
+ this.error = error;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLogLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLogLine.java
new file mode 100644
index 00000000..f878e91b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLogLine.java
@@ -0,0 +1,259 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.scripts;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ScriptLogLine extends QRecordEntity
+{
+ public static final String TABLE_NAME = "scriptLogLine";
+
+ @QField()
+ private Integer id;
+
+ @QField()
+ private Instant createDate;
+
+ @QField()
+ private Instant modifyDate;
+
+ @QField(possibleValueSourceName = "scriptLog")
+ private Integer scriptLogId;
+
+ @QField()
+ private Instant timestamp;
+
+ @QField()
+ private String text;
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ **
+ *******************************************************************************/
+ public ScriptLogLine withId(Integer id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for createDate
+ **
+ *******************************************************************************/
+ public ScriptLogLine withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for modifyDate
+ **
+ *******************************************************************************/
+ public ScriptLogLine withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptLogId
+ **
+ *******************************************************************************/
+ public Integer getScriptLogId()
+ {
+ return scriptLogId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptLogId
+ **
+ *******************************************************************************/
+ public void setScriptLogId(Integer scriptLogId)
+ {
+ this.scriptLogId = scriptLogId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptLogId
+ **
+ *******************************************************************************/
+ public ScriptLogLine withScriptLogId(Integer scriptLogId)
+ {
+ this.scriptLogId = scriptLogId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for timestamp
+ **
+ *******************************************************************************/
+ public Instant getTimestamp()
+ {
+ return timestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for timestamp
+ **
+ *******************************************************************************/
+ public void setTimestamp(Instant timestamp)
+ {
+ this.timestamp = timestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for timestamp
+ **
+ *******************************************************************************/
+ public ScriptLogLine withTimestamp(Instant timestamp)
+ {
+ this.timestamp = timestamp;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for text
+ **
+ *******************************************************************************/
+ public String getText()
+ {
+ return text;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for text
+ **
+ *******************************************************************************/
+ public void setText(String text)
+ {
+ this.text = text;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for text
+ **
+ *******************************************************************************/
+ public ScriptLogLine withText(String text)
+ {
+ this.text = text;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java
new file mode 100644
index 00000000..ff8f0d26
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java
@@ -0,0 +1,356 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.scripts;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+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;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ScriptRevision extends QRecordEntity
+{
+ public static final String TABLE_NAME = "scriptRevision";
+
+ @QField()
+ private Integer id;
+
+ @QField()
+ private Instant createDate;
+
+ @QField()
+ private Instant modifyDate;
+
+ @QField(possibleValueSourceName = "script")
+ private Integer scriptId;
+
+ @QField()
+ private String contents;
+
+ @QField()
+ private Integer sequenceNo;
+
+ @QField()
+ private String commitMessage;
+
+ @QField()
+ private String author;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ScriptRevision()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ScriptRevision(QRecord qRecord) throws QException
+ {
+ populateFromQRecord(qRecord);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ **
+ *******************************************************************************/
+ public ScriptRevision withId(Integer id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for createDate
+ **
+ *******************************************************************************/
+ public ScriptRevision withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for modifyDate
+ **
+ *******************************************************************************/
+ public ScriptRevision withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptId
+ **
+ *******************************************************************************/
+ public Integer getScriptId()
+ {
+ return scriptId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptId
+ **
+ *******************************************************************************/
+ public void setScriptId(Integer scriptId)
+ {
+ this.scriptId = scriptId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptId
+ **
+ *******************************************************************************/
+ public ScriptRevision withScriptId(Integer scriptId)
+ {
+ this.scriptId = scriptId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for contents
+ **
+ *******************************************************************************/
+ public String getContents()
+ {
+ return contents;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for contents
+ **
+ *******************************************************************************/
+ public void setContents(String contents)
+ {
+ this.contents = contents;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for contents
+ **
+ *******************************************************************************/
+ public ScriptRevision withContents(String contents)
+ {
+ this.contents = contents;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sequenceNo
+ **
+ *******************************************************************************/
+ public Integer getSequenceNo()
+ {
+ return sequenceNo;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sequenceNo
+ **
+ *******************************************************************************/
+ public void setSequenceNo(Integer sequenceNo)
+ {
+ this.sequenceNo = sequenceNo;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sequenceNo
+ **
+ *******************************************************************************/
+ public ScriptRevision withSequenceNo(Integer sequenceNo)
+ {
+ this.sequenceNo = sequenceNo;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for commitMessage
+ **
+ *******************************************************************************/
+ public String getCommitMessage()
+ {
+ return commitMessage;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for commitMessage
+ **
+ *******************************************************************************/
+ public void setCommitMessage(String commitMessage)
+ {
+ this.commitMessage = commitMessage;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for commitMessage
+ **
+ *******************************************************************************/
+ public ScriptRevision withCommitMessage(String commitMessage)
+ {
+ this.commitMessage = commitMessage;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for author
+ **
+ *******************************************************************************/
+ public String getAuthor()
+ {
+ return author;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for author
+ **
+ *******************************************************************************/
+ public void setAuthor(String author)
+ {
+ this.author = author;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for author
+ **
+ *******************************************************************************/
+ public ScriptRevision withAuthor(String author)
+ {
+ this.author = author;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptType.java
new file mode 100644
index 00000000..6b50d26e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptType.java
@@ -0,0 +1,259 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.scripts;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ScriptType extends QRecordEntity
+{
+ public static final String TABLE_NAME = "scriptType";
+
+ @QField()
+ private Integer id;
+
+ @QField()
+ private Instant createDate;
+
+ @QField()
+ private Instant modifyDate;
+
+ @QField()
+ private String name;
+
+ @QField()
+ private String helpText;
+
+ @QField()
+ private String sampleCode;
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ **
+ *******************************************************************************/
+ public ScriptType withId(Integer id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for createDate
+ **
+ *******************************************************************************/
+ public ScriptType withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for modifyDate
+ **
+ *******************************************************************************/
+ public ScriptType withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for name
+ **
+ *******************************************************************************/
+ public String getName()
+ {
+ return name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for name
+ **
+ *******************************************************************************/
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ **
+ *******************************************************************************/
+ public ScriptType withName(String name)
+ {
+ this.name = name;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for helpText
+ **
+ *******************************************************************************/
+ public String getHelpText()
+ {
+ return helpText;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for helpText
+ **
+ *******************************************************************************/
+ public void setHelpText(String helpText)
+ {
+ this.helpText = helpText;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for helpText
+ **
+ *******************************************************************************/
+ public ScriptType withHelpText(String helpText)
+ {
+ this.helpText = helpText;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sampleCode
+ **
+ *******************************************************************************/
+ public String getSampleCode()
+ {
+ return sampleCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sampleCode
+ **
+ *******************************************************************************/
+ public void setSampleCode(String sampleCode)
+ {
+ this.sampleCode = sampleCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sampleCode
+ **
+ *******************************************************************************/
+ public ScriptType withSampleCode(String sampleCode)
+ {
+ this.sampleCode = sampleCode;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java
new file mode 100644
index 00000000..39c2249d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java
@@ -0,0 +1,217 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.scripts;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
+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.Capability;
+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;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ScriptsMetaDataProvider
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ defineStandardScriptsTables(instance, backendName, backendDetailEnricher);
+ defineStandardScriptsPossibleValueSources(instance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineStandardScriptsTables(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ for(QTableMetaData tableMetaData : defineStandardScriptsTables(backendName, backendDetailEnricher))
+ {
+ instance.addTable(tableMetaData);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineStandardScriptsPossibleValueSources(QInstance instance) throws QException
+ {
+ instance.addPossibleValueSource(new QPossibleValueSource()
+ .withName(Script.TABLE_NAME)
+ .withTableName(Script.TABLE_NAME)
+ );
+
+ instance.addPossibleValueSource(new QPossibleValueSource()
+ .withName(ScriptRevision.TABLE_NAME)
+ .withTableName(ScriptRevision.TABLE_NAME)
+ );
+
+ instance.addPossibleValueSource(new QPossibleValueSource()
+ .withName(ScriptType.TABLE_NAME)
+ .withTableName(ScriptType.TABLE_NAME)
+ );
+
+ instance.addPossibleValueSource(new QPossibleValueSource()
+ .withName(ScriptLog.TABLE_NAME)
+ .withTableName(ScriptLog.TABLE_NAME)
+ );
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List defineStandardScriptsTables(String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ List rs = new ArrayList<>();
+ rs.add(enrich(backendDetailEnricher, defineScriptTypeTable(backendName)));
+ rs.add(enrich(backendDetailEnricher, defineScriptTable(backendName)));
+ rs.add(enrich(backendDetailEnricher, defineScriptRevisionTable(backendName)));
+ rs.add(enrich(backendDetailEnricher, defineScriptLogTable(backendName)));
+ rs.add(enrich(backendDetailEnricher, defineScriptLogLineTable(backendName)));
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData enrich(Consumer backendDetailEnricher, QTableMetaData table)
+ {
+ if(backendDetailEnricher != null)
+ {
+ backendDetailEnricher.accept(table);
+ }
+ return (table);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineStandardTable(String backendName, String name, Class extends QRecordEntity> fieldsFromEntity) throws QException
+ {
+ return new QTableMetaData()
+ .withName(name)
+ .withBackendName(backendName)
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields("name")
+ .withPrimaryKeyField("id")
+ .withFieldsFromEntity(fieldsFromEntity);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineScriptTable(String backendName) throws QException
+ {
+ return (defineStandardTable(backendName, Script.TABLE_NAME, Script.class)
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name", "scriptTypeId", "currentScriptRevisionId")))
+ .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineScriptTypeTable(String backendName) throws QException
+ {
+ QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptType.TABLE_NAME, ScriptType.class)
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name")))
+ .withSection(new QFieldSection("details", new QIcon().withName("dataset"), Tier.T2, List.of("helpText", "sampleCode")))
+ .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
+ tableMetaData.getField("sampleCode").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR));
+ return (tableMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineScriptRevisionTable(String backendName) throws QException
+ {
+ QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptRevision.TABLE_NAME, ScriptRevision.class)
+ .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE)
+ .withRecordLabelFormat("%s v%s")
+ .withRecordLabelFields(List.of("scriptId", "sequenceNo"))
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scriptId", "sequenceNo")))
+ .withSection(new QFieldSection("code", new QIcon().withName("data_object"), Tier.T2, List.of("contents")))
+ .withSection(new QFieldSection("changeManagement", new QIcon().withName("history"), Tier.T2, List.of("commitMessage", "author")))
+ .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
+
+ tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR));
+ return (tableMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineScriptLogTable(String backendName) throws QException
+ {
+ return (defineStandardTable(backendName, ScriptLog.TABLE_NAME, ScriptLog.class)
+ .withRecordLabelFields(List.of("id"))
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id")))
+ .withSection(new QFieldSection("script", new QIcon().withName("data_object"), Tier.T2, List.of("scriptId", "scriptRevisionId")))
+ .withSection(new QFieldSection("timing", new QIcon().withName("schedule"), Tier.T2, List.of("startTimestamp", "endTimestamp", "runTimeMillis", "createDate", "modifyDate")))
+ .withSection(new QFieldSection("error", "Error", new QIcon().withName("error_outline"), Tier.T2, List.of("hadError", "error")))
+ .withSection(new QFieldSection("inputOutput", "Input/Output", new QIcon().withName("chat"), Tier.T2, List.of("input", "output"))));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineScriptLogLineTable(String backendName) throws QException
+ {
+ return (defineStandardTable(backendName, ScriptLogLine.TABLE_NAME, ScriptLogLine.class)
+ .withRecordLabelFields(List.of("id")));
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java
index 6b53dd6b..aaa9bc36 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java
@@ -22,12 +22,16 @@
package com.kingsrook.qqq.backend.core.modules.authentication;
+import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
+import com.auth0.client.auth.AuthAPI;
+import com.auth0.exception.Auth0Exception;
+import com.auth0.json.auth.TokenHolder;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
@@ -65,6 +69,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800;
public static final String AUTH0_ID_TOKEN_KEY = "sessionId";
+ public static final String BASIC_AUTH_KEY = "basicAuthString";
public static final String TOKEN_NOT_PROVIDED_ERROR = "Id Token was not provided";
public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token";
@@ -82,6 +87,43 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
@Override
public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException
{
+ ///////////////////////////////////////////////////////////
+ // check if we are processing a Basic Auth Session first //
+ ///////////////////////////////////////////////////////////
+ if(context.containsKey(BASIC_AUTH_KEY))
+ {
+ Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
+ AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret());
+ try
+ {
+ /////////////////////////////////////////////////
+ // decode the credentials from the header auth //
+ /////////////////////////////////////////////////
+ String base64Credentials = context.get(BASIC_AUTH_KEY).trim();
+ byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
+ String credentials = new String(credDecoded, StandardCharsets.UTF_8);
+
+ /////////////////////////////////////
+ // call auth0 with a login request //
+ /////////////////////////////////////
+ TokenHolder result = auth.login(credentials.split(":")[0], credentials.split(":")[1].toCharArray())
+ .setScope("openid email nickname")
+ .execute();
+
+ context.put(AUTH0_ID_TOKEN_KEY, result.getIdToken());
+ }
+ catch(Auth0Exception e)
+ {
+ ////////////////
+ // ¯\_(ツ)_/¯ //
+ ////////////////
+ String message = "An unknown error occurred during handling basic auth";
+ LOG.error(message, e);
+ throw (new QAuthenticationException(message));
+ }
+
+ }
+
//////////////////////////////////////////////////
// get the jwt id token from the context object //
//////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java
index 2b01799d..e99d4004 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java
@@ -31,6 +31,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
{
private String baseUrl;
+ private String clientId;
+ private String clientSecret;
@@ -76,4 +78,69 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
this.baseUrl = baseUrl;
}
+
+
+ /*******************************************************************************
+ ** Fluent setter, override to help fluent flows
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withClientId(String clientId)
+ {
+ setClientId(clientId);
+ return this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for clientId
+ **
+ *******************************************************************************/
+ public String getClientId()
+ {
+ return clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientId
+ **
+ *******************************************************************************/
+ public void setClientId(String clientId)
+ {
+ this.clientId = clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter, override to help fluent flows
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withClientSecret(String clientSecret)
+ {
+ setClientSecret(clientSecret);
+ return this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for clientSecret
+ **
+ *******************************************************************************/
+ public String getClientSecret()
+ {
+ return clientSecret;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientSecret
+ **
+ *******************************************************************************/
+ public void setClientSecret(String clientSecret)
+ {
+ this.clientSecret = clientSecret;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
index ca95e65e..619dd5d4 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
@@ -73,6 +73,7 @@ public class QBackendModuleDispatcher
// e.g., backend-core shouldn't need to "know" about the modules.
"com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule",
"com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule",
+ "com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration.EnumerationBackendModule",
"com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule",
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
index 13e6347a..70d51742 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.backend;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
@@ -48,7 +49,10 @@ public interface QBackendModuleInterface
/*******************************************************************************
** Method to identify the class used for backend meta data for this module.
*******************************************************************************/
- Class extends QBackendMetaData> getBackendMetaDataClass();
+ default Class extends QBackendMetaData> getBackendMetaDataClass()
+ {
+ return (QBackendMetaData.class);
+ }
/*******************************************************************************
** Method to identify the class used for table-backend details for this module.
@@ -76,6 +80,15 @@ public interface QBackendModuleInterface
return null;
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default GetInterface getGetInterface()
+ {
+ throwNotImplemented("Get");
+ return null;
+ }
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java
new file mode 100644
index 00000000..67650182
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java
@@ -0,0 +1,71 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+
+
+/*******************************************************************************
+ ** Backend module for a table based on a java enum. So we can expose an enum
+ ** as a table (similar to exposing an enum as a possible value source), with multiple
+ ** fields in the enum (exposed via getter methods in the enum) as fields in the table.
+ **
+ ** Only supports read-operations, as you can't modify an enum.
+ *******************************************************************************/
+public class EnumerationBackendModule implements QBackendModuleInterface
+{
+
+ /*******************************************************************************
+ ** Method where a backend module must be able to provide its type (name).
+ *******************************************************************************/
+ @Override
+ public String getBackendType()
+ {
+ return ("enum");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Class extends QTableBackendDetails> getTableBackendDetailsClass()
+ {
+ return (EnumerationTableBackendDetails.class);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QueryInterface getQueryInterface()
+ {
+ return new EnumerationQueryAction();
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java
new file mode 100644
index 00000000..039e0123
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java
@@ -0,0 +1,84 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEnum;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class EnumerationQueryAction implements QueryInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QueryOutput execute(QueryInput queryInput) throws QException
+ {
+ try
+ {
+ QTableMetaData table = queryInput.getTable();
+ EnumerationTableBackendDetails backendDetails = (EnumerationTableBackendDetails) table.getBackendDetails();
+ Class extends QRecordEnum> enumClass = backendDetails.getEnumClass();
+ QRecordEnum[] values = (QRecordEnum[]) enumClass.getMethod("values").invoke(null);
+
+ //////////////////////////////////////////////
+ // note - not good streaming behavior here. //
+ //////////////////////////////////////////////
+
+ List recordList = new ArrayList<>();
+ for(QRecordEnum value : values)
+ {
+ QRecord record = value.toQRecord();
+ boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(queryInput.getFilter(), record);
+ if(recordMatches)
+ {
+ recordList.add(record);
+ }
+ }
+
+ BackendQueryFilterUtils.sortRecordList(queryInput.getFilter(), recordList);
+ recordList = BackendQueryFilterUtils.applySkipAndLimit(queryInput, recordList);
+
+ QueryOutput queryOutput = new QueryOutput(queryInput);
+ queryOutput.addRecords(recordList);
+ return queryOutput;
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error executing query", e));
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationTableBackendDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationTableBackendDetails.java
new file mode 100644
index 00000000..ee92f8ce
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationTableBackendDetails.java
@@ -0,0 +1,82 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration;
+
+
+import com.kingsrook.qqq.backend.core.model.data.QRecordEnum;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class EnumerationTableBackendDetails extends QTableBackendDetails
+{
+ private Class extends QRecordEnum> enumClass;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public EnumerationTableBackendDetails()
+ {
+ super();
+ setBackendType(EnumerationBackendModule.class);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for enumClass
+ **
+ *******************************************************************************/
+ public Class extends QRecordEnum> getEnumClass()
+ {
+ return enumClass;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for enumClass
+ **
+ *******************************************************************************/
+ public void setEnumClass(Class extends QRecordEnum> enumClass)
+ {
+ this.enumClass = enumClass;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for enumClass
+ **
+ *******************************************************************************/
+ public EnumerationTableBackendDetails withEnumClass(Class extends QRecordEnum> enumClass)
+ {
+ this.enumClass = enumClass;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
index 30346a7f..cb423dc5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
@@ -27,7 +27,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
-import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@@ -53,17 +52,6 @@ public class MemoryBackendModule implements QBackendModuleInterface
- /*******************************************************************************
- ** Method to identify the class used for backend meta data for this module.
- *******************************************************************************/
- @Override
- public Class extends QBackendMetaData> getBackendMetaDataClass()
- {
- return (QBackendMetaData.class);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
index 54370e6f..153699a2 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
@@ -23,27 +23,21 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.io.Serializable;
-import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
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.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
-import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
-import com.kingsrook.qqq.backend.core.utils.ValueUtils;
-import org.apache.commons.lang.NotImplementedException;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
/*******************************************************************************
@@ -126,7 +120,7 @@ public class MemoryRecordStore
for(QRecord qRecord : tableData.values())
{
- boolean recordMatches = doesRecordMatch(input.getFilter(), qRecord);
+ boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord);
if(recordMatches)
{
@@ -139,349 +133,6 @@ public class MemoryRecordStore
- /*******************************************************************************
- **
- *******************************************************************************/
- @SuppressWarnings("checkstyle:indentation")
- private boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord)
- {
- if(filter == null || !filter.hasAnyCriteria())
- {
- return (true);
- }
-
- /////////////////////////////////////////////////////////////////////////////////////
- // for an AND query, default to a TRUE answer, and we'll &= each criteria's value. //
- // for an OR query, default to FALSE, and |= each criteria's value. //
- /////////////////////////////////////////////////////////////////////////////////////
- AtomicBoolean recordMatches = new AtomicBoolean(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.AND) ? true : false);
-
- ///////////////////////////////////////
- // if there are criteria, apply them //
- ///////////////////////////////////////
- for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria()))
- {
- String fieldName = criterion.getFieldName();
- Serializable value = qRecord.getValue(fieldName);
-
- boolean criterionMatches = switch(criterion.getOperator())
- {
- case EQUALS -> testEquals(criterion, value);
- case NOT_EQUALS -> !testEquals(criterion, value);
- case IN -> testIn(criterion, value);
- case NOT_IN -> !testIn(criterion, value);
- case IS_BLANK -> testBlank(criterion, value);
- case IS_NOT_BLANK -> !testBlank(criterion, value);
- case CONTAINS -> testContains(criterion, fieldName, value);
- case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
- case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
- case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
- case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
- case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
- case GREATER_THAN -> testGreaterThan(criterion, value);
- case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
- case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
- case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
- case BETWEEN ->
- {
- QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
- QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
- criteria1.getValues().remove(0);
- yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
- }
- case NOT_BETWEEN ->
- {
- QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
- QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues());
- criteria1.getValues().remove(0);
- yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
- }
- };
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- Boolean shortCircuitValue = applyBooleanOperator(recordMatches, criterionMatches, filter.getBooleanOperator());
- if(shortCircuitValue != null)
- {
- return (shortCircuitValue);
- }
- }
-
- ////////////////////////////////////////
- // apply sub-filters if there are any //
- ////////////////////////////////////////
- for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
- {
- boolean subFilterMatches = doesRecordMatch(subFilter, qRecord);
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- Boolean shortCircuitValue = applyBooleanOperator(recordMatches, subFilterMatches, filter.getBooleanOperator());
- if(shortCircuitValue != null)
- {
- return (shortCircuitValue);
- }
- }
-
- return (recordMatches.getPlain());
- }
-
-
-
- /*******************************************************************************
- ** Based on an incoming boolean value (accumulator), a new value, and a boolean
- ** operator, update the accumulator, and if we can then short-circuit remaining
- ** operations, return a true or false. Returning null means to keep going.
- *******************************************************************************/
- private Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator)
- {
- boolean accumulatorValue = accumulator.getPlain();
- if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND))
- {
- accumulatorValue &= newValue;
- if(!accumulatorValue)
- {
- return (false);
- }
- }
- else
- {
- accumulatorValue |= newValue;
- if(accumulatorValue)
- {
- return (true);
- }
- }
-
- accumulator.set(accumulatorValue);
- return (null);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testBlank(QFilterCriteria criterion, Serializable value)
- {
- if(value == null)
- {
- return (true);
- }
-
- if("".equals(ValueUtils.getValueAsString(value)))
- {
- return (true);
- }
-
- return (false);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testGreaterThan(QFilterCriteria criterion, Serializable value)
- {
- Serializable criterionValue = criterion.getValues().get(0);
- if(criterionValue == null)
- {
- throw (new IllegalArgumentException("Missing criterion value in query"));
- }
-
- if(value == null)
- {
- /////////////////////////////////////////////////////////////////////////////////////
- // a database would say 'false' for if a null column is > a value, so do the same. //
- /////////////////////////////////////////////////////////////////////////////////////
- return (false);
- }
-
- if(value instanceof LocalDate valueDate && criterionValue instanceof LocalDate criterionValueDate)
- {
- return (valueDate.isAfter(criterionValueDate));
- }
-
- if(value instanceof Number valueNumber && criterionValue instanceof Number criterionValueNumber)
- {
- return (valueNumber.doubleValue() > criterionValueNumber.doubleValue());
- }
-
- if(value instanceof LocalDate || criterionValue instanceof LocalDate)
- {
- LocalDate valueDate;
- if(value instanceof LocalDate ld)
- {
- valueDate = ld;
- }
- else
- {
- valueDate = ValueUtils.getValueAsLocalDate(value);
- }
-
- LocalDate criterionDate;
- if(criterionValue instanceof LocalDate ld)
- {
- criterionDate = ld;
- }
- else
- {
- criterionDate = ValueUtils.getValueAsLocalDate(criterionValue);
- }
-
- return (valueDate.isAfter(criterionDate));
- }
-
- throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + value.getClass().getSimpleName() + "][" + criterionValue.getClass().getSimpleName() + "]"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testIn(QFilterCriteria criterion, Serializable value)
- {
- if(!criterion.getValues().contains(value))
- {
- return (false);
- }
- return (true);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testEquals(QFilterCriteria criterion, Serializable value)
- {
- if(value == null)
- {
- return (false);
- }
-
- Serializable criteriaValue = criterion.getValues().get(0);
- if(value instanceof String && criteriaValue instanceof Number)
- {
- criteriaValue = String.valueOf(criteriaValue);
- }
- else if(criteriaValue instanceof String && value instanceof Number)
- {
- value = String.valueOf(value);
- }
-
- if(!value.equals(criteriaValue))
- {
- return (false);
- }
- return (true);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testContains(QFilterCriteria criterion, String fieldName, Serializable value)
- {
- String stringValue = getStringFieldValue(value, fieldName, criterion);
- String criterionValue = getFirstStringCriterionValue(criterion);
-
- if(!stringValue.contains(criterionValue))
- {
- return (false);
- }
-
- return (true);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testStartsWith(QFilterCriteria criterion, String fieldName, Serializable value)
- {
- String stringValue = getStringFieldValue(value, fieldName, criterion);
- String criterionValue = getFirstStringCriterionValue(criterion);
-
- if(!stringValue.startsWith(criterionValue))
- {
- return (false);
- }
-
- return (true);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private boolean testEndsWith(QFilterCriteria criterion, String fieldName, Serializable value)
- {
- String stringValue = getStringFieldValue(value, fieldName, criterion);
- String criterionValue = getFirstStringCriterionValue(criterion);
-
- if(!stringValue.endsWith(criterionValue))
- {
- return (false);
- }
-
- return (true);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private String getFirstStringCriterionValue(QFilterCriteria criteria)
- {
- if(CollectionUtils.nullSafeIsEmpty(criteria.getValues()))
- {
- throw new IllegalArgumentException("Missing value for [" + criteria.getOperator() + "] criteria on field [" + criteria.getFieldName() + "]");
- }
- Serializable value = criteria.getValues().get(0);
- if(value == null)
- {
- return "";
- }
-
- if(!(value instanceof String stringValue))
- {
- throw new ClassCastException("Value [" + value + "] for criteria [" + criteria.getFieldName() + "] is not a String, which is required for the [" + criteria.getOperator() + "] operator.");
- }
-
- return (stringValue);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private String getStringFieldValue(Serializable value, String fieldName, QFilterCriteria criterion)
- {
- if(value == null)
- {
- return "";
- }
-
- if(!(value instanceof String stringValue))
- {
- throw new ClassCastException("Value [" + value + "] in field [" + fieldName + "] is not a String, which is required for the [" + criterion.getOperator() + "] operator.");
- }
-
- return (stringValue);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java
new file mode 100644
index 00000000..aa2bdfb1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java
@@ -0,0 +1,475 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils;
+
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.commons.lang.NotImplementedException;
+
+
+/*******************************************************************************
+ ** Utility class for backend modules that need to do filter operations.
+ **
+ ** e.g., like an in-memory module, or one that's working with files - basically
+ ** one that doesn't have filtering provided by the backend (like a database or API).
+ *******************************************************************************/
+public class BackendQueryFilterUtils
+{
+
+ /*******************************************************************************
+ ** Test if record matches filter.
+ *******************************************************************************/
+ @SuppressWarnings("checkstyle:indentation")
+ public static boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord)
+ {
+ if(filter == null || !filter.hasAnyCriteria())
+ {
+ return (true);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // for an AND query, default to a TRUE answer, and we'll &= each criteria's value. //
+ // for an OR query, default to FALSE, and |= each criteria's value. //
+ /////////////////////////////////////////////////////////////////////////////////////
+ AtomicBoolean recordMatches = new AtomicBoolean(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.AND) ? true : false);
+
+ ///////////////////////////////////////
+ // if there are criteria, apply them //
+ ///////////////////////////////////////
+ for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria()))
+ {
+ String fieldName = criterion.getFieldName();
+ Serializable value = qRecord.getValue(fieldName);
+
+ boolean criterionMatches = switch(criterion.getOperator())
+ {
+ case EQUALS -> testEquals(criterion, value);
+ case NOT_EQUALS -> !testEquals(criterion, value);
+ case IN -> testIn(criterion, value);
+ case NOT_IN -> !testIn(criterion, value);
+ case IS_BLANK -> testBlank(criterion, value);
+ case IS_NOT_BLANK -> !testBlank(criterion, value);
+ case CONTAINS -> testContains(criterion, fieldName, value);
+ case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
+ case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
+ case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
+ case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
+ case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
+ case GREATER_THAN -> testGreaterThan(criterion, value);
+ case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
+ case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
+ case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
+ case BETWEEN ->
+ {
+ QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
+ QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
+ criteria1.getValues().remove(0);
+ yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
+ }
+ case NOT_BETWEEN ->
+ {
+ QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
+ QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues());
+ criteria1.getValues().remove(0);
+ yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ Boolean shortCircuitValue = applyBooleanOperator(recordMatches, criterionMatches, filter.getBooleanOperator());
+ if(shortCircuitValue != null)
+ {
+ return (shortCircuitValue);
+ }
+ }
+
+ ////////////////////////////////////////
+ // apply sub-filters if there are any //
+ ////////////////////////////////////////
+ for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
+ {
+ boolean subFilterMatches = doesRecordMatch(subFilter, qRecord);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ Boolean shortCircuitValue = applyBooleanOperator(recordMatches, subFilterMatches, filter.getBooleanOperator());
+ if(shortCircuitValue != null)
+ {
+ return (shortCircuitValue);
+ }
+ }
+
+ return (recordMatches.getPlain());
+ }
+
+
+
+ /*******************************************************************************
+ ** Based on an incoming boolean value (accumulator), a new value, and a boolean
+ ** operator, update the accumulator, and if we can then short-circuit remaining
+ ** operations, return a true or false. Returning null means to keep going.
+ *******************************************************************************/
+ private static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator)
+ {
+ boolean accumulatorValue = accumulator.getPlain();
+ if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND))
+ {
+ accumulatorValue &= newValue;
+ if(!accumulatorValue)
+ {
+ return (false);
+ }
+ }
+ else
+ {
+ accumulatorValue |= newValue;
+ if(accumulatorValue)
+ {
+ return (true);
+ }
+ }
+
+ accumulator.set(accumulatorValue);
+ return (null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testBlank(QFilterCriteria criterion, Serializable value)
+ {
+ if(value == null)
+ {
+ return (true);
+ }
+
+ if("".equals(ValueUtils.getValueAsString(value)))
+ {
+ return (true);
+ }
+
+ return (false);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testGreaterThan(QFilterCriteria criterion, Serializable value)
+ {
+ Serializable criterionValue = criterion.getValues().get(0);
+ if(criterionValue == null)
+ {
+ throw (new IllegalArgumentException("Missing criterion value in query"));
+ }
+
+ if(value == null)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////
+ // a database would say 'false' for if a null column is > a value, so do the same. //
+ /////////////////////////////////////////////////////////////////////////////////////
+ return (false);
+ }
+
+ return isGreaterThan(criterionValue, value);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean isGreaterThan(Serializable a, Serializable b)
+ {
+ if(Objects.equals(a, b))
+ {
+ return false;
+ }
+
+ if(b instanceof LocalDate valueDate && a instanceof LocalDate criterionValueDate)
+ {
+ return (valueDate.isAfter(criterionValueDate));
+ }
+
+ if(b instanceof Number valueNumber && a instanceof Number criterionValueNumber)
+ {
+ return (valueNumber.doubleValue() > criterionValueNumber.doubleValue());
+ }
+
+ if(b instanceof String valueString && a instanceof String criterionValueString)
+ {
+ return (valueString.compareTo(criterionValueString) > 0);
+ }
+
+ if(b instanceof LocalDate || a instanceof LocalDate)
+ {
+ LocalDate valueDate;
+ if(b instanceof LocalDate ld)
+ {
+ valueDate = ld;
+ }
+ else
+ {
+ valueDate = ValueUtils.getValueAsLocalDate(b);
+ }
+
+ LocalDate criterionDate;
+ if(a instanceof LocalDate ld)
+ {
+ criterionDate = ld;
+ }
+ else
+ {
+ criterionDate = ValueUtils.getValueAsLocalDate(a);
+ }
+
+ return (valueDate.isAfter(criterionDate));
+ }
+
+ throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + b.getClass().getSimpleName() + "][" + a.getClass().getSimpleName() + "]"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testIn(QFilterCriteria criterion, Serializable value)
+ {
+ if(!criterion.getValues().contains(value))
+ {
+ return (false);
+ }
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testEquals(QFilterCriteria criterion, Serializable value)
+ {
+ if(value == null)
+ {
+ return (false);
+ }
+
+ Serializable criteriaValue = criterion.getValues().get(0);
+ if(value instanceof String && criteriaValue instanceof Number)
+ {
+ criteriaValue = String.valueOf(criteriaValue);
+ }
+ else if(criteriaValue instanceof String && value instanceof Number)
+ {
+ value = String.valueOf(value);
+ }
+
+ if(!value.equals(criteriaValue))
+ {
+ return (false);
+ }
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testContains(QFilterCriteria criterion, String fieldName, Serializable value)
+ {
+ String stringValue = getStringFieldValue(value, fieldName, criterion);
+ String criterionValue = getFirstStringCriterionValue(criterion);
+
+ if(!stringValue.contains(criterionValue))
+ {
+ return (false);
+ }
+
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testStartsWith(QFilterCriteria criterion, String fieldName, Serializable value)
+ {
+ String stringValue = getStringFieldValue(value, fieldName, criterion);
+ String criterionValue = getFirstStringCriterionValue(criterion);
+
+ if(!stringValue.startsWith(criterionValue))
+ {
+ return (false);
+ }
+
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean testEndsWith(QFilterCriteria criterion, String fieldName, Serializable value)
+ {
+ String stringValue = getStringFieldValue(value, fieldName, criterion);
+ String criterionValue = getFirstStringCriterionValue(criterion);
+
+ if(!stringValue.endsWith(criterionValue))
+ {
+ return (false);
+ }
+
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static String getFirstStringCriterionValue(QFilterCriteria criteria)
+ {
+ if(CollectionUtils.nullSafeIsEmpty(criteria.getValues()))
+ {
+ throw new IllegalArgumentException("Missing value for [" + criteria.getOperator() + "] criteria on field [" + criteria.getFieldName() + "]");
+ }
+ Serializable value = criteria.getValues().get(0);
+ if(value == null)
+ {
+ return "";
+ }
+
+ if(!(value instanceof String stringValue))
+ {
+ throw new ClassCastException("Value [" + value + "] for criteria [" + criteria.getFieldName() + "] is not a String, which is required for the [" + criteria.getOperator() + "] operator.");
+ }
+
+ return (stringValue);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static String getStringFieldValue(Serializable value, String fieldName, QFilterCriteria criterion)
+ {
+ if(value == null)
+ {
+ return "";
+ }
+
+ if(!(value instanceof String stringValue))
+ {
+ throw new ClassCastException("Value [" + value + "] in field [" + fieldName + "] is not a String, which is required for the [" + criterion.getOperator() + "] operator.");
+ }
+
+ return (stringValue);
+ }
+
+
+
+ /*******************************************************************************
+ ** Sort list of records based on filter.
+ *******************************************************************************/
+ public static void sortRecordList(QQueryFilter filter, List recordList)
+ {
+ if(filter == null || CollectionUtils.nullSafeIsEmpty(filter.getOrderBys()))
+ {
+ return;
+ }
+
+ recordList.sort((a, b) ->
+ {
+ for(QFilterOrderBy orderBy : filter.getOrderBys())
+ {
+ Serializable valueA = a.getValue(orderBy.getFieldName());
+ Serializable valueB = b.getValue(orderBy.getFieldName());
+ if(Objects.equals(valueA, valueB))
+ {
+ continue;
+ }
+ else if(isGreaterThan(valueA, valueB) && orderBy.getIsAscending())
+ {
+ return (-1);
+ }
+ else
+ {
+ return (1);
+ }
+ }
+
+ return (0);
+ });
+ }
+
+
+
+ /*******************************************************************************
+ ** Apply skip & limit attributes from queryInput to a list of records.
+ *******************************************************************************/
+ public static List applySkipAndLimit(QueryInput queryInput, List recordList)
+ {
+ Integer skip = queryInput.getSkip();
+ if(skip != null && skip > 0)
+ {
+ if(skip < recordList.size())
+ {
+ recordList = recordList.subList(skip, recordList.size());
+ }
+ else
+ {
+ recordList.clear();
+ }
+ }
+
+ Integer limit = queryInput.getLimit();
+ if(limit != null && limit >= 0 && limit < recordList.size())
+ {
+ recordList = recordList.subList(0, limit);
+ }
+ return recordList;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/BasepullConfiguration.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/BasepullConfiguration.java
new file mode 100644
index 00000000..e207258d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/BasepullConfiguration.java
@@ -0,0 +1,247 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.basepull;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ ** Class for storing all basepull configuration data
+ **
+ *******************************************************************************/
+public class BasepullConfiguration implements Serializable
+{
+ private String tableName; // the table that stores the basepull timestamps
+ private String keyField; // the field in the basepull timestamps table that stores the key of the basepull (e.g., a process name)
+ private String keyValue; // the key applied to the keyField - optional - if not set, process.getName is used.
+
+ private String lastRunTimeFieldName; // the field in the basepull timestamps table that stores the last-run time for the job.
+ private Integer hoursBackForInitialTimestamp; // for the first-run use-case (where there is no row in the timestamps table), how many hours back in time to look.
+
+ private String timestampField; // the name of the field in the table being queried against the last-run timestamp.
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ public String getTableName()
+ {
+ return tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableName
+ **
+ *******************************************************************************/
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public BasepullConfiguration withTableName(String tableName)
+ {
+ this.tableName = tableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for keyField
+ **
+ *******************************************************************************/
+ public String getKeyField()
+ {
+ return keyField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for keyField
+ **
+ *******************************************************************************/
+ public void setKeyField(String keyField)
+ {
+ this.keyField = keyField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for keyField
+ **
+ *******************************************************************************/
+ public BasepullConfiguration withKeyField(String keyField)
+ {
+ this.keyField = keyField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for keyValue
+ **
+ *******************************************************************************/
+ public String getKeyValue()
+ {
+ return keyValue;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for keyValue
+ **
+ *******************************************************************************/
+ public void setKeyValue(String keyValue)
+ {
+ this.keyValue = keyValue;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for keyValue
+ **
+ *******************************************************************************/
+ public BasepullConfiguration withKeyValue(String keyValue)
+ {
+ this.keyValue = keyValue;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for lastRunTimeFieldName
+ **
+ *******************************************************************************/
+ public String getLastRunTimeFieldName()
+ {
+ return lastRunTimeFieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for lastRunTimeFieldName
+ **
+ *******************************************************************************/
+ public void setLastRunTimeFieldName(String lastRunTimeFieldName)
+ {
+ this.lastRunTimeFieldName = lastRunTimeFieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for lastRunTimeFieldName
+ **
+ *******************************************************************************/
+ public BasepullConfiguration withLastRunTimeFieldName(String lastRunTimeFieldName)
+ {
+ this.lastRunTimeFieldName = lastRunTimeFieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for hoursBackForInitialTimestamp
+ **
+ *******************************************************************************/
+ public Integer getHoursBackForInitialTimestamp()
+ {
+ return hoursBackForInitialTimestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for hoursBackForInitialTimestamp
+ **
+ *******************************************************************************/
+ public void setHoursBackForInitialTimestamp(Integer hoursBackForInitialTimestamp)
+ {
+ this.hoursBackForInitialTimestamp = hoursBackForInitialTimestamp;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for hoursBackForInitialTimestamp
+ **
+ *******************************************************************************/
+ public BasepullConfiguration withHoursBackForInitialTimestamp(Integer hoursBackForInitialTimestamp)
+ {
+ this.hoursBackForInitialTimestamp = hoursBackForInitialTimestamp;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for timestampField
+ **
+ *******************************************************************************/
+ public String getTimestampField()
+ {
+ return timestampField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for timestampField
+ **
+ *******************************************************************************/
+ public void setTimestampField(String timestampField)
+ {
+ this.timestampField = timestampField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for timestampField
+ **
+ *******************************************************************************/
+ public BasepullConfiguration withTimestampField(String timestampField)
+ {
+ this.timestampField = timestampField;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStep.java
new file mode 100644
index 00000000..806483af
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStep.java
@@ -0,0 +1,102 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.basepull;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
+
+
+/*******************************************************************************
+ ** Version of ExtractViaQueryStep that knows how to set up a basepull query.
+ *******************************************************************************/
+public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected QQueryFilter getQueryFilter(RunBackendStepInput runBackendStepInput) throws QException
+ {
+ //////////////////////////////////////////////////////////////
+ // get input query filter or if not found, create a new one //
+ //////////////////////////////////////////////////////////////
+ QQueryFilter queryFilter = new QQueryFilter();
+ try
+ {
+ queryFilter = super.getQueryFilter(runBackendStepInput);
+ }
+ catch(QException qe)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // if we catch here, assume that is because there was no default filter, continue on //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // build up a query filter that is against the source table for the given source table timestamp //
+ // field, finding any records that need processed. //
+ // query will be for: timestamp > lastRun AND timestamp <= thisRun. //
+ // then thisRun will be stored, so the next run shouldn't find any records from thisRun. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ queryFilter.addCriteria(new QFilterCriteria()
+ .withFieldName(runBackendStepInput.getValueString(RunProcessAction.BASEPULL_TIMESTAMP_FIELD))
+ .withOperator(QCriteriaOperator.GREATER_THAN)
+ .withValues(List.of(getLastRunTimeString(runBackendStepInput))));
+
+ queryFilter.addCriteria(new QFilterCriteria()
+ .withFieldName(runBackendStepInput.getValueString(RunProcessAction.BASEPULL_TIMESTAMP_FIELD))
+ .withOperator(QCriteriaOperator.LESS_THAN_OR_EQUALS)
+ .withValues(List.of(getThisRunTimeString(runBackendStepInput))));
+
+ queryFilter.addOrderBy(new QFilterOrderBy(runBackendStepInput.getValueString(RunProcessAction.BASEPULL_TIMESTAMP_FIELD)));
+
+ return (queryFilter);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected String getLastRunTimeString(RunBackendStepInput runBackendStepInput) throws QException
+ {
+ return (runBackendStepInput.getBasepullLastRunTime().toString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected String getThisRunTimeString(RunBackendStepInput runBackendStepInput) throws QException
+ {
+ return (runBackendStepInput.getValueInstant(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY).toString());
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java
index ea1dbc58..ce227961 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java
@@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
/*******************************************************************************
@@ -49,6 +50,18 @@ public abstract class AbstractExtractStep implements BackendStep
+ /*******************************************************************************
+ ** Allow subclasses to do an action before the run begins.
+ *******************************************************************************/
+ public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ ////////////////////////
+ // noop in base class //
+ ////////////////////////
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java
index a7a86049..2f1cdc05 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java
@@ -54,6 +54,20 @@ public class ExtractViaQueryStep extends AbstractExtractStep
{
public static final String FIELD_SOURCE_TABLE = "sourceTable";
+ private QQueryFilter queryFilter;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ super.preRun(runBackendStepInput, runBackendStepOutput);
+ queryFilter = getQueryFilter(runBackendStepInput);
+ }
+
/*******************************************************************************
@@ -66,7 +80,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
QueryInput queryInput = new QueryInput(runBackendStepInput.getInstance());
queryInput.setSession(runBackendStepInput.getSession());
queryInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
- queryInput.setFilter(getQueryFilter(runBackendStepInput));
+ queryInput.setFilter(queryFilter);
queryInput.setRecordPipe(getRecordPipe());
queryInput.setLimit(getLimit());
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
@@ -88,7 +102,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
CountInput countInput = new CountInput(runBackendStepInput.getInstance());
countInput.setSession(runBackendStepInput.getSession());
countInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
- countInput.setFilter(getQueryFilter(runBackendStepInput));
+ countInput.setFilter(queryFilter);
CountOutput countOutput = new CountAction().execute(countInput);
return (countOutput.getCount());
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java
index 86a7e74a..b75c9edb 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java
@@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@@ -49,6 +50,9 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep
{
public static final String FIELD_DESTINATION_TABLE = "destinationTable";
+ protected List recordsToInsert = null;
+ protected List recordsToUpdate = null;
+
/*******************************************************************************
@@ -58,22 +62,20 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
- QTableMetaData tableMetaData = runBackendStepInput.getInstance().getTable(runBackendStepInput.getValueString(FIELD_DESTINATION_TABLE));
- List recordsToInsert = new ArrayList<>();
- List recordsToUpdate = new ArrayList<>();
- for(QRecord record : runBackendStepInput.getRecords())
- {
- if(record.getValue(tableMetaData.getPrimaryKeyField()) == null)
- {
- recordsToInsert.add(record);
- }
- else
- {
- recordsToUpdate.add(record);
- }
- }
+ evaluateRecords(runBackendStepInput);
+ insertAndUpdateRecords(runBackendStepInput, runBackendStepOutput);
+ }
- if(!recordsToInsert.isEmpty())
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void insertAndUpdateRecords(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ QTableMetaData tableMetaData = runBackendStepInput.getInstance().getTable(runBackendStepInput.getValueString(FIELD_DESTINATION_TABLE));
+
+ if(CollectionUtils.nullSafeHasContents(recordsToInsert))
{
InsertInput insertInput = new InsertInput(runBackendStepInput.getInstance());
insertInput.setSession(runBackendStepInput.getSession());
@@ -84,7 +86,7 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep
runBackendStepOutput.getRecords().addAll(insertOutput.getRecords());
}
- if(!recordsToUpdate.isEmpty())
+ if(CollectionUtils.nullSafeHasContents(recordsToUpdate))
{
UpdateInput updateInput = new UpdateInput(runBackendStepInput.getInstance());
updateInput.setSession(runBackendStepInput.getSession());
@@ -110,4 +112,27 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep
return (Optional.of(new InsertAction().openTransaction(insertInput)));
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected void evaluateRecords(RunBackendStepInput runBackendStepInput) throws QException
+ {
+ QTableMetaData tableMetaData = runBackendStepInput.getInstance().getTable(runBackendStepInput.getValueString(FIELD_DESTINATION_TABLE));
+ recordsToInsert = new ArrayList<>();
+ recordsToUpdate = new ArrayList<>();
+ for(QRecord record : runBackendStepInput.getRecords())
+ {
+ if(record.getValue(tableMetaData.getPrimaryKeyField()) == null)
+ {
+ recordsToInsert.add(record);
+ }
+ else
+ {
+ recordsToUpdate.add(record);
+ }
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java
index 66e3f9c6..2f0d9d00 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java
@@ -28,6 +28,7 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
@@ -63,6 +64,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
RecordPipe recordPipe = new RecordPipe();
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
extractStep.setRecordPipe(recordPipe);
+ extractStep.preRun(runBackendStepInput, runBackendStepOutput);
AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
AbstractLoadStep loadStep = getLoadStep(runBackendStepInput);
@@ -88,14 +90,26 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
updateRecordsWithDisplayValuesAndPossibleValues(runBackendStepInput, loadedRecordList);
runBackendStepOutput.setRecords(loadedRecordList);
- ////////////////////////////////////////////////////////////////////////////////////////////////////
- // get the process summary from the ... transform step? the load step? each knows some... todo? //
- ////////////////////////////////////////////////////////////////////////////////////////////////////
- runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true));
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // get the process summary from the load step, if it's a summary-provider -- else, use the transform step (which is always a provider) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(loadStep instanceof ProcessSummaryProviderInterface provider)
+ {
+ runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, provider.doGetProcessSummary(runBackendStepOutput, true));
+ }
+ else
+ {
+ runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true));
+ }
transformStep.postRun(runBackendStepInput, runBackendStepOutput);
loadStep.postRun(runBackendStepInput, runBackendStepOutput);
+ //////////////////////////////////////////////////////////////////////////////
+ // set the flag to state that the basepull timestamp should be updated now. //
+ //////////////////////////////////////////////////////////////////////////////
+ runBackendStepOutput.addValue(RunProcessAction.BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD, true);
+
/////////////////////
// commit the work //
/////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
index 7dd6c185..7c3c782e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
@@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import org.apache.logging.log4j.LogManager;
@@ -66,6 +67,12 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
+ if(runBackendStepInput.getFrontendStepBehavior() != null && runBackendStepInput.getFrontendStepBehavior().equals(RunProcessInput.FrontendStepBehavior.SKIP))
+ {
+ LOG.info("Skipping preview because frontent behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "].");
+ return;
+ }
+
/////////////////////////////////////////////////////////////////
// if we're running inside an automation, then skip this step. //
/////////////////////////////////////////////////////////////////
@@ -75,13 +82,21 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
- ///////////////////////////////////////////
- // request a count from the extract step //
- ///////////////////////////////////////////
+ //////////////////////////////////////////
+ // set up the extract & transform steps //
+ //////////////////////////////////////////
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
- Integer recordCount = extractStep.doCount(runBackendStepInput);
+ RecordPipe recordPipe = new RecordPipe();
+ extractStep.setLimit(limit);
+ extractStep.setRecordPipe(recordPipe);
+ extractStep.preRun(runBackendStepInput, runBackendStepOutput);
+
+ Integer recordCount = extractStep.doCount(runBackendStepInput);
runBackendStepOutput.addValue(StreamedETLProcess.FIELD_RECORD_COUNT, recordCount);
+ AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
+ transformStep.preRun(runBackendStepInput, runBackendStepOutput);
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the count is less than the normal limit here, and this process supports validation, then go straight to the validation step //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -93,16 +108,6 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
// return;
// }
- ////////////////////////////////////////////////////////
- // proceed with a doing a limited extract & transform //
- ////////////////////////////////////////////////////////
- RecordPipe recordPipe = new RecordPipe();
- extractStep.setLimit(limit);
- extractStep.setRecordPipe(recordPipe);
-
- AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
- transformStep.preRun(runBackendStepInput, runBackendStepOutput);
-
List previewRecordList = new ArrayList<>();
new AsyncRecordPipeLoop().run("StreamedETL>Preview>ExtractStep", PROCESS_OUTPUT_RECORD_LIST_LIMIT, recordPipe, (status) ->
{
@@ -140,7 +145,6 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
-
/*******************************************************************************
**
*******************************************************************************/
@@ -154,7 +158,7 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
///////////////////////////////////////////////////////////////////////
// make streamed input & output objects from the run input & outputs //
///////////////////////////////////////////////////////////////////////
- StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords);
+ StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords);
StreamedBackendStepOutput streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput);
/////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
index e10a57df..ac9b9e8c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
@@ -85,6 +85,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
extractStep.setLimit(null);
extractStep.setRecordPipe(recordPipe);
+ extractStep.preRun(runBackendStepInput, runBackendStepOutput);
AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
transformStep.preRun(runBackendStepInput, runBackendStepOutput);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java
index caaf431d..ebbd80c9 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java
@@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit
import java.io.Serializable;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
@@ -131,8 +136,8 @@ public class StreamedETLWithFrontendProcess
.withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE)))
.withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true)))
.withField(new QFieldMetaData(FIELD_DEFAULT_QUERY_FILTER, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DEFAULT_QUERY_FILTER)))
- .withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(extractStepClass)))
- .withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(transformStepClass)))
+ .withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(extractStepClass == null ? null : new QCodeReference(extractStepClass)))
+ .withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(transformStepClass == null ? null : new QCodeReference(transformStepClass)))
.withField(new QFieldMetaData(FIELD_PREVIEW_MESSAGE, QFieldType.STRING).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_PREVIEW_MESSAGE, DEFAULT_PREVIEW_MESSAGE_FOR_INSERT)))
);
@@ -153,7 +158,7 @@ public class StreamedETLWithFrontendProcess
.withName(STEP_NAME_EXECUTE)
.withCode(new QCodeReference(StreamedETLExecuteStep.class))
.withInputData(new QFunctionInputMetaData()
- .withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(loadStepClass))))
+ .withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(loadStepClass == null ? null : new QCodeReference(loadStepClass))))
.withOutputMetaData(new QFunctionOutputMetaData()
.withField(new QFieldMetaData(FIELD_PROCESS_SUMMARY, QFieldType.STRING))
);
@@ -169,4 +174,204 @@ public class StreamedETLWithFrontendProcess
.addStep(executeStep)
.addStep(resultStep);
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Builder processMetaDataBuilder()
+ {
+ return (new Builder(defineProcessMetaData(null, null, null, Collections.emptyMap())));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class Builder extends AbstractProcessMetaDataBuilder
+ {
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Builder(QProcessMetaData processMetaData)
+ {
+ super(processMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for extractStepClass
+ **
+ *******************************************************************************/
+ public Builder withExtractStepClass(Class extends AbstractExtractStep> extractStepClass)
+ {
+ setInputFieldDefaultValue(FIELD_EXTRACT_CODE, new QCodeReference(extractStepClass));
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for transformStepClass
+ **
+ *******************************************************************************/
+ public Builder withTransformStepClass(Class extends AbstractTransformStep> transformStepClass)
+ {
+ setInputFieldDefaultValue(FIELD_TRANSFORM_CODE, new QCodeReference(transformStepClass));
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for loadStepClass
+ **
+ *******************************************************************************/
+ public Builder withLoadStepClass(Class extends AbstractLoadStep> loadStepClass)
+ {
+ setInputFieldDefaultValue(FIELD_LOAD_CODE, new QCodeReference(loadStepClass));
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sourceTable
+ **
+ *******************************************************************************/
+ public Builder withSourceTable(String sourceTable)
+ {
+ setInputFieldDefaultValue(FIELD_SOURCE_TABLE, sourceTable);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for destinationTable
+ **
+ *******************************************************************************/
+ public Builder withDestinationTable(String destinationTable)
+ {
+ setInputFieldDefaultValue(FIELD_DESTINATION_TABLE, destinationTable);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for supportsFullValidation
+ **
+ *******************************************************************************/
+ public Builder withSupportsFullValidation(Boolean supportsFullValidation)
+ {
+ setInputFieldDefaultValue(FIELD_SUPPORTS_FULL_VALIDATION, supportsFullValidation);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for doFullValidation
+ **
+ *******************************************************************************/
+ public Builder withDoFullValidation(Boolean doFullValidation)
+ {
+ setInputFieldDefaultValue(FIELD_DO_FULL_VALIDATION, doFullValidation);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for defaultQueryFilter
+ **
+ *******************************************************************************/
+ public Builder withDefaultQueryFilter(QQueryFilter defaultQueryFilter)
+ {
+ setInputFieldDefaultValue(FIELD_DEFAULT_QUERY_FILTER, defaultQueryFilter);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for previewMessage
+ **
+ *******************************************************************************/
+ public Builder withPreviewMessage(String previewMessage)
+ {
+ setInputFieldDefaultValue(FIELD_PREVIEW_MESSAGE, previewMessage);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ **
+ *******************************************************************************/
+ public Builder withName(String name)
+ {
+ processMetaData.setName(name);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for label
+ **
+ *******************************************************************************/
+ public Builder withLabel(String name)
+ {
+ processMetaData.setLabel(name);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public Builder withTableName(String tableName)
+ {
+ processMetaData.setTableName(tableName);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for icon
+ **
+ *******************************************************************************/
+ public Builder withIcon(QIcon icon)
+ {
+ processMetaData.setIcon(icon);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Builder withReviewStepRecordFields(List fieldList)
+ {
+ QFrontendStepMetaData reviewStep = processMetaData.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW);
+ for(QFieldMetaData fieldMetaData : fieldList)
+ {
+ reviewStep.withRecordListField(fieldMetaData);
+ }
+
+ return (this);
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java
index da042f3c..31aa8744 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.mock;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@@ -64,6 +65,7 @@ public class MockBackendStep implements BackendStep
runBackendStepOutput.setValues(runBackendStepInput.getValues());
runBackendStepOutput.addValue(FIELD_MOCK_VALUE, MOCK_VALUE);
runBackendStepOutput.addValue("noOfPeopleGreeted", runBackendStepInput.getRecords().size());
+ runBackendStepOutput.addValue(RunProcessAction.BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD, true);
if("there".equalsIgnoreCase(runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX)))
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java
index 40525957..ca0525fd 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java
@@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@@ -46,6 +47,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
*******************************************************************************/
public class ExecuteReportStep implements BackendStep
{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
@@ -70,10 +75,9 @@ public class ExecuteReportStep implements BackendStep
new GenerateReportAction().execute(reportInput);
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault());
- String datePart = formatter.format(Instant.now());
+ String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, report);
- runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx");
+ runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + ".xlsx");
runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath());
}
}
@@ -82,4 +86,26 @@ public class ExecuteReportStep implements BackendStep
throw (new QException("Error running report", e));
}
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, QReportMetaData report)
+ {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault());
+ String datePart = formatter.format(Instant.now());
+
+ String downloadFileBaseName = runBackendStepInput.getValueString("downloadFileBaseName");
+ if(!StringUtils.hasContent(downloadFileBaseName))
+ {
+ downloadFileBaseName = report.getLabel();
+ }
+
+ downloadFileBaseName = downloadFileBaseName.replaceAll("/", "-");
+
+ return (downloadFileBaseName + " - " + datePart);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java
new file mode 100644
index 00000000..696c2133
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java
@@ -0,0 +1,124 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.reports;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+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.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+
+
+/*******************************************************************************
+ ** Version of PrepareReportStep for a report that runs off a single record.
+ *******************************************************************************/
+public class PrepareReportForRecordStep extends PrepareReportStep
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ super.run(runBackendStepInput, runBackendStepOutput);
+
+ //////////////////////////////////////////////////////////////////////////////////
+ // look for the recordId having been posted to the process - error if not found //
+ //////////////////////////////////////////////////////////////////////////////////
+ Serializable recordId = null;
+ if("recordIds".equals(runBackendStepInput.getValueString("recordsParam")))
+ {
+ String recordIdsString = runBackendStepInput.getValueString("recordIds");
+ String[] recordIdsArray = recordIdsString.split(",");
+ if(recordIdsArray.length != 1)
+ {
+ throw (new QUserFacingException("Exactly 1 record must be selected as input to this report."));
+ }
+
+ recordId = recordIdsArray[0];
+ }
+ else
+ {
+ throw (new QUserFacingException("No record was selected as input to this report."));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // look for the recordI input field on the process - put the input recordId in that field. //
+ // then remove that input field from the process's inputFieldList //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ @SuppressWarnings("unchecked")
+ ArrayList inputFieldList = (ArrayList) runBackendStepOutput.getValue("inputFieldList");
+ if(CollectionUtils.nullSafeHasContents(inputFieldList))
+ {
+ Iterator inputFieldListIterator = inputFieldList.iterator();
+ while(inputFieldListIterator.hasNext())
+ {
+ QFieldMetaData fieldMetaData = inputFieldListIterator.next();
+ if(fieldMetaData.getName().equals(RunReportForRecordProcess.FIELD_RECORD_ID))
+ {
+ runBackendStepOutput.addValue(RunReportForRecordProcess.FIELD_RECORD_ID, recordId);
+ inputFieldListIterator.remove();
+ runBackendStepOutput.addValue("inputFieldList", inputFieldList);
+ break;
+ }
+ }
+ }
+
+ GetInput getInput = new GetInput(runBackendStepInput.getInstance());
+ getInput.setSession(runBackendStepInput.getSession());
+ getInput.setTableName(runBackendStepInput.getTableName());
+ getInput.setPrimaryKey(recordId);
+ getInput.setShouldGenerateDisplayValues(true);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ QRecord record = getOutput.getRecord();
+ if(record == null)
+ {
+ throw (new QUserFacingException("The selected record for the report was not found."));
+ }
+
+ String reportName = runBackendStepInput.getValueString("reportName");
+ QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName);
+ // runBackendStepOutput.addValue("downloadFileBaseName", runBackendStepInput.getTable().getLabel() + " " + record.getRecordLabel());
+ runBackendStepOutput.addValue("downloadFileBaseName", report.getLabel() + " - " + record.getRecordLabel());
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // if there are no more input fields, then remove the INPUT step from the process. //
+ /////////////////////////////////////////////////////////////////////////////////////
+ inputFieldList = (ArrayList) runBackendStepOutput.getValue("inputFieldList");
+ if(!CollectionUtils.nullSafeHasContents(inputFieldList))
+ {
+ removeInputStepFromProcess(runBackendStepOutput);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java
index a80af740..47c056b4 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java
@@ -43,6 +43,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*******************************************************************************/
public class PrepareReportStep implements BackendStep
{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
@@ -68,12 +72,22 @@ public class PrepareReportStep implements BackendStep
}
else
{
- //////////////////////////////////////////////////////////////
- // no input? re-route the process to skip the input screen //
- //////////////////////////////////////////////////////////////
- List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
- stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT));
- runBackendStepOutput.getProcessState().setStepList(stepList);
+ removeInputStepFromProcess(runBackendStepOutput);
}
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected void removeInputStepFromProcess(RunBackendStepOutput runBackendStepOutput)
+ {
+ //////////////////////////////////////////////////////////////
+ // no input? re-route the process to skip the input screen //
+ //////////////////////////////////////////////////////////////
+ List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
+ stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT));
+ runBackendStepOutput.getProcessState().setStepList(stepList);
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java
new file mode 100644
index 00000000..cd4c5aca
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java
@@ -0,0 +1,167 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.reports;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
+
+
+/*******************************************************************************
+ ** Definition for Basic process to run a report.
+ *******************************************************************************/
+public class RunReportForRecordProcess
+{
+ public static final String PROCESS_NAME = "reports.forRecord";
+
+ public static final String STEP_NAME_PREPARE = "prepare";
+ public static final String STEP_NAME_INPUT = "input";
+ public static final String STEP_NAME_EXECUTE = "execute";
+ public static final String STEP_NAME_ACCESS = "accessReport";
+
+ public static final String FIELD_REPORT_NAME = "reportName";
+ public static final String FIELD_RECORD_ID = "recordId";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Builder processMetaDataBuilder()
+ {
+ return (new Builder(defineProcessMetaData()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static QProcessMetaData defineProcessMetaData()
+ {
+ QStepMetaData prepareStep = new QBackendStepMetaData()
+ .withName(STEP_NAME_PREPARE)
+ .withCode(new QCodeReference(PrepareReportForRecordStep.class))
+ .withInputData(new QFunctionInputMetaData()
+ .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING))
+ .withField(new QFieldMetaData(FIELD_RECORD_ID, QFieldType.STRING)));
+
+ QStepMetaData inputStep = new QFrontendStepMetaData()
+ .withName(STEP_NAME_INPUT)
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
+
+ QStepMetaData executeStep = new QBackendStepMetaData()
+ .withName(STEP_NAME_EXECUTE)
+ .withCode(new QCodeReference(ExecuteReportStep.class))
+ .withInputData(new QFunctionInputMetaData()
+ .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING)));
+
+ QStepMetaData accessStep = new QFrontendStepMetaData()
+ .withName(STEP_NAME_ACCESS)
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM));
+ // .withViewField(new QFieldMetaData("outputFile", QFieldType.STRING))
+ // .withViewField(new QFieldMetaData("message", QFieldType.STRING));
+
+ return new QProcessMetaData()
+ .withName(PROCESS_NAME)
+ .addStep(prepareStep)
+ .addStep(inputStep)
+ .addStep(executeStep)
+ .addStep(accessStep);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class Builder extends AbstractProcessMetaDataBuilder
+ {
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Builder(QProcessMetaData processMetaData)
+ {
+ super(processMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ **
+ *******************************************************************************/
+ public Builder withProcessName(String name)
+ {
+ processMetaData.setName(name);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public Builder withTableName(String tableName)
+ {
+ processMetaData.setTableName(tableName);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for icon
+ **
+ *******************************************************************************/
+ public Builder withIcon(QIcon icon)
+ {
+ processMetaData.setIcon(icon);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for reportName
+ **
+ *******************************************************************************/
+ public Builder withReportName(String reportName)
+ {
+ setInputFieldDefaultValue(RunReportForRecordProcess.FIELD_REPORT_NAME, reportName);
+ return (this);
+ }
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
new file mode 100644
index 00000000..7962ec68
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
@@ -0,0 +1,269 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.utils;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.ListingHash;
+
+
+/*******************************************************************************
+ ** Utility methods for working with QQQ records and table actions inside user -
+ ** defined QQQ processes steps.
+ *******************************************************************************/
+public class GeneralProcessUtils
+{
+
+ /*******************************************************************************
+ ** For a list of sourceRecords,
+ ** lookup records in the foreignTableName,
+ ** that have their foreignTablePrimaryKeyName in the sourceTableForeignKeyFieldName on the sourceRecords.
+ **
+ ** e.g., for a list of orders (with a clientId field), build a map of client.id => client record
+ ** via getForeignRecordMap(input, orderList, "clientId", "client", "id")
+ *******************************************************************************/
+ public static Map getForeignRecordMap(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException
+ {
+ Map foreignRecordMap = new HashMap<>();
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(foreignTableName);
+ List foreignIds = new ArrayList<>(sourceRecords.stream().map(r -> r.getValue(sourceTableForeignKeyFieldName)).toList());
+
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(foreignTablePrimaryKeyName, QCriteriaOperator.IN, foreignIds)));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ for(QRecord foreignRecord : queryOutput.getRecords())
+ {
+ foreignRecordMap.put(foreignRecord.getValue(foreignTablePrimaryKeyName), foreignRecord);
+ }
+ return foreignRecordMap;
+ }
+
+
+
+ /*******************************************************************************
+ ** For a list of sourceRecords,
+ ** lookup records in the foreignTableName,
+ ** that have their foreignTableForeignKeyName in the sourceTableForeignKeyFieldName on the sourceRecords.
+ **
+ ** e.g., for a list of orders, build a ListingHash of order.id => List(OrderLine records)
+ ** via getForeignRecordListingHashMap(input, orderList, "id", "orderLine", "orderId")
+ *******************************************************************************/
+ public static ListingHash getForeignRecordListingHashMap(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTableForeignKeyName) throws QException
+ {
+ ListingHash foreignRecordMap = new ListingHash<>();
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(foreignTableName);
+ List foreignIds = new ArrayList<>(sourceRecords.stream().map(r -> r.getValue(sourceTableForeignKeyFieldName)).toList());
+
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(foreignTableForeignKeyName, QCriteriaOperator.IN, foreignIds)));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ for(QRecord foreignRecord : queryOutput.getRecords())
+ {
+ foreignRecordMap.add(foreignRecord.getValue(foreignTableForeignKeyName), foreignRecord);
+ }
+ return foreignRecordMap;
+ }
+
+
+
+ /*******************************************************************************
+ ** For a list of sourceRecords,
+ ** lookup records in the foreignTableName,
+ ** that have their foreignTablePrimaryKeyName in the sourceTableForeignKeyFieldName on the sourceRecords.
+ ** and set those foreign records as a value in the sourceRecords.
+ **
+ ** e.g., for a list of orders (with a clientId field), setValue("client", QRecord(client));
+ ** via addForeignRecordsToRecordList(input, orderList, "clientId", "client", "id")
+ *******************************************************************************/
+ public static void addForeignRecordsToRecordList(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException
+ {
+ Map foreignRecordMap = getForeignRecordMap(parentActionInput, sourceRecords, sourceTableForeignKeyFieldName, foreignTableName, foreignTablePrimaryKeyName);
+ for(QRecord sourceRecord : sourceRecords)
+ {
+ QRecord foreignRecord = foreignRecordMap.get(sourceRecord.getValue(sourceTableForeignKeyFieldName));
+ sourceRecord.setValue(foreignTableName, foreignRecord);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** For a list of sourceRecords,
+ ** lookup records in the foreignTableName,
+ ** that have their foreignTableForeignKeyName in the sourceTableForeignKeyFieldName on the sourceRecords.
+ **
+ ** e.g., for a list of orders, setValue("orderLine", List(QRecord(orderLine)))
+ ** via addForeignRecordsListToRecordList(input, orderList, "id", "orderLine", "orderId")
+ *******************************************************************************/
+ public static void addForeignRecordsListToRecordList(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTableForeignKeyName) throws QException
+ {
+ ListingHash foreignRecordMap = getForeignRecordListingHashMap(parentActionInput, sourceRecords, sourceTableForeignKeyFieldName, foreignTableName, foreignTableForeignKeyName);
+ for(QRecord sourceRecord : sourceRecords)
+ {
+ List foreignRecordList = foreignRecordMap.get(sourceRecord.getValue(sourceTableForeignKeyFieldName));
+ if(foreignRecordList != null)
+ {
+ if(foreignRecordList instanceof Serializable s)
+ {
+ sourceRecord.setValue(foreignTableName, s);
+ }
+ else
+ {
+ sourceRecord.setValue(foreignTableName, new ArrayList<>(foreignRecordList));
+ }
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Run a query on tableName, for where fieldName equals fieldValue, and return
+ ** the list of QRecords.
+ *******************************************************************************/
+ public static List getRecordListByField(AbstractActionInput parentActionInput, String tableName, String fieldName, Serializable fieldValue) throws QException
+ {
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(tableName);
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, List.of(fieldValue))));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ return (queryOutput.getRecords());
+ }
+
+
+
+ /*******************************************************************************
+ ** Query to get one record by a unique key value. That field can be the primary
+ ** key, or any other field on the table. Note, if multiple rows do match the value,
+ ** only 1 (determined in an unspecified way) is returned.
+ *******************************************************************************/
+ public static Optional getRecordById(AbstractActionInput parentActionInput, String tableName, String fieldName, Serializable fieldValue) throws QException
+ {
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(tableName);
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, List.of(fieldValue))));
+ queryInput.setLimit(1);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ return (queryOutput.getRecords().stream().findFirst());
+ }
+
+
+
+ /*******************************************************************************
+ ** Load all rows from a table.
+ **
+ ** Note, this is inherently unsafe, if you were to call it on a table with
+ ** too many rows... Caveat emptor.
+ *******************************************************************************/
+ public static List loadTable(AbstractActionInput parentActionInput, String tableName) throws QException
+ {
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(tableName);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ return (queryOutput.getRecords());
+ }
+
+
+
+ /*******************************************************************************
+ ** Load all rows from a table, 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.
+ **
+ ** Also, note, this is inherently unsafe, if you were to call it on a table with
+ ** too many rows... Caveat emptor.
+ *******************************************************************************/
+ public static Map loadTableToMap(AbstractActionInput parentActionInput, String tableName, String keyFieldName) throws QException
+ {
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(tableName);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ List records = queryOutput.getRecords();
+
+ Map map = new HashMap<>();
+ for(QRecord record : records)
+ {
+ Serializable value = record.getValue(keyFieldName);
+ if(value != null)
+ {
+ map.put(value, record);
+ }
+ }
+ return (map);
+ }
+
+
+
+ /*******************************************************************************
+ ** Load all rows from a table, into a ListingHash, keyed by the keyFieldName.
+ **
+ ** Note - null values from the key field are NOT put in the map.
+ **
+ ** The ordering of the records is not specified.
+ **
+ ** Also, note, this is inherently unsafe, if you were to call it on a table with
+ ** too many rows... Caveat emptor.
+ *******************************************************************************/
+ public static ListingHash loadTableToListingHash(AbstractActionInput parentActionInput, String tableName, String keyFieldName) throws QException
+ {
+ QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
+ queryInput.setSession(parentActionInput.getSession());
+ queryInput.setTableName(tableName);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ List records = queryOutput.getRecords();
+
+ ListingHash map = new ListingHash<>();
+ for(QRecord record : records)
+ {
+ Serializable value = record.getValue(keyFieldName);
+ if(value != null)
+ {
+ map.add(value, record);
+ }
+ }
+ return (map);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java
index 88f01258..919526f2 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java
@@ -23,9 +23,10 @@ package com.kingsrook.qqq.backend.core.utils;
import java.io.IOException;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -230,34 +231,86 @@ public class JsonUtils
** Convert a json object into a QRecord
**
*******************************************************************************/
- public static QRecord parseQRecord(JSONObject jsonObject, Map fields)
+ public static QRecord parseQRecord(JSONObject jsonObject, Map fields, boolean useBackendFieldNames)
{
QRecord record = new QRecord();
+
+ FIELDS_LOOP:
for(String fieldName : fields.keySet())
{
- QFieldMetaData metaData = fields.get(fieldName);
- String backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName;
- switch(metaData.getType())
+ String originalBackendName = null;
+ try
{
- case INTEGER -> record.setValue(fieldName, jsonObject.optInt(backendName));
- case DECIMAL -> record.setValue(fieldName, jsonObject.optBigDecimal(backendName, null));
- case BOOLEAN -> record.setValue(fieldName, jsonObject.optBoolean(backendName));
- case DATE_TIME ->
+ QFieldMetaData metaData = fields.get(fieldName);
+ String backendName = fieldName;
+ if(useBackendFieldNames)
{
- String dateTimeString = jsonObject.optString(backendName);
- if(StringUtils.hasContent(dateTimeString))
+ backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName;
+ }
+
+ originalBackendName = backendName;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the field backend name has dots in it, interpret that to mean traversal down sub-objects //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ JSONObject jsonObjectToUse = jsonObject;
+ if(backendName.contains("."))
+ {
+ ArrayList levels = new ArrayList<>(List.of(backendName.split("\\.")));
+ backendName = levels.remove(levels.size() - 1);
+
+ for(String level : levels)
{
try
{
- record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_ZONED_DATE_TIME));
+ jsonObjectToUse = jsonObjectToUse.optJSONObject(level);
+ if(jsonObjectToUse == null)
+ {
+ continue FIELDS_LOOP;
+ }
}
- catch(DateTimeParseException dtpe1)
+ catch(Exception e)
{
- record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME));
+ continue FIELDS_LOOP;
}
}
}
- default -> record.setValue(fieldName, jsonObject.optString(backendName));
+
+ if(jsonObjectToUse.isNull(backendName))
+ {
+ record.setValue(fieldName, null);
+ continue;
+ }
+
+ switch(metaData.getType())
+ {
+ case INTEGER -> record.setValue(fieldName, jsonObjectToUse.optInt(backendName));
+ case DECIMAL -> record.setValue(fieldName, jsonObjectToUse.optBigDecimal(backendName, null));
+ case BOOLEAN -> record.setValue(fieldName, jsonObjectToUse.optBoolean(backendName));
+ case DATE_TIME ->
+ {
+ String dateTimeString = jsonObjectToUse.optString(backendName);
+ if(StringUtils.hasContent(dateTimeString))
+ {
+ Instant instant = ValueUtils.getValueAsInstant(dateTimeString);
+ record.setValue(fieldName, instant);
+ }
+ }
+ case DATE ->
+ {
+ String dateString = jsonObjectToUse.optString(backendName);
+ if(StringUtils.hasContent(dateString))
+ {
+ LocalDate localDate = ValueUtils.getValueAsLocalDate(dateString);
+ record.setValue(fieldName, localDate);
+ }
+ }
+ default -> record.setValue(fieldName, jsonObjectToUse.optString(backendName));
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.debug("Caught exception parsing field [" + fieldName + "] as [" + originalBackendName + "]", e);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
index 12a6fbff..022c6541 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
@@ -30,6 +30,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Calendar;
@@ -498,6 +499,22 @@ public class ValueUtils
}
else
{
+ try
+ {
+ return LocalDateTime.parse(s, DateTimeFormatter.ISO_ZONED_DATE_TIME).toInstant(ZoneOffset.UTC);
+ }
+ catch(DateTimeParseException e2)
+ {
+ try
+ {
+ return LocalDateTime.parse(s, DateTimeFormatter.ISO_DATE_TIME).toInstant(ZoneOffset.UTC);
+ }
+ catch(Exception e3)
+ {
+ // just throw the original
+ }
+ }
+
throw (e);
}
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java
index 692aac89..646d7979 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java
@@ -23,19 +23,27 @@ package com.kingsrook.qqq.backend.core.actions.processes;
import java.io.Serializable;
+import java.time.DayOfWeek;
+import java.time.Instant;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
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.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@@ -47,6 +55,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
@@ -70,6 +79,80 @@ public class RunProcessTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testBasepull() throws QException
+ {
+ TestCallback callback = new TestCallback();
+ RunProcessInput request = new RunProcessInput(TestUtils.defineInstance());
+ request.setSession(TestUtils.getMockSession());
+ request.setProcessName(TestUtils.PROCESS_NAME_BASEPULL);
+ request.setCallback(callback);
+ RunProcessOutput result = new RunProcessAction().execute(request);
+ assertNotNull(result);
+
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // get the last run time and 'this' run time - because the definition states that if no //
+ // rows found, the last runtime timestamp should be for 24 hours ago //
+ //////////////////////////////////////////////////////////////////////////////////////////
+ Instant lastRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_LAST_RUNTIME_KEY);
+ Instant thisRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY);
+ assertTrue(thisRunTime.isAfter(lastRunTime), "new run time should be after last run time.");
+
+ DayOfWeek lastRunTimeDayOfWeek = lastRunTime.atZone(ZoneId.systemDefault()).getDayOfWeek();
+ DayOfWeek thisRunTimeDayOfWeek = thisRunTime.atZone(ZoneId.systemDefault()).getDayOfWeek();
+ thisRunTimeDayOfWeek = thisRunTimeDayOfWeek.minus(1);
+ assertEquals(lastRunTimeDayOfWeek.getValue(), thisRunTimeDayOfWeek.getValue(), "last and this run times should be the same day after subtracting a day");
+
+ ///////////////////////////////////////////////
+ // make sure new stamp stored in backend too //
+ ///////////////////////////////////////////////
+ assertEquals(thisRunTime, getBasepullLastRunTime(), "last run time should be properly stored in backend");
+
+ ////////////////////////////////////////////////////
+ // run the process one more time and check values //
+ ////////////////////////////////////////////////////
+ result = new RunProcessAction().execute(request);
+ assertNotNull(result);
+
+ ////////////////////////////////
+ // this should still be after //
+ ////////////////////////////////
+ lastRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_LAST_RUNTIME_KEY);
+ thisRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY);
+ assertTrue(thisRunTime.isAfter(lastRunTime), "new run time should be after last run time.");
+
+ ///////////////////////////////////////////////
+ // make sure new stamp stored in backend too //
+ ///////////////////////////////////////////////
+ assertEquals(thisRunTime, getBasepullLastRunTime(), "last run time should be properly stored in backend");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Instant getBasepullLastRunTime() throws QException
+ {
+ QueryInput queryInput = new QueryInput(TestUtils.defineInstance());
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria()
+ .withFieldName(TestUtils.BASEPULL_KEY_FIELD_NAME)
+ .withOperator(QCriteriaOperator.EQUALS)
+ .withValues(List.of(TestUtils.PROCESS_NAME_BASEPULL))));
+ queryInput.setSession(TestUtils.getMockSession());
+ queryInput.setTableName(TestUtils.TABLE_NAME_BASEPULL);
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertNotNull(queryOutput);
+ assertEquals(1, queryOutput.getRecords().size(), "Should have one record");
+ return (ValueUtils.getValueAsInstant(queryOutput.getRecords().get(0).getValue(TestUtils.BASEPULL_LAST_RUN_TIME_FIELD_NAME)));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeActionTest.java
new file mode 100644
index 00000000..400eaf23
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeActionTest.java
@@ -0,0 +1,177 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.NoopCodeExecutionLogger;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+
+/*******************************************************************************
+ ** Unit test for ExecuteCodeAction
+ *******************************************************************************/
+class ExecuteCodeActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ @AfterEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new NoopCodeExecutionLogger());
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+ assertEquals(16, executeCodeOutput.getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private ExecuteCodeInput setupInput(QInstance qInstance, Map context, QCodeExecutionLoggerInterface executionLogger)
+ {
+ ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(qInstance);
+ executeCodeInput.setSession(new QSession());
+ executeCodeInput.setCodeReference(new QCodeReference(ScriptInJava.class, QCodeUsage.CUSTOMIZER));
+ executeCodeInput.setContext(context);
+ executeCodeInput.setExecutionLogger(executionLogger);
+ return executeCodeInput;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testLog4jLogger() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new Log4jCodeExecutionLogger());
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+ assertEquals(16, executeCodeOutput.getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTableLogger() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ new ScriptsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
+
+ ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new StoreScriptLogAndScriptLogLineExecutionLogger(1701, 1702));
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+ assertEquals(16, executeCodeOutput.getOutput());
+
+ List scriptLogRecords = TestUtils.queryTable(qInstance, "scriptLog");
+ List scriptLogLineRecords = TestUtils.queryTable(qInstance, "scriptLogLine");
+ assertEquals(1, scriptLogRecords.size());
+ assertEquals(1701, scriptLogRecords.get(0).getValueInteger("scriptId"));
+ assertEquals(1702, scriptLogRecords.get(0).getValueInteger("scriptRevisionId"));
+ assertEquals(1, scriptLogLineRecords.size());
+ assertEquals(1, scriptLogLineRecords.get(0).getValueInteger("scriptLogId"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testException()
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of(), new NoopCodeExecutionLogger());
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ assertThrows(QCodeException.class, () ->
+ {
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+ });
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class ScriptInJava implements Function, Serializable>
+ {
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Serializable apply(Map context)
+ {
+ ((QCodeExecutionLoggerInterface) context.get("logger")).log("Test a log");
+
+ int x = ValueUtils.getValueAsInteger(context.get("x"));
+ return (x * x);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptActionTest.java
new file mode 100644
index 00000000..5d708ed4
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptActionTest.java
@@ -0,0 +1,306 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+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.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+
+/*******************************************************************************
+ ** Unit test for RunAssociatedScriptAction
+ *******************************************************************************/
+class RunAssociatedScriptActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance instance = setupInstance();
+
+ insertScript(instance, 1, """
+ return "Hello";
+ """);
+
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance);
+ runAssociatedScriptInput.setSession(new QSession());
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(1)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // ok - since the core module doesn't have the javascript language support module as a dep, this action will fail - but at least we can confirm it fails with this specific exception! //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QException.class)
+ .hasRootCauseInstanceOf(ClassNotFoundException.class)
+ .hasRootCauseMessage("com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QInstance setupInstance() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+ QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER))
+ .withAssociatedScript(new AssociatedScript()
+ .withScriptTypeId(1)
+ .withFieldName("testScriptId")
+ );
+
+ new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null);
+
+ TestUtils.insertRecords(instance, table, List.of(
+ new QRecord().withValue("id", 1),
+ new QRecord().withValue("id", 2)
+ ));
+
+ TestUtils.insertRecords(instance, instance.getTable("scriptType"), List.of(
+ new QRecord().withValue("id", 1).withValue("name", "Test Script Type")
+ ));
+ return instance;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordNotFound() throws QException
+ {
+ QInstance instance = setupInstance();
+
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance);
+ runAssociatedScriptInput.setSession(new QSession());
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(-9999)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QNotFoundException.class)
+ .hasMessageMatching("The requested record.*was not found.*");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNoScriptInRecord() throws QException
+ {
+ QInstance instance = setupInstance();
+
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance);
+ runAssociatedScriptInput.setSession(new QSession());
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(1)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QNotFoundException.class)
+ .hasMessageMatching("The input record.*does not have a script specified for.*");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testBadScriptIdInRecord() throws QException
+ {
+ QInstance instance = setupInstance();
+
+ UpdateInput updateInput = new UpdateInput(instance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("testScriptId", -9998)));
+ new UpdateAction().execute(updateInput);
+
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance);
+ runAssociatedScriptInput.setSession(new QSession());
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(1)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QNotFoundException.class)
+ .hasMessageMatching("The script for record .* was not found.*");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNoCurrentScriptRevisionOnScript() throws QException
+ {
+ QInstance instance = setupInstance();
+
+ insertScript(instance, 1, """
+ return "Hello";
+ """);
+
+ GetInput getInput = new GetInput(instance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ getInput.setPrimaryKey(1);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ Integer scriptId = getOutput.getRecord().getValueInteger("testScriptId");
+
+ UpdateInput updateInput = new UpdateInput(instance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName("script");
+ updateInput.setRecords(List.of(new QRecord().withValue("id", scriptId).withValue("currentScriptRevisionId", null)));
+ new UpdateAction().execute(updateInput);
+
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance);
+ runAssociatedScriptInput.setSession(new QSession());
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(1)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QNotFoundException.class)
+ .hasMessageMatching("The script for record .* does not have a current version.*");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testBadCurrentScriptRevisionOnScript() throws QException
+ {
+ QInstance instance = setupInstance();
+
+ insertScript(instance, 1, """
+ return "Hello";
+ """);
+
+ GetInput getInput = new GetInput(instance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ getInput.setPrimaryKey(1);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ Integer scriptId = getOutput.getRecord().getValueInteger("testScriptId");
+
+ UpdateInput updateInput = new UpdateInput(instance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName("script");
+ updateInput.setRecords(List.of(new QRecord().withValue("id", scriptId).withValue("currentScriptRevisionId", 9997)));
+ new UpdateAction().execute(updateInput);
+
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance);
+ runAssociatedScriptInput.setSession(new QSession());
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(1)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QNotFoundException.class)
+ .hasMessageMatching("The current revision of the script for record .* was not found.*");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void insertScript(QInstance instance, Serializable recordId, String code) throws QException
+ {
+ StoreAssociatedScriptInput storeAssociatedScriptInput = new StoreAssociatedScriptInput(instance);
+ storeAssociatedScriptInput.setSession(new QSession());
+ storeAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ storeAssociatedScriptInput.setRecordPrimaryKey(recordId);
+ storeAssociatedScriptInput.setCode(code);
+ storeAssociatedScriptInput.setFieldName("testScriptId");
+ StoreAssociatedScriptOutput storeAssociatedScriptOutput = new StoreAssociatedScriptOutput();
+ new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput);
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptActionTest.java
new file mode 100644
index 00000000..8c0cced7
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptActionTest.java
@@ -0,0 +1,172 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+
+/*******************************************************************************
+ ** Unit test for StoreAssociatedScriptAction
+ *******************************************************************************/
+class StoreAssociatedScriptActionTest
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ @AfterEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+ QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER))
+ .withAssociatedScript(new AssociatedScript()
+ .withScriptTypeId(1)
+ .withFieldName("testScriptId")
+ )
+ .withField(new QFieldMetaData("otherScriptId", QFieldType.INTEGER))
+ .withAssociatedScript(new AssociatedScript()
+ .withScriptTypeId(2)
+ .withFieldName("otherScriptId")
+ );
+
+ new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null);
+
+ TestUtils.insertRecords(instance, table, List.of(
+ new QRecord().withValue("id", 1),
+ new QRecord().withValue("id", 2),
+ new QRecord().withValue("id", 3)
+ ));
+
+ TestUtils.insertRecords(instance, instance.getTable("scriptType"), List.of(
+ new QRecord().withValue("id", 1).withValue("name", "Test Script"),
+ new QRecord().withValue("id", 2).withValue("name", "Other Script")
+ ));
+
+ StoreAssociatedScriptInput storeAssociatedScriptInput = new StoreAssociatedScriptInput(instance);
+ storeAssociatedScriptInput.setSession(new QSession());
+ storeAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ storeAssociatedScriptInput.setRecordPrimaryKey(1);
+ storeAssociatedScriptInput.setCode("var i = 0;");
+ storeAssociatedScriptInput.setCommitMessage("Test commit");
+ storeAssociatedScriptInput.setFieldName("testScriptId");
+ StoreAssociatedScriptOutput storeAssociatedScriptOutput = new StoreAssociatedScriptOutput();
+
+ ///////////////////////////////////////////////
+ // insert 1st version of script for record 1 //
+ ///////////////////////////////////////////////
+ new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput);
+ assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "testScriptId", 1);
+ assertValueInField(instance, "script", 1, "currentScriptRevisionId", 1);
+
+ ////////////////////////////////////////////
+ // add 2nd version of script for record 1 //
+ ////////////////////////////////////////////
+ storeAssociatedScriptInput.setCode("var i = 1;");
+ storeAssociatedScriptInput.setCommitMessage("2nd commit");
+ new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput);
+ assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "testScriptId", 1);
+ assertValueInField(instance, "script", 1, "currentScriptRevisionId", 2);
+
+ ///////////////////////////////////////////////
+ // insert 1st version of script for record 3 //
+ ///////////////////////////////////////////////
+ storeAssociatedScriptInput.setRecordPrimaryKey(3);
+ storeAssociatedScriptInput.setCode("var i = 2;");
+ storeAssociatedScriptInput.setCommitMessage("First Commit here");
+ new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput);
+ assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 3, "testScriptId", 2);
+ assertValueInField(instance, "script", 2, "currentScriptRevisionId", 3);
+
+ /////////////////////////////////////
+ // make sure no script on record 2 //
+ /////////////////////////////////////
+ assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 2, "testScriptId", null);
+
+ ////////////////////////////////////
+ // add another script to record 1 //
+ ////////////////////////////////////
+ storeAssociatedScriptInput.setRecordPrimaryKey(1);
+ storeAssociatedScriptInput.setCode("var i = 3;");
+ storeAssociatedScriptInput.setCommitMessage("Other field");
+ storeAssociatedScriptInput.setFieldName("otherScriptId");
+ new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput);
+ assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "testScriptId", 1);
+ assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "otherScriptId", 3);
+ assertValueInField(instance, "script", 3, "currentScriptRevisionId", 4);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void assertValueInField(QInstance instance, String tableName, Serializable recordId, String fieldName, Serializable value) throws QException
+ {
+ GetInput getInput = new GetInput(instance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(tableName);
+ getInput.setPrimaryKey(recordId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ if(getOutput.getRecord() == null)
+ {
+ fail("Expected value [" + value + "] in field [" + fieldName + "], record [" + tableName + "][" + recordId + "], but the record wasn't found...");
+ }
+ Serializable actual = getOutput.getRecord().getValue(fieldName);
+ assertEquals(value, actual, "Expected value in field [" + fieldName + "], record [" + tableName + "][" + recordId + "]");
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionTest.java
new file mode 100644
index 00000000..7afad2de
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionTest.java
@@ -0,0 +1,52 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+
+/*******************************************************************************
+ ** Unit test for TestScriptAction
+ *******************************************************************************/
+class TestScriptActionTest
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ @Disabled("Not yet done.")
+ void test() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+ TestScriptInput input = new TestScriptInput(instance);
+ TestScriptOutput output = new TestScriptOutput();
+ new TestScriptAction().run(input, output);
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLoggerTest.java
new file mode 100644
index 00000000..dcee174b
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLoggerTest.java
@@ -0,0 +1,85 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+
+/*******************************************************************************
+ ** Unit test for BuildScriptLogAndScriptLogLineExecutionLogger
+ *******************************************************************************/
+class BuildScriptLogAndScriptLogLineExecutionLoggerTest
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+ new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null);
+ ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(instance);
+ executeCodeInput.setSession(new QSession());
+ executeCodeInput.setInput(Map.of("a", 1));
+
+ BuildScriptLogAndScriptLogLineExecutionLogger logger = new BuildScriptLogAndScriptLogLineExecutionLogger(9999, 8888);
+ logger.acceptExecutionStart(executeCodeInput);
+ logger.acceptLogLine("This is a log");
+ logger.acceptLogLine("This is also a log");
+ logger.acceptExecutionEnd(true);
+
+ QRecord scriptLog = logger.getScriptLog();
+ assertNull(scriptLog.getValueInteger("id"));
+ assertNotNull(scriptLog.getValue("startTimestamp"));
+ assertNotNull(scriptLog.getValue("endTimestamp"));
+ assertNotNull(scriptLog.getValue("runTimeMillis"));
+ assertEquals(9999, scriptLog.getValueInteger("scriptId"));
+ assertEquals(8888, scriptLog.getValueInteger("scriptRevisionId"));
+ assertEquals("{a=1}", scriptLog.getValueString("input"));
+ assertEquals("true", scriptLog.getValueString("output"));
+ assertNull(scriptLog.getValueString("exception"));
+ assertFalse(scriptLog.getValueBoolean("hadError"));
+
+ List scriptLogLineRecords = logger.getScriptLogLines();
+ assertEquals(2, scriptLogLineRecords.size());
+ QRecord scriptLogLine = scriptLogLineRecords.get(0);
+ assertNull(scriptLogLine.getValueInteger("scriptLogId"));
+ assertNotNull(scriptLogLine.getValue("timestamp"));
+ assertEquals("This is a log", scriptLogLine.getValueString("text"));
+ scriptLogLine = scriptLogLineRecords.get(1);
+ assertEquals("This is also a log", scriptLogLine.getValueString("text"));
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLoggerTest.java
new file mode 100644
index 00000000..42fb6cca
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLoggerTest.java
@@ -0,0 +1,103 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts.logging;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+
+/*******************************************************************************
+ ** Unit test for StoreScriptLogAndScriptLogLineExecutionLogger
+ *******************************************************************************/
+class StoreScriptLogAndScriptLogLineExecutionLoggerTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ @AfterEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+ new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null);
+ ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(instance);
+ executeCodeInput.setSession(new QSession());
+ executeCodeInput.setInput(Map.of("a", 1));
+
+ StoreScriptLogAndScriptLogLineExecutionLogger logger = new StoreScriptLogAndScriptLogLineExecutionLogger(9999, 8888);
+ logger.acceptExecutionStart(executeCodeInput);
+ logger.acceptLogLine("This is a log");
+ logger.acceptLogLine("This is also a log");
+ logger.acceptExecutionEnd(true);
+
+ List scriptLogRecords = TestUtils.queryTable(instance, "scriptLog");
+ assertEquals(1, scriptLogRecords.size());
+ QRecord scriptLog = scriptLogRecords.get(0);
+ assertNotNull(scriptLog.getValueInteger("id"));
+ assertNotNull(scriptLog.getValue("startTimestamp"));
+ assertNotNull(scriptLog.getValue("endTimestamp"));
+ assertNotNull(scriptLog.getValue("runTimeMillis"));
+ assertEquals(9999, scriptLog.getValueInteger("scriptId"));
+ assertEquals(8888, scriptLog.getValueInteger("scriptRevisionId"));
+ assertEquals("{a=1}", scriptLog.getValueString("input"));
+ assertEquals("true", scriptLog.getValueString("output"));
+ assertNull(scriptLog.getValueString("exception"));
+ assertFalse(scriptLog.getValueBoolean("hadError"));
+
+ List scriptLogLineRecords = TestUtils.queryTable(instance, "scriptLogLine");
+ assertEquals(2, scriptLogLineRecords.size());
+ QRecord scriptLogLine = scriptLogLineRecords.get(0);
+ assertEquals(scriptLog.getValueInteger("id"), scriptLogLine.getValueInteger("scriptLogId"));
+ assertNotNull(scriptLogLine.getValue("timestamp"));
+ assertEquals("This is a log", scriptLogLine.getValueString("text"));
+ scriptLogLine = scriptLogLineRecords.get(1);
+ assertEquals("This is also a log", scriptLogLine.getValueString("text"));
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
new file mode 100644
index 00000000..dd576194
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
@@ -0,0 +1,58 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.tables;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+/*******************************************************************************
+ ** Unit test for GetAction
+ **
+ *******************************************************************************/
+class GetActionTest
+{
+
+ /*******************************************************************************
+ ** At the core level, there isn't much that can be asserted, as it uses the
+ ** mock implementation - just confirming that all of the "wiring" works.
+ **
+ *******************************************************************************/
+ @Test
+ public void test() throws QException
+ {
+ GetInput request = new GetInput(TestUtils.defineInstance());
+ request.setSession(TestUtils.getMockSession());
+ request.setTableName("person");
+ request.setPrimaryKey(1);
+ request.setShouldGenerateDisplayValues(true);
+ request.setShouldTranslatePossibleValues(true);
+ GetOutput result = new GetAction().execute(request);
+ assertNotNull(result);
+ assertNotNull(result.getRecord());
+ }
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
index d6da4b05..2d20dbc6 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
@@ -51,8 +51,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
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.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
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;
@@ -142,9 +144,8 @@ class QInstanceValidatorTest
qInstance.setTables(null);
qInstance.setProcesses(null);
},
- "At least 1 table must be defined",
- "Unrecognized table shape for possibleValueSource shape",
- "Unrecognized processName for queue");
+ true,
+ "At least 1 table must be defined");
}
@@ -161,9 +162,8 @@ class QInstanceValidatorTest
qInstance.setTables(new HashMap<>());
qInstance.setProcesses(new HashMap<>());
},
- "At least 1 table must be defined",
- "Unrecognized table shape for possibleValueSource shape",
- "Unrecognized processName for queue");
+ true,
+ "At least 1 table must be defined");
}
@@ -569,6 +569,40 @@ class QInstanceValidatorTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFieldSectionDuplicateName()
+ {
+ QTableMetaData table = new QTableMetaData().withName("test")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id")))
+ .withSection(new QFieldSection("section1", "Section 2", new QIcon("person"), Tier.T2, List.of("name")))
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.INTEGER));
+ assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section named");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFieldSectionDuplicateLabel()
+ {
+ QTableMetaData table = new QTableMetaData().withName("test")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id")))
+ .withSection(new QFieldSection("section2", "Section 1", new QIcon("person"), Tier.T2, List.of("name")))
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.INTEGER));
+ assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section labeled");
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -680,10 +714,13 @@ class QInstanceValidatorTest
@Test
void testAppSectionsMissingLabel()
{
+ ///////////////////////////////////////////////////////////////////////////////////
+ // the enricher makes a label from the name, so, we'll just make them both null. //
+ ///////////////////////////////////////////////////////////////////////////////////
QAppMetaData app = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
- .withSection(new QAppSection("Section 1", null, new QIcon("person"), List.of("test"), null, null));
- assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label");
+ .withSection(new QAppSection(null, null, new QIcon("person"), List.of("test"), null, null));
+ assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label", "Missing a name");
}
@@ -802,6 +839,7 @@ class QInstanceValidatorTest
possibleValueSource.setOrderByFields(List.of(new QFilterOrderBy("id")));
possibleValueSource.setCustomCodeReference(new QCodeReference());
possibleValueSource.setEnumValues(null);
+ possibleValueSource.setType(QPossibleValueSourceType.ENUM);
},
"should not have a tableName",
"should not have searchFields",
@@ -828,6 +866,7 @@ class QInstanceValidatorTest
possibleValueSource.setOrderByFields(new ArrayList<>());
possibleValueSource.setCustomCodeReference(new QCodeReference());
possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test")));
+ possibleValueSource.setType(QPossibleValueSourceType.TABLE);
},
"should not have enum values",
"should not have a customCodeReference",
@@ -857,6 +896,7 @@ class QInstanceValidatorTest
possibleValueSource.setOrderByFields(List.of(new QFilterOrderBy("id")));
possibleValueSource.setCustomCodeReference(null);
possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test")));
+ possibleValueSource.setType(QPossibleValueSourceType.CUSTOM);
},
"should not have enum values",
"should not have a tableName",
@@ -1251,6 +1291,138 @@ class QInstanceValidatorTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReportName()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(null),
+ "Inconsistent naming for report");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(""),
+ "Inconsistent naming for report");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName("wrongName"),
+ "Inconsistent naming for report");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReportNoDataSources()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(null),
+ "At least 1 data source",
+ "unrecognized dataSourceName");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(new ArrayList<>()),
+ "At least 1 data source",
+ "unrecognized dataSourceName");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReportDataSourceNames()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(null),
+ "Missing name for a dataSource",
+ "unrecognized dataSourceName");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(""),
+ "Missing name for a dataSource",
+ "unrecognized dataSourceName");
+
+ assertValidationFailureReasons((qInstance) ->
+ {
+ List dataSources = new ArrayList<>(qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources());
+ dataSources.add(dataSources.get(0));
+ qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).setDataSources(dataSources);
+ },
+ "More than one dataSource with name");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReportDataSourceTables()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable("notATable"),
+ "is not a table in this instance");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(null),
+ "does not have a sourceTable");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(""),
+ "does not have a sourceTable");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReportDataSourceTablesFilter()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName(null),
+ "Missing fieldName for a criteria");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName("notAField"),
+ "is not a field in this table");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setOperator(null),
+ "Missing operator for a criteria");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy(null)),
+ "Missing fieldName for an orderBy");
+
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy("notAField")),
+ "is not a field in this table");
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReportDataSourceStaticDataSupplier()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference()),
+ "has both a sourceTable and a staticDataSupplier");
+
+ assertValidationFailureReasons((qInstance) ->
+ {
+ QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
+ dataSource.setSourceTable(null);
+ dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA, null));
+ },
+ "missing a code reference name");
+
+ assertValidationFailureReasons((qInstance) ->
+ {
+ QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
+ dataSource.setSourceTable(null);
+ dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null));
+ },
+ "is not of the expected type");
+
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryActionTest.java
new file mode 100644
index 00000000..24d7d013
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryActionTest.java
@@ -0,0 +1,271 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEnum;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for EnumerationQueryAction
+ *******************************************************************************/
+class EnumerationQueryActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testUnfilteredQuery() throws QException
+ {
+ QInstance instance = defineQInstance();
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName("statesEnum");
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(2, queryOutput.getRecords().size());
+
+ assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id"));
+ assertEquals("Missouri", queryOutput.getRecords().get(0).getValueString("name"));
+ assertEquals("MO", queryOutput.getRecords().get(0).getValueString("postalCode"));
+ assertEquals(15_000_000, queryOutput.getRecords().get(0).getValueInteger("population"));
+
+ assertEquals(2, queryOutput.getRecords().get(1).getValueInteger("id"));
+ assertEquals("Illinois", queryOutput.getRecords().get(1).getValueString("name"));
+ assertEquals("IL", queryOutput.getRecords().get(1).getValueString("postalCode"));
+ assertEquals(25_000_000, queryOutput.getRecords().get(1).getValueInteger("population"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFilteredQuery() throws QException
+ {
+ QInstance instance = defineQInstance();
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName("statesEnum");
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("population", QCriteriaOperator.GREATER_THAN, List.of(20_000_000))));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size());
+
+ assertEquals(2, queryOutput.getRecords().get(0).getValueInteger("id"));
+ assertEquals("IL", queryOutput.getRecords().get(0).getValueString("postalCode"));
+ assertEquals(25_000_000, queryOutput.getRecords().get(0).getValueInteger("population"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testQueryOrderBy() throws QException
+ {
+ QInstance instance = defineQInstance();
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName("statesEnum");
+
+ queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("name")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Illinois", "Missouri"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("name", false)));
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testQuerySkipLimit() throws QException
+ {
+ QInstance instance = defineQInstance();
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName("statesEnum");
+ queryInput.setSkip(0);
+ queryInput.setLimit(null);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setSkip(1);
+ queryInput.setLimit(null);
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setSkip(2);
+ queryInput.setLimit(null);
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of(), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setSkip(null);
+ queryInput.setLimit(1);
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Missouri"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setSkip(null);
+ queryInput.setLimit(2);
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setSkip(null);
+ queryInput.setLimit(3);
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+
+ queryInput.setSkip(null);
+ queryInput.setLimit(0);
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(List.of(), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QInstance defineQInstance()
+ {
+ QInstance instance = TestUtils.defineInstance();
+ instance.addBackend(new QBackendMetaData()
+ .withName("enum")
+ .withBackendType("enum")
+ );
+
+ instance.addTable(new QTableMetaData()
+ .withName("statesEnum")
+ .withBackendName("enum")
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.STRING))
+ .withField(new QFieldMetaData("postalCode", QFieldType.STRING))
+ .withField(new QFieldMetaData("population", QFieldType.INTEGER))
+ .withBackendDetails(new EnumerationTableBackendDetails().withEnumClass(States.class))
+ );
+ return instance;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static enum States implements QRecordEnum
+ {
+ MO(1, "Missouri", "MO", 15_000_000),
+ IL(2, "Illinois", "IL", 25_000_000);
+
+
+ private final Integer id;
+ private final String name;
+ private final String postalCode;
+ private final Integer population;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ States(int id, String name, String postalCode, int population)
+ {
+ this.id = id;
+ this.name = name;
+ this.postalCode = postalCode;
+ this.population = population;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for name
+ **
+ *******************************************************************************/
+ public String getName()
+ {
+ return name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for postalCode
+ **
+ *******************************************************************************/
+ public String getPostalCode()
+ {
+ return postalCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for population
+ **
+ *******************************************************************************/
+ public Integer getPopulation()
+ {
+ return population;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
index 3f451a6b..21e00c65 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
@@ -271,12 +271,12 @@ class MemoryBackendModuleTest
assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(2))).size());
assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(1))).size());
assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(0))).size());
+ assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of("Darin"))).size());
assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of())));
assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of())));
assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, List.of())));
assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of())));
- assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of("Bob"))));
{
////////////////////////////
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStepTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStepTest.java
new file mode 100644
index 00000000..096ebae1
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStepTest.java
@@ -0,0 +1,74 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.basepull;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for ExtractViaBasepullQueryStep
+ *******************************************************************************/
+class ExtractViaBasepullQueryStepTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ Instant timestamp = Instant.parse("1980-05-31T15:36:00Z");
+ Instant now = Instant.now();
+
+ RunBackendStepInput input = new RunBackendStepInput(TestUtils.defineInstance());
+ input.addValue(RunProcessAction.BASEPULL_TIMESTAMP_FIELD, "createDate");
+ input.addValue(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY, now);
+ input.setBasepullLastRunTime(timestamp);
+ QQueryFilter queryFilter = new ExtractViaBasepullQueryStep().getQueryFilter(input);
+
+ System.out.println(queryFilter);
+
+ assertEquals(2, queryFilter.getCriteria().size());
+ assertEquals("createDate", queryFilter.getCriteria().get(0).getFieldName());
+ assertEquals(QCriteriaOperator.GREATER_THAN, queryFilter.getCriteria().get(0).getOperator());
+ assertEquals(timestamp.toString(), queryFilter.getCriteria().get(0).getValues().get(0));
+
+ assertEquals("createDate", queryFilter.getCriteria().get(1).getFieldName());
+ assertEquals(QCriteriaOperator.LESS_THAN_OR_EQUALS, queryFilter.getCriteria().get(1).getOperator());
+ assertEquals(now.toString(), queryFilter.getCriteria().get(1).getValues().get(0));
+
+ assertEquals(1, queryFilter.getOrderBys().size());
+ assertEquals("createDate", queryFilter.getOrderBys().get(0).getFieldName());
+ assertTrue(queryFilter.getOrderBys().get(0).getIsAscending());
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java
new file mode 100644
index 00000000..57ae10cf
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java
@@ -0,0 +1,62 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.reports;
+
+
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+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.QInstance;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+
+
+/*******************************************************************************
+ ** Unit test for BasicRunReportProcess
+ *******************************************************************************/
+class RunReportForRecordProcessTest
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRunReport() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+ TestUtils.insertDefaultShapes(instance);
+
+ RunProcessInput runProcessInput = new RunProcessInput(instance);
+ runProcessInput.setSession(TestUtils.getMockSession());
+ runProcessInput.setProcessName(TestUtils.PROCESS_NAME_RUN_SHAPES_PERSON_REPORT);
+ runProcessInput.addValue(BasicRunReportProcess.FIELD_REPORT_NAME, TestUtils.REPORT_NAME_SHAPES_PERSON);
+ runProcessInput.addValue("recordsParam", "recordIds");
+ runProcessInput.addValue("recordIds", "1");
+
+ RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
+
+ // runProcessOutput = new RunProcessAction().execute(runProcessInput);
+ // assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_ACCESS);
+ // assertThat(runProcessOutput.getValues()).containsKeys("downloadFileName", "serverFilePath");
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtilsTest.java
new file mode 100644
index 00000000..ad98187a
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtilsTest.java
@@ -0,0 +1,326 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.utils;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
+import com.kingsrook.qqq.backend.core.utils.ListingHash;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for GeneralProcessUtils
+ *******************************************************************************/
+class GeneralProcessUtilsTest
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @AfterEach
+ @BeforeEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetForeignRecordMap() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("favoriteShapeId", 3),
+ new QRecord().withValue("favoriteShapeId", 1)
+ ));
+ TestUtils.insertDefaultShapes(instance);
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ Map foreignRecordMap = GeneralProcessUtils.getForeignRecordMap(queryInput, queryOutput.getRecords(), "favoriteShapeId", TestUtils.TABLE_NAME_SHAPE, "id");
+
+ assertEquals(2, foreignRecordMap.size());
+ assertEquals(1, foreignRecordMap.get(1).getValueInteger("id"));
+ assertEquals(3, foreignRecordMap.get(3).getValueInteger("id"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetForeignRecordListingHashMap() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("favoriteShapeId", 3),
+ new QRecord().withValue("id", 2).withValue("favoriteShapeId", 3),
+ new QRecord().withValue("id", 3).withValue("favoriteShapeId", 1)
+ ));
+ TestUtils.insertDefaultShapes(instance);
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ ListingHash foreignRecordListingHashMap = GeneralProcessUtils.getForeignRecordListingHashMap(queryInput, queryOutput.getRecords(), "id", TestUtils.TABLE_NAME_PERSON_MEMORY, "favoriteShapeId");
+
+ assertEquals(2, foreignRecordListingHashMap.size());
+
+ assertEquals(1, foreignRecordListingHashMap.get(1).size());
+ assertEquals(Set.of(3), foreignRecordListingHashMap.get(1).stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet()));
+
+ assertEquals(2, foreignRecordListingHashMap.get(3).size());
+ assertEquals(Set.of(1, 2), foreignRecordListingHashMap.get(3).stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAddForeignRecordsToRecordList() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("favoriteShapeId", 3),
+ new QRecord().withValue("favoriteShapeId", 1)
+ ));
+ TestUtils.insertDefaultShapes(instance);
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ GeneralProcessUtils.addForeignRecordsToRecordList(queryInput, queryOutput.getRecords(), "favoriteShapeId", TestUtils.TABLE_NAME_SHAPE, "id");
+
+ for(QRecord record : queryOutput.getRecords())
+ {
+ assertEquals(record.getValue("favoriteShapeId"), ((QRecord) record.getValue(TestUtils.TABLE_NAME_SHAPE)).getValue("id"));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAddForeignRecordsListToRecordList() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("favoriteShapeId", 3),
+ new QRecord().withValue("id", 2).withValue("favoriteShapeId", 3),
+ new QRecord().withValue("id", 3).withValue("favoriteShapeId", 1)
+ ));
+ TestUtils.insertDefaultShapes(instance);
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ GeneralProcessUtils.addForeignRecordsListToRecordList(queryInput, queryOutput.getRecords(), "id", TestUtils.TABLE_NAME_PERSON_MEMORY, "favoriteShapeId");
+
+ for(QRecord record : queryOutput.getRecords())
+ {
+ @SuppressWarnings("unchecked")
+ List foreignRecordList = (List) record.getValue(TestUtils.TABLE_NAME_PERSON_MEMORY);
+
+ if(record.getValueInteger("id").equals(3))
+ {
+ assertEquals(2, foreignRecordList.size());
+ }
+ else if(record.getValueInteger("id").equals(2))
+ {
+ assertNull(foreignRecordList);
+ continue;
+ }
+
+ for(QRecord foreignRecord : foreignRecordList)
+ {
+ assertEquals(record.getValue("id"), foreignRecord.getValue("favoriteShapeId"));
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetRecordListByField() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("favoriteShapeId", 3),
+ new QRecord().withValue("id", 2).withValue("favoriteShapeId", 3),
+ new QRecord().withValue("id", 3).withValue("favoriteShapeId", 1)
+ ));
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ List records = GeneralProcessUtils.getRecordListByField(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "favoriteShapeId", 3);
+ assertEquals(2, records.size());
+ assertTrue(records.stream().anyMatch(r -> r.getValue("id").equals(1)));
+ assertTrue(records.stream().anyMatch(r -> r.getValue("id").equals(2)));
+ assertTrue(records.stream().noneMatch(r -> r.getValue("id").equals(3)));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetRecordById() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("firstName", "Darin"),
+ new QRecord().withValue("id", 2).withValue("firstName", "James"),
+ new QRecord().withValue("id", 3).withValue("firstName", "Tim")
+ ));
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ Optional record = GeneralProcessUtils.getRecordById(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName", "James");
+ assertTrue(record.isPresent());
+ assertEquals(2, record.get().getValueInteger("id"));
+
+ record = GeneralProcessUtils.getRecordById(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName", "Bobby");
+ assertFalse(record.isPresent());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testLoadTable() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("firstName", "Darin"),
+ new QRecord().withValue("id", 2).withValue("firstName", "James"),
+ new QRecord().withValue("id", 3).withValue("firstName", "Tim")
+ ));
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ List records = GeneralProcessUtils.loadTable(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY);
+ assertEquals(3, records.size());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testLoadTableToMap() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("firstName", "Darin"),
+ new QRecord().withValue("id", 2).withValue("firstName", "James"),
+ new QRecord().withValue("id", 3).withValue("firstName", "Tim")
+ ));
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ Map recordMapById = GeneralProcessUtils.loadTableToMap(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "id");
+ assertEquals(3, recordMapById.size());
+ assertEquals("Darin", recordMapById.get(1).getValueString("firstName"));
+ assertEquals("James", recordMapById.get(2).getValueString("firstName"));
+
+ Map recordMapByFirstName = GeneralProcessUtils.loadTableToMap(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName");
+ assertEquals(3, recordMapByFirstName.size());
+ assertEquals(1, recordMapByFirstName.get("Darin").getValueInteger("id"));
+ assertEquals(3, recordMapByFirstName.get("Tim").getValueInteger("id"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testLoadTableToListingHash() throws QException
+ {
+ QInstance instance = TestUtils.defineInstance();
+
+ TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"),
+ new QRecord().withValue("id", 2).withValue("firstName", "James").withValue("lastName", "Maes"),
+ new QRecord().withValue("id", 3).withValue("firstName", "James").withValue("lastName", "Brown")
+ ));
+
+ QueryInput queryInput = new QueryInput(instance);
+ queryInput.setSession(new QSession());
+ ListingHash map = GeneralProcessUtils.loadTableToListingHash(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName");
+ assertEquals(2, map.size());
+ assertEquals(1, map.get("Darin").size());
+ assertEquals(2, map.get("James").size());
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
index 618da13e..b22f1f00 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
@@ -76,6 +76,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
@@ -87,9 +92,11 @@ import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationM
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
+import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
+import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -116,9 +123,13 @@ public class TestUtils
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
public static final String PROCESS_NAME_INCREASE_BIRTHDATE = "increaseBirthdate";
public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge";
+ public static final String PROCESS_NAME_BASEPULL = "basepullTest";
+ public static final String PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport";
public static final String TABLE_NAME_PERSON_FILE = "personFile";
public static final String TABLE_NAME_PERSON_MEMORY = "personMemory";
public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly";
+ public static final String TABLE_NAME_BASEPULL = "basepullTest";
+ public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport";
public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type
@@ -128,6 +139,9 @@ public class TestUtils
public static final String POLLING_AUTOMATION = "polling";
public static final String DEFAULT_QUEUE_PROVIDER = "defaultQueueProvider";
+ public static final String BASEPULL_KEY_FIELD_NAME = "processName";
+ public static final String BASEPULL_LAST_RUN_TIME_FIELD_NAME = "lastRunTime";
+
/*******************************************************************************
@@ -146,6 +160,7 @@ public class TestUtils
qInstance.addTable(definePersonMemoryTable());
qInstance.addTable(defineTableIdAndNameOnly());
qInstance.addTable(defineTableShape());
+ qInstance.addTable(defineTableBasepull());
qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
@@ -158,6 +173,10 @@ public class TestUtils
qInstance.addProcess(new BasicETLProcess().defineProcessMetaData());
qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData());
qInstance.addProcess(defineProcessIncreasePersonBirthdate());
+ qInstance.addProcess(defineProcessBasepull());
+
+ qInstance.addReport(defineShapesPersonsReport());
+ qInstance.addProcess(defineShapesPersonReportProcess());
qInstance.addAutomationProvider(definePollingAutomationProvider());
@@ -486,6 +505,26 @@ public class TestUtils
+ /*******************************************************************************
+ ** Define a basepullTable
+ *******************************************************************************/
+ public static QTableMetaData defineTableBasepull()
+ {
+ return (new QTableMetaData()
+ .withName(TABLE_NAME_BASEPULL)
+ .withLabel("Basepull Test")
+ .withPrimaryKeyField("id")
+ .withBackendName(MEMORY_BACKEND_NAME)
+ .withFields(TestUtils.defineTablePerson().getFields()))
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false))
+ .withField(new QFieldMetaData(BASEPULL_KEY_FIELD_NAME, QFieldType.STRING).withBackendName("process_name").withIsRequired(true))
+ .withField(new QFieldMetaData(BASEPULL_LAST_RUN_TIME_FIELD_NAME, QFieldType.DATE_TIME).withBackendName("last_run_time").withIsRequired(true));
+ }
+
+
+
/*******************************************************************************
** Define a 3nd version of the 'person' table, backed by the in-memory backend
*******************************************************************************/
@@ -732,6 +771,43 @@ public class TestUtils
+ /*******************************************************************************
+ ** Define a sample basepull process
+ *******************************************************************************/
+ private static QProcessMetaData defineProcessBasepull()
+ {
+ return new QProcessMetaData()
+ .withBasepullConfiguration(new BasepullConfiguration()
+ .withKeyField(BASEPULL_KEY_FIELD_NAME)
+ .withLastRunTimeFieldName(BASEPULL_LAST_RUN_TIME_FIELD_NAME)
+ .withHoursBackForInitialTimestamp(24)
+ .withKeyValue(PROCESS_NAME_BASEPULL)
+ .withTableName(defineTableBasepull().getName()))
+ .withName(PROCESS_NAME_BASEPULL)
+ .withTableName(TABLE_NAME_PERSON)
+ .addStep(new QBackendStepMetaData()
+ .withName("prepare")
+ .withCode(new QCodeReference()
+ .withName(MockBackendStep.class.getName())
+ .withCodeType(QCodeType.JAVA)
+ .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context?
+ .withInputData(new QFunctionInputMetaData()
+ .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON))
+ .withFieldList(List.of(
+ new QFieldMetaData("greetingPrefix", QFieldType.STRING),
+ new QFieldMetaData("greetingSuffix", QFieldType.STRING)
+ )))
+ .withOutputMetaData(new QFunctionOutputMetaData()
+ .withRecordListMetaData(new QRecordListMetaData()
+ .withTableName(TABLE_NAME_PERSON)
+ .withField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
+ )
+ .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
+ );
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -886,4 +962,52 @@ public class TestUtils
.withProcessName(PROCESS_NAME_INCREASE_BIRTHDATE));
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QReportMetaData defineShapesPersonsReport()
+ {
+ return new QReportMetaData()
+ .withName(REPORT_NAME_SHAPES_PERSON)
+ .withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT)
+ .withInputFields(List.of(
+ new QFieldMetaData(RunReportForRecordProcess.FIELD_RECORD_ID, QFieldType.INTEGER).withIsRequired(true)
+ ))
+ .withDataSources(List.of(
+ new QReportDataSource()
+ .withName("persons")
+ .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withQueryFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("favoriteShapeId", QCriteriaOperator.EQUALS, List.of("${input." + RunReportForRecordProcess.FIELD_RECORD_ID + "}")))
+ )
+ ))
+ .withViews(List.of(
+ new QReportView()
+ .withName("person")
+ .withDataSourceName("persons")
+ .withType(ReportType.TABLE)
+ .withColumns(List.of(
+ new QReportField().withName("id"),
+ new QReportField().withName("firstName"),
+ new QReportField().withName("lastName")
+ ))
+ ));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QProcessMetaData defineShapesPersonReportProcess()
+ {
+ return RunReportForRecordProcess.processMetaDataBuilder()
+ .withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT)
+ .withReportName(REPORT_NAME_SHAPES_PERSON)
+ .withTableName(TestUtils.TABLE_NAME_SHAPE)
+ .getProcessMetaData();
+ }
+
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java
index f922fcc9..4a79a30d 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.api;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
@@ -31,8 +32,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.api.actions.APICountAction;
+import com.kingsrook.qqq.backend.module.api.actions.APIGetAction;
import com.kingsrook.qqq.backend.module.api.actions.APIInsertAction;
import com.kingsrook.qqq.backend.module.api.actions.APIQueryAction;
+import com.kingsrook.qqq.backend.module.api.actions.APIUpdateAction;
/*******************************************************************************
@@ -94,6 +97,17 @@ public class APIBackendModule implements QBackendModuleInterface
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public GetInterface getGetInterface()
+ {
+ return (new APIGetAction());
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -111,7 +125,7 @@ public class APIBackendModule implements QBackendModuleInterface
@Override
public UpdateInterface getUpdateInterface()
{
- return (null); //return (new RDBMSUpdateAction());
+ return (new APIUpdateAction());
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java
index 81240b42..1433ee8b 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java
@@ -22,13 +22,11 @@
package com.kingsrook.qqq.backend.module.api.actions;
-import java.util.List;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
-import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
@@ -58,23 +56,24 @@ public class APICountAction extends AbstractAPIAction implements CountInterface
try
{
QQueryFilter filter = countInput.getFilter();
- String paramString = apiActionUtil.buildQueryString(filter, null, null, table.getFields());
+ String paramString = apiActionUtil.buildQueryStringForGet(filter, null, null, table.getFields());
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
HttpClient client = httpClientBuilder.build();
- String url = apiActionUtil.buildTableUrl(table);
- HttpGet request = new HttpGet(url + paramString);
+ String url = apiActionUtil.buildTableUrl(table) + paramString;
+ LOG.info("API URL: " + url);
+ HttpGet request = new HttpGet(url);
apiActionUtil.setupAuthorizationInRequest(request);
apiActionUtil.setupContentTypeInRequest(request);
apiActionUtil.setupAdditionalHeaders(request);
- HttpResponse response = client.execute(request);
- List queryResults = apiActionUtil.processGetResponse(table, response);
+ HttpResponse response = client.execute(request);
+ Integer count = apiActionUtil.processGetResponseForCount(table, response);
CountOutput rs = new CountOutput();
- rs.setCount(queryResults.size());
+ rs.setCount(count);
return rs;
}
catch(Exception e)
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java
new file mode 100644
index 00000000..a00c1b9d
--- /dev/null
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java
@@ -0,0 +1,83 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.api.actions;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class APIGetAction extends AbstractAPIAction implements GetInterface
+{
+ private static final Logger LOG = LogManager.getLogger(APIGetAction.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetOutput execute(GetInput getInput) throws QException
+ {
+ QTableMetaData table = getInput.getTable();
+ preAction(getInput);
+
+ try
+ {
+ String urlSuffix = apiActionUtil.buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey());
+
+ HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
+ HttpClient client = httpClientBuilder.build();
+
+ String url = apiActionUtil.buildTableUrl(table);
+ HttpGet request = new HttpGet(url + urlSuffix);
+
+ apiActionUtil.setupAuthorizationInRequest(request);
+ apiActionUtil.setupContentTypeInRequest(request);
+ apiActionUtil.setupAdditionalHeaders(request);
+
+ HttpResponse response = client.execute(request);
+ QRecord record = apiActionUtil.processSingleRecordGetResponse(table, response);
+
+ GetOutput rs = new GetOutput();
+ rs.setRecord(record);
+ return rs;
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error in API get", e);
+ throw new QException("Error executing get: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java
index d7a9aaa2..2f5cc8d8 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java
@@ -22,13 +22,11 @@
package com.kingsrook.qqq.backend.module.api.actions;
-import java.util.List;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
-import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
@@ -55,34 +53,83 @@ public class APIQueryAction extends AbstractAPIAction implements QueryInterface
QTableMetaData table = queryInput.getTable();
preAction(queryInput);
- try
+ QueryOutput queryOutput = new QueryOutput(queryInput);
+ Integer originalLimit = queryInput.getLimit();
+ Integer limit = originalLimit;
+ Integer skip = queryInput.getSkip();
+
+ if(limit == null)
{
- QQueryFilter filter = queryInput.getFilter();
- String paramString = apiActionUtil.buildQueryString(filter, queryInput.getLimit(), queryInput.getSkip(), table.getFields());
-
- HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
- HttpClient client = httpClientBuilder.build();
-
- String url = apiActionUtil.buildTableUrl(table) + paramString;
- HttpGet request = new HttpGet(url);
-
- LOG.info("API URL: " + url);
-
- apiActionUtil.setupAuthorizationInRequest(request);
- apiActionUtil.setupContentTypeInRequest(request);
- apiActionUtil.setupAdditionalHeaders(request);
-
- HttpResponse response = client.execute(request);
- List queryResults = apiActionUtil.processGetResponse(table, response);
-
- QueryOutput queryOutput = new QueryOutput(queryInput);
- queryOutput.addRecords(queryResults);
- return (queryOutput);
+ limit = apiActionUtil.getApiStandardLimit();
}
- catch(Exception e)
+
+ int totalCount = 0;
+ while(true)
{
- LOG.warn("Error in API Query", e);
- throw new QException("Error executing query: " + e.getMessage(), e);
+ try
+ {
+ QQueryFilter filter = queryInput.getFilter();
+ String paramString = apiActionUtil.buildQueryStringForGet(filter, limit, skip, table.getFields());
+
+ HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
+ HttpClient client = httpClientBuilder.build();
+
+ String url = apiActionUtil.buildTableUrl(table) + paramString;
+ LOG.info("API URL: " + url);
+
+ ///////////////////////////
+ // todo - 429 handling!! //
+ ///////////////////////////
+ HttpGet request = new HttpGet(url);
+
+ apiActionUtil.setupAuthorizationInRequest(request);
+ apiActionUtil.setupContentTypeInRequest(request);
+ apiActionUtil.setupAdditionalHeaders(request);
+
+ HttpResponse response = client.execute(request);
+
+ int count = apiActionUtil.processGetResponse(table, response, queryOutput);
+ totalCount += count;
+
+ /////////////////////////////////////////////////////////////////////////
+ // if we've fetched at least as many as the original limit, then break //
+ /////////////////////////////////////////////////////////////////////////
+ if(originalLimit != null && totalCount >= originalLimit)
+ {
+ return (queryOutput);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // if we got back less than a full page this time, then we must be done, so break //
+ ////////////////////////////////////////////////////////////////////////////////////
+ if(count == 0 || (limit != null && count < limit))
+ {
+ return (queryOutput);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // if there's an async callback that says we're cancelled, break //
+ ///////////////////////////////////////////////////////////////////
+ if(queryInput.getAsyncJobCallback().wasCancelRequested())
+ {
+ LOG.info("Breaking query job, as requested.");
+ return (queryOutput);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // else, increment the skip by the count we just got, and query for more. //
+ ////////////////////////////////////////////////////////////////////////////
+ if(skip == null)
+ {
+ skip = 0;
+ }
+ skip += count;
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error in API Query", e);
+ throw new QException("Error executing query: " + e.getMessage(), e);
+ }
}
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java
new file mode 100644
index 00000000..7d1f2a86
--- /dev/null
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java
@@ -0,0 +1,194 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.api.actions;
+
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.SleepUtils;
+import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class APIUpdateAction extends AbstractAPIAction implements UpdateInterface
+{
+ private static final Logger LOG = LogManager.getLogger(APIUpdateAction.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public UpdateOutput execute(UpdateInput updateInput) throws QException
+ {
+ UpdateOutput updateOutput = new UpdateOutput();
+ updateOutput.setRecords(new ArrayList<>());
+
+ if(CollectionUtils.nullSafeIsEmpty(updateInput.getRecords()))
+ {
+ LOG.info("Update request called with 0 records. Returning with no-op");
+ return (updateOutput);
+ }
+
+ QTableMetaData table = updateInput.getTable();
+ preAction(updateInput);
+
+ HttpClientConnectionManager connectionManager = null;
+ try
+ {
+ connectionManager = new PoolingHttpClientConnectionManager();
+
+ // todo - supports bulk put?
+
+ for(QRecord record : updateInput.getRecords())
+ {
+ putRecords(updateOutput, table, connectionManager, record);
+
+ if(updateInput.getRecords().size() > 1 && apiActionUtil.getMillisToSleepAfterEveryCall() > 0)
+ {
+ SleepUtils.sleep(apiActionUtil.getMillisToSleepAfterEveryCall(), TimeUnit.MILLISECONDS);
+ }
+ }
+
+ return (updateOutput);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error in API Insert for [" + table.getName() + "]", e);
+ throw new QException("Error executing update: " + e.getMessage(), e);
+ }
+ finally
+ {
+ if(connectionManager != null)
+ {
+ connectionManager.shutdown();
+ }
+ }
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void putRecords(UpdateOutput updateOutput, QTableMetaData table, HttpClientConnectionManager connectionManager, QRecord record) throws RateLimitException
+ {
+ int sleepMillis = apiActionUtil.getInitialRateLimitBackoffMillis();
+ int rateLimitsCaught = 0;
+ while(true)
+ {
+ try
+ {
+ putOneTime(updateOutput, table, connectionManager, record);
+ return;
+ }
+ catch(RateLimitException rle)
+ {
+ rateLimitsCaught++;
+ if(rateLimitsCaught > apiActionUtil.getMaxAllowedRateLimitErrors())
+ {
+ LOG.warn("Giving up PUT to [" + table.getName() + "] after too many rate-limit errors (" + apiActionUtil.getMaxAllowedRateLimitErrors() + ")");
+ record.addError("Error: " + rle.getMessage());
+ updateOutput.addRecord(record);
+ return;
+ }
+
+ LOG.info("Caught RateLimitException [#" + rateLimitsCaught + "] PUT'ing to [" + table.getName() + "] - sleeping [" + sleepMillis + "]...");
+ SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS);
+ sleepMillis *= 2;
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void putOneTime(UpdateOutput insertOutput, QTableMetaData table, HttpClientConnectionManager connectionManager, QRecord record) throws RateLimitException
+ {
+ try
+ {
+ CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build();
+
+ String url = buildTableUrl(table);
+ url += record.getValueString("number");
+ HttpPut request = new HttpPut(url);
+ apiActionUtil.setupAuthorizationInRequest(request);
+ apiActionUtil.setupContentTypeInRequest(request);
+ apiActionUtil.setupAdditionalHeaders(request);
+
+ request.setEntity(apiActionUtil.recordToEntity(table, record));
+
+ HttpResponse response = client.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ if(statusCode == 429)
+ {
+ throw (new RateLimitException(EntityUtils.toString(response.getEntity())));
+ }
+
+ QRecord outputRecord = apiActionUtil.processPostResponse(table, record, response);
+ insertOutput.addRecord(outputRecord);
+ }
+ catch(RateLimitException rle)
+ {
+ throw (rle);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error posting to [" + table.getName() + "]", e);
+ record.addError("Error: " + e.getMessage());
+ insertOutput.addRecord(record);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected String buildTableUrl(QTableMetaData table)
+ {
+ return (backendMetaData.getBaseUrl() + "/orders/SalesOrder/");
+ }
+
+}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
index ce277999..e78fe58f 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
@@ -26,20 +26,23 @@ import java.io.IOException;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
import org.apache.http.HttpEntity;
@@ -72,7 +75,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
public long getMillisToSleepAfterEveryCall()
{
- return 0;
+ return (0);
}
@@ -82,7 +85,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
public int getInitialRateLimitBackoffMillis()
{
- return 0;
+ return (0);
}
@@ -92,7 +95,17 @@ public class BaseAPIActionUtil
*******************************************************************************/
public int getMaxAllowedRateLimitErrors()
{
- return 0;
+ return (0);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Integer getApiStandardLimit()
+ {
+ return (20);
}
@@ -101,7 +114,7 @@ public class BaseAPIActionUtil
** method to build up a query string based on a given QFilter object
**
*******************************************************************************/
- protected String buildQueryString(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException
+ protected String buildQueryStringForGet(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException
{
// todo: reasonable default action
return (null);
@@ -109,6 +122,19 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ ** Do a default query string for a single-record GET - e.g., a query for just 1 record.
+ *******************************************************************************/
+ public String buildUrlSuffixForSingleRecordGet(Serializable primaryKey) throws QException
+ {
+ QTableMetaData table = actionInput.getTable();
+ QQueryFilter filter = new QQueryFilter()
+ .withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(primaryKey)));
+ return (buildQueryStringForGet(filter, 1, 0, table.getFields()));
+ }
+
+
+
/*******************************************************************************
** As part of making a request - set up its authorization header (not just
** strictly "Authorization", but whatever is needed for auth).
@@ -205,6 +231,14 @@ public class BaseAPIActionUtil
{
JSONObject body = recordToJsonObject(table, record);
String json = body.toString();
+
+ String tablePath = getBackendDetails(table).getTablePath();
+ if(tablePath != null)
+ {
+ body = new JSONObject();
+ body.put(tablePath, new JSONObject(json));
+ json = body.toString();
+ }
LOG.debug(json);
return (new StringEntity(json));
}
@@ -248,7 +282,8 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected QRecord jsonObjectToRecord(JSONObject jsonObject, Map fields) throws IOException
{
- QRecord record = JsonUtils.parseQRecord(jsonObject, fields);
+ QRecord record = JsonUtils.parseQRecord(jsonObject, fields, true);
+ record.getBackendDetails().put(QRecord.BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT, jsonObject.toString());
return (record);
}
@@ -257,16 +292,21 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
- protected List processGetResponse(QTableMetaData table, HttpResponse response) throws IOException
+ protected int processGetResponse(QTableMetaData table, HttpResponse response, QueryOutput queryOutput) throws IOException
{
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
+ if(statusCode >= 400)
+ {
+ handleGetResponseError(table, response);
+ }
+
HttpEntity entity = response.getEntity();
String resultString = EntityUtils.toString(entity);
- List recordList = new ArrayList<>();
- if(StringUtils.hasContent(resultString))
+ int count = 0;
+ if(StringUtils.hasContent(resultString) && !resultString.equals("null"))
{
JSONArray resultList = null;
JSONObject jsonObject = null;
@@ -289,16 +329,30 @@ public class BaseAPIActionUtil
{
for(int i = 0; i < resultList.length(); i++)
{
- recordList.add(jsonObjectToRecord(resultList.getJSONObject(i), table.getFields()));
+ queryOutput.addRecord(jsonObjectToRecord(resultList.getJSONObject(i), table.getFields()));
+ count++;
}
}
else
{
- recordList.add(jsonObjectToRecord(jsonObject, table.getFields()));
+ queryOutput.addRecord(jsonObjectToRecord(jsonObject, table.getFields()));
+ count++;
}
}
- return (recordList);
+ return (count);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void handleGetResponseError(QTableMetaData table, HttpResponse response) throws IOException
+ {
+ HttpEntity entity = response.getEntity();
+ String resultString = EntityUtils.toString(entity);
+ throw new IOException("Error performing query: " + resultString);
}
@@ -308,14 +362,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected QRecord processPostResponse(QTableMetaData table, QRecord record, HttpResponse response) throws IOException
{
- int statusCode = response.getStatusLine().getStatusCode();
- LOG.debug(statusCode);
-
- HttpEntity entity = response.getEntity();
- String resultString = EntityUtils.toString(entity);
- LOG.debug(resultString);
-
- JSONObject jsonObject = JsonUtils.toJSONObject(resultString);
+ JSONObject jsonObject = getJsonObject(response);
String primaryKeyFieldName = table.getPrimaryKeyField();
String primaryKeyBackendName = getFieldBackendName(table.getField(primaryKeyFieldName));
@@ -346,6 +393,24 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected JSONObject getJsonObject(HttpResponse response) throws IOException
+ {
+ int statusCode = response.getStatusLine().getStatusCode();
+ LOG.debug(statusCode);
+
+ HttpEntity entity = response.getEntity();
+ String resultString = EntityUtils.toString(entity);
+ LOG.debug(resultString);
+
+ JSONObject jsonObject = JsonUtils.toJSONObject(resultString);
+ return jsonObject;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -418,8 +483,36 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
- protected String urlEncode(String s)
+ protected String urlEncode(Serializable s)
{
- return (URLEncoder.encode(s, StandardCharsets.UTF_8));
+ return (URLEncoder.encode(ValueUtils.getValueAsString(s), StandardCharsets.UTF_8));
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QRecord processSingleRecordGetResponse(QTableMetaData table, HttpResponse response) throws IOException
+ {
+ return (jsonObjectToRecord(getJsonObject(response), table.getFields()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Integer processGetResponseForCount(QTableMetaData table, HttpResponse response) throws IOException
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // set up a query output with a blank query input - e.g., one that isn't using a pipe. //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ QueryOutput queryOutput = new QueryOutput(new QueryInput());
+ processGetResponse(table, response, queryOutput);
+ List records = queryOutput.getRecords();
+
+ return (records == null ? null : records.size());
+ }
+
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java
index c90b7b20..773b61f8 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java
@@ -24,8 +24,10 @@ package com.kingsrook.qqq.backend.module.api.model.metadata;
import java.io.Serializable;
import java.util.HashMap;
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.api.APIBackendModule;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
@@ -379,4 +381,14 @@ public class APIBackendMetaData extends QBackendMetaData
return (this);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void performValidation(QInstanceValidator qInstanceValidator)
+ {
+ qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName());
+ }
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilder.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilder.java
new file mode 100644
index 00000000..48167586
--- /dev/null
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilder.java
@@ -0,0 +1,133 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.api.utils;
+
+
+import java.io.Serializable;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.utils.Pair;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+
+
+/*******************************************************************************
+ ** Utility for building a query string - taking care of things like:
+ ** - do I need the "?"
+ ** - do I need a "&"
+ ** - urlEncoding params (depending on which method you call: (name, value) does
+ ** encode -- (pair) does not.)
+ *******************************************************************************/
+public class QueryStringBuilder
+{
+ private List> pairs = new ArrayList<>();
+
+
+
+ /*******************************************************************************
+ ** Assumes both name and value have NOT been previous URL Encoded
+ *******************************************************************************/
+ public void addPair(String name, Serializable value)
+ {
+ String valueString = urlEncode(ValueUtils.getValueAsString(value));
+ pairs.add(new Pair<>(urlEncode(name), valueString));
+ }
+
+
+
+ /*******************************************************************************
+ ** Assumes both name and value have NOT been previous URL Encoded
+ *******************************************************************************/
+ public QueryStringBuilder withPair(String name, Serializable value)
+ {
+ addPair(name, value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Assumes both parts are already properly uri encoded
+ *******************************************************************************/
+ public void addPair(String pair)
+ {
+ String[] parts = pair.split("=", 2);
+ if(parts.length == 1)
+ {
+ pairs.add(new Pair<>(parts[0], ""));
+ }
+ else
+ {
+ pairs.add(new Pair<>(parts[0], parts[1]));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Assumes both parts are already properly uri encoded
+ *******************************************************************************/
+ public QueryStringBuilder withPair(String pair)
+ {
+ addPair(pair);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String toQueryString()
+ {
+ if(pairs.isEmpty())
+ {
+ return ("");
+ }
+
+ return ("?" + pairs.stream().map(p -> p.getA() + "=" + p.getB()).collect(Collectors.joining("&")));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return (toQueryString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String urlEncode(Serializable s)
+ {
+ return (URLEncoder.encode(ValueUtils.getValueAsString(s), StandardCharsets.UTF_8));
+ }
+
+}
diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java
index 81e4427a..16d98fc9 100644
--- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java
+++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java
@@ -32,9 +32,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.DisabledOnOs;
-import org.junit.jupiter.api.condition.OS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -43,7 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
**
*******************************************************************************/
-@DisabledOnOs(OS.LINUX)
+@Disabled // OnOs(OS.LINUX)
public class EasyPostApiTest
{
diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java
new file mode 100644
index 00000000..4ac4d52d
--- /dev/null
+++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java
@@ -0,0 +1,51 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.api.model.metadata;
+
+
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for APIBackendMetaData
+ *******************************************************************************/
+class APIBackendMetaDataTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ APIBackendMetaData apiBackendMetaData = new APIBackendMetaData()
+ .withName("test");
+ QInstanceValidator qInstanceValidator = new QInstanceValidator();
+ apiBackendMetaData.performValidation(qInstanceValidator);
+ assertEquals(1, qInstanceValidator.getErrors().size());
+ assertThat(qInstanceValidator.getErrors()).anyMatch(e -> e.contains("Missing baseUrl"));
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilderTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilderTest.java
new file mode 100644
index 00000000..6427f15c
--- /dev/null
+++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilderTest.java
@@ -0,0 +1,113 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.api.utils;
+
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for QueryStringBuilder
+ *******************************************************************************/
+class QueryStringBuilderTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testEmpty()
+ {
+ assertEquals("", new QueryStringBuilder().toQueryString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSimpleWithoutAnd()
+ {
+ QueryStringBuilder queryStringBuilder = new QueryStringBuilder();
+ queryStringBuilder.addPair("foo", 1);
+ assertEquals("?foo=1", queryStringBuilder.toQueryString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSimpleWithAnd()
+ {
+ QueryStringBuilder queryStringBuilder = new QueryStringBuilder();
+ queryStringBuilder.addPair("foo", 1);
+ queryStringBuilder.addPair("bar=2");
+ assertEquals("?foo=1&bar=2", queryStringBuilder.toQueryString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFluent()
+ {
+ assertEquals("?foo=1&bar=2", new QueryStringBuilder()
+ .withPair("foo", 1)
+ .withPair("bar=2")
+ .toQueryString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testEncoding()
+ {
+ QueryStringBuilder queryStringBuilder = new QueryStringBuilder();
+ queryStringBuilder.addPair("percent", "99%"); // % should get encoded to %25
+ queryStringBuilder.addPair("and=this%26that"); // %26 should stay as-is -- not be re-encoded.
+ assertEquals("?percent=99%25&and=this%26that", queryStringBuilder.toQueryString());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testPairWithoutValue()
+ {
+ QueryStringBuilder queryStringBuilder = new QueryStringBuilder();
+ queryStringBuilder.addPair("name1");
+ queryStringBuilder.addPair("name2=");
+ assertEquals("?name1=&name2=", queryStringBuilder.toString());
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
index 4f6d6331..94a09dbe 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
@@ -401,4 +401,14 @@ public abstract class AbstractRDBMSAction implements QActionInterface
}
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected String escapeIdentifier(String id)
+ {
+ return ("`" + id + "`");
+ }
+
}
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
index 3a568f95..92b4d752 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
@@ -57,7 +57,7 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
QTableMetaData table = countInput.getTable();
String tableName = getTableName(table);
- String sql = "SELECT count(*) as record_count FROM " + tableName;
+ String sql = "SELECT count(*) as record_count FROM " + escapeIdentifier(tableName);
QQueryFilter filter = countInput.getFilter();
List params = new ArrayList<>();
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
index 6753fe19..36efffa4 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
@@ -186,7 +186,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte
// todo sql customization - can edit sql and/or param list?
String sql = "DELETE FROM "
- + tableName
+ + escapeIdentifier(tableName)
+ " WHERE "
+ primaryKeyName + " = ?";
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
index f4a08148..9e3b8450 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
@@ -109,7 +109,7 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
{
for(List page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE))
{
- String tableName = getTableName(table);
+ String tableName = escapeIdentifier(getTableName(table));
StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES");
List params = new ArrayList<>();
int recordIndex = 0;
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
index ad5e42d2..c326f865 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
@@ -72,7 +72,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
.map(this::getColumnName)
.collect(Collectors.joining(", "));
- String sql = "SELECT " + columns + " FROM " + tableName;
+ String sql = "SELECT " + columns + " FROM " + escapeIdentifier(tableName);
QQueryFilter filter = queryInput.getFilter();
List params = new ArrayList<>();
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java
index 05026f16..0e03ba60 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java
@@ -223,7 +223,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
.map(f -> this.getColumnName(table.getField(f)) + " = ?")
.collect(Collectors.joining(", "));
- String tableName = getTableName(table);
+ String tableName = escapeIdentifier(getTableName(table));
return ("UPDATE " + tableName
+ " SET " + columns
+ " WHERE " + getColumnName(table.getField(table.getPrimaryKeyField())) + " ");
diff --git a/qqq-dev-tools/MODULE_LIST b/qqq-dev-tools/MODULE_LIST
index 8c54c8af..1d7132d4 100644
--- a/qqq-dev-tools/MODULE_LIST
+++ b/qqq-dev-tools/MODULE_LIST
@@ -2,6 +2,7 @@ qqq-backend-core
qqq-backend-module-api
qqq-backend-module-rdbms
qqq-backend-module-filesystem
+qqq-language-support-javascript
qqq-middleware-javalin
qqq-middleware-picocli
qqq-middleware-lambda
diff --git a/qqq-dev-tools/bin/reset-snapshot-deps.sh b/qqq-dev-tools/bin/reset-snapshot-deps.sh
index 77240dee..ddfe2aa1 100755
--- a/qqq-dev-tools/bin/reset-snapshot-deps.sh
+++ b/qqq-dev-tools/bin/reset-snapshot-deps.sh
@@ -9,5 +9,9 @@ CURRENT_VERSION="$(cat $QQQ_DEV_TOOLS_DIR/CURRENT-SNAPSHOT-VERSION)"
MODULE_LIST_FILE=$QQQ_DEV_TOOLS_DIR/MODULE_LIST
for artifact in $(cat $MODULE_LIST_FILE); do
- update-dep.sh $artifact ${CURRENT_VERSION}-SNAPSHOT
+ update-dep.sh $artifact ${CURRENT_VERSION}-SNAPSHOT -q
done
+
+echo
+echo git diff pom.xml
+git diff pom.xml
diff --git a/qqq-dev-tools/bin/update-all-qqq-deps.sh b/qqq-dev-tools/bin/update-all-qqq-deps.sh
index 09cad332..4e19bcd4 100755
--- a/qqq-dev-tools/bin/update-all-qqq-deps.sh
+++ b/qqq-dev-tools/bin/update-all-qqq-deps.sh
@@ -9,6 +9,14 @@ CURRENT_VERSION="$(cat $QQQ_DEV_TOOLS_DIR/CURRENT-SNAPSHOT-VERSION)"
MODULE_LIST_FILE=$QQQ_DEV_TOOLS_DIR/MODULE_LIST
for module in $(cat $MODULE_LIST_FILE); do
+ echo "Updating $module..."
version=$(get-latest-snapshot.sh $module $CURRENT_VERSION)
- update-dep.sh $module $version
+ update-dep.sh $module $version -q
done
+
+echo
+echo git diff pom.xml
+git diff pom.xml
+newVersion=$(grep -A1 qqq-backend-core pom.xml | tail -1 | sed 's/.*//;s/<\/version>.*//;s/-........\......./ snapshot/')
+echo "You might want to commit that with:"
+echo " git commit -m \"Updated qqq deps to $newVersion\" pom.xml"
diff --git a/qqq-dev-tools/bin/update-dep.sh b/qqq-dev-tools/bin/update-dep.sh
index 22b6ae7d..1fdf54d2 100755
--- a/qqq-dev-tools/bin/update-dep.sh
+++ b/qqq-dev-tools/bin/update-dep.sh
@@ -8,6 +8,11 @@
dep=$1
version=$2
+verbose=1
+if [ "$3" == "-q" ]; then
+ verbose=0;
+fi
+
if [ -z "$dep" -o -z "$version" ]; then
echo "What dependency?"
@@ -38,7 +43,12 @@ if [ $lineNo -lt $dependenciesTagLineNo ]; then
exit 0;
fi
-echo "Going to update version of $dep at line $lineNo"
+if [ "$verbose" == "1" ]; then
+ echo "Going to update version of $dep at line $lineNo"
+fi
+
gsed -i "${lineNo}s/.*$version" pom.xml
-git diff pom.xml
+if [ "$verbose" == "1" ]; then
+ git diff pom.xml
+fi
diff --git a/qqq-dev-tools/bin/xbar-circleci-latest.sh b/qqq-dev-tools/bin/xbar-circleci-latest.sh
index b44c85ab..096e28c2 100755
--- a/qqq-dev-tools/bin/xbar-circleci-latest.sh
+++ b/qqq-dev-tools/bin/xbar-circleci-latest.sh
@@ -87,7 +87,7 @@ checkBuild()
color="gray"
fi
- if [ $index -lt 1 -o $seconds -lt 300 ]; then
+ if [ $index -lt 1 -o $seconds -lt 600 ]; then
echo -n "${shortRepo}(${shortAge})${icon} "
fi
details="$details\n$repo: $jobName: $buildStatus @ $age ago | color=$color | href=$url | image=$avatarB64"
diff --git a/qqq-dev-tools/bin/xbar-latest-snapshots.sh b/qqq-dev-tools/bin/xbar-latest-snapshots.sh
index 990ad917..8ff90204 100755
--- a/qqq-dev-tools/bin/xbar-latest-snapshots.sh
+++ b/qqq-dev-tools/bin/xbar-latest-snapshots.sh
@@ -31,5 +31,6 @@ function doOne
doOne "qqq-backend-core "
doOne "qqq-backend-module-rdbms "
doOne "qqq-backend-module-filesystem "
+doOne "qqq-backend-module-api "
doOne "qqq-middleware-picocli "
doOne "qqq-middleware-javalin "
diff --git a/qqq-language-support-javascript/pom.xml b/qqq-language-support-javascript/pom.xml
new file mode 100644
index 00000000..ba2af5a4
--- /dev/null
+++ b/qqq-language-support-javascript/pom.xml
@@ -0,0 +1,90 @@
+
+
+
+
+ 4.0.0
+
+ qqq-language-support-javascript
+
+
+ com.kingsrook.qqq
+ qqq-parent-project
+ ${revision}
+
+
+
+
+
+
+ 0.10
+ 0.10
+
+
+
+
+
+ com.kingsrook.qqq
+ qqq-backend-core
+ ${revision}
+
+
+
+
+ org.openjdk.nashorn
+ nashorn-core
+ 15.4
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+ org.apache.logging.log4j
+ log4j-core
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java
new file mode 100644
index 00000000..3c28a026
--- /dev/null
+++ b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java
@@ -0,0 +1,172 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.languages.javascript;
+
+
+import javax.script.Bindings;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import org.apache.commons.lang.NotImplementedException;
+import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import org.openjdk.nashorn.internal.runtime.ECMAException;
+import org.openjdk.nashorn.internal.runtime.ParserException;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QJavaScriptExecutor implements QCodeExecutor
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
+ {
+ String code = getCode(codeReference);
+ Serializable output = runInline(code, inputContext, executionLogger);
+ return (output);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Serializable runInline(String code, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
+ {
+ new NashornScriptEngineFactory();
+ ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
+
+ //////////////////////////////////////////////
+ // setup the javascript environment/context //
+ //////////////////////////////////////////////
+ Bindings bindings = engine.createBindings();
+ bindings.putAll(inputContext);
+
+ if(!bindings.containsKey("logger"))
+ {
+ bindings.put("logger", executionLogger);
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+ // wrap the user's code in an immediately-invoked function expression //
+ ////////////////////////////////////////////////////////////////////////
+ String codeToRun = """
+ (function userDefinedFunction()
+ {
+ %s
+ })();
+ """.formatted(code);
+
+ Serializable output;
+ try
+ {
+ output = (Serializable) engine.eval(codeToRun, bindings);
+ }
+ catch(ScriptException se)
+ {
+ QCodeException qCodeException = getQCodeExceptionFromScriptException(se);
+ throw (qCodeException);
+ }
+
+ return (output);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QCodeException getQCodeExceptionFromScriptException(ScriptException se)
+ {
+ boolean isParserException = ExceptionUtils.findClassInRootChain(se, ParserException.class) != null;
+ boolean isUserThrownException = ExceptionUtils.findClassInRootChain(se, ECMAException.class) != null;
+
+ String message = se.getMessage();
+ String errorContext = null;
+ if(message != null)
+ {
+ message = message.replaceFirst(" in .*", "");
+ message = message.replaceFirst(":\\d+:\\d+", "");
+
+ if(message.contains("\n"))
+ {
+ String[] parts = message.split("\n", 2);
+ message = parts[0];
+ errorContext = parts[1];
+ }
+ }
+
+ int actualScriptLineNumber = se.getLineNumber() - 2;
+
+ String prefix = "Script Exception";
+ boolean includeColumn = true;
+ boolean includeContext = false;
+ if(isParserException)
+ {
+ prefix = "Script parser exception";
+ includeContext = true;
+ }
+ else if(isUserThrownException)
+ {
+ prefix = "Script threw an exception";
+ includeColumn = false;
+ }
+
+ QCodeException qCodeException = new QCodeException(prefix + " at line " + actualScriptLineNumber + (includeColumn ? (" column " + se.getColumnNumber()) : "") + ": " + message);
+ if(includeContext)
+ {
+ qCodeException.setContext(errorContext);
+ }
+
+ return (qCodeException);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getCode(QCodeReference codeReference)
+ {
+ if(StringUtils.hasContent(codeReference.getInlineCode()))
+ {
+ return (codeReference.getInlineCode());
+ }
+ else
+ {
+ throw (new NotImplementedException("Only inline code is implemented at this time."));
+ }
+ }
+
+}
diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java
new file mode 100644
index 00000000..192379c7
--- /dev/null
+++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java
@@ -0,0 +1,322 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.languages.javascript;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for ExecuteCodeAction
+ *******************************************************************************/
+class ExecuteCodeActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testHelloWorld() throws QException
+ {
+ ExecuteCodeInput input = new ExecuteCodeInput(TestUtils.defineInstance())
+ .withCodeReference(new QCodeReference("helloWorld.js", QCodeType.JAVA_SCRIPT, QCodeUsage.CUSTOMIZER)
+ .withInlineCode("""
+ return "Hello, " + input"""))
+ .withContext("input", "World");
+ ExecuteCodeOutput output = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(input, output);
+ assertEquals("Hello, World", output.getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSetInContextObject() throws QException
+ {
+ OneTestOutput oneTestOutput = testOne(3, """
+ var a = 1;
+ var b = 2;
+ output.setD(a + b + input.getC());
+ """);
+ assertEquals(6, oneTestOutput.testOutput().getD());
+ assertNull(oneTestOutput.executeCodeOutput().getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReturnsContextObject() throws QException
+ {
+ OneTestOutput oneTestOutput = testOne(4, """
+ var a = 1;
+ var b = 2;
+ output.setD(a + b + input.getC());
+ return (output);
+ """);
+ assertEquals(7, oneTestOutput.testOutput().getD());
+ assertTrue(oneTestOutput.executeCodeOutput().getOutput() instanceof TestOutput);
+ assertEquals(7, ((TestOutput) oneTestOutput.executeCodeOutput().getOutput()).getD());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testReturnsPrimitive() throws QException
+ {
+ OneTestOutput oneTestOutput = testOne(5, """
+ var a = 1;
+ var b = 2;
+ output.setD(a + b + input.getC());
+ return output.getD()
+ """);
+ assertEquals(8, oneTestOutput.testOutput().getD());
+ assertEquals(8, oneTestOutput.executeCodeOutput().getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testThrows() throws QException
+ {
+ String code = """
+ var a = 1;
+ var b = 2;
+ if (input.getC() === 6)
+ {
+ throw ("oh no, six!");
+ }
+ output.setD(a + b + input.getC());
+ return output.getD()
+ """;
+
+ Assertions.assertThatThrownBy(() -> testOne(6, code))
+ .isInstanceOf(QCodeException.class)
+ .hasMessageContaining("threw")
+ .hasMessageContaining("oh no, six!")
+ .hasMessageContaining("line 5:");
+
+ OneTestOutput oneTestOutput = testOne(7, code);
+ assertEquals(10, oneTestOutput.testOutput().getD());
+ assertEquals(10, oneTestOutput.executeCodeOutput().getOutput());
+
+ Assertions.assertThatThrownBy(() -> testOne(6, """
+ var a = null;
+ return a.toString();
+ """))
+ .isInstanceOf(QCodeException.class)
+ .hasMessageContaining("threw")
+ .hasMessageContaining("TypeError: null has no such function \"toString\"");
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSyntaxError() throws QException
+ {
+ Assertions.assertThatThrownBy(() -> testOne(6, """
+ var a = 1;
+ if (input.getC() === 6
+ {
+ """))
+ .isInstanceOf(QCodeException.class)
+ .hasMessageContaining("parser")
+ .hasMessageContaining("line 3 column 0");
+
+ Assertions.assertThatThrownBy(() -> testOne(6, """
+ var a = 1;
+ vr b = 2;
+ """))
+ .isInstanceOf(QCodeException.class)
+ .hasMessageContaining("parser")
+ .hasMessageContaining("line 2 column 3");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testLogs() throws QException
+ {
+ OneTestOutput oneTestOutput = testOne(5, """
+ logger.log("This is a log.");
+ """);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private OneTestOutput testOne(Integer inputValueC, String code) throws QException
+ {
+ System.out.println();
+ QInstance instance = TestUtils.defineInstance();
+
+ TestInput testInput = new TestInput();
+ testInput.setC(inputValueC);
+
+ TestOutput testOutput = new TestOutput();
+
+ ExecuteCodeInput input = new ExecuteCodeInput(instance);
+ input.setSession(new QSession());
+ input.setCodeReference(new QCodeReference("test.js", QCodeType.JAVA_SCRIPT, QCodeUsage.CUSTOMIZER).withInlineCode(code));
+ input.withContext("input", testInput);
+ input.withContext("output", testOutput);
+
+ ExecuteCodeOutput output = new ExecuteCodeOutput();
+
+ ExecuteCodeAction executeCodeAction = new ExecuteCodeAction();
+ executeCodeAction.run(input, output);
+
+ return (new OneTestOutput(output, testOutput));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private record OneTestOutput(ExecuteCodeOutput executeCodeOutput, TestOutput testOutput)
+ {
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class TestInput implements Serializable
+ {
+ private Integer c;
+
+
+
+ /*******************************************************************************
+ ** Getter for c
+ **
+ *******************************************************************************/
+ public Integer getC()
+ {
+ return c;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for c
+ **
+ *******************************************************************************/
+ public void setC(Integer c)
+ {
+ this.c = c;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return "TestInput{c=" + c + '}';
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class TestOutput implements Serializable
+ {
+ private Integer d;
+
+
+
+ /*******************************************************************************
+ ** Getter for d
+ **
+ *******************************************************************************/
+ public Integer getD()
+ {
+ return d;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for d
+ **
+ *******************************************************************************/
+ public void setD(Integer d)
+ {
+ this.d = d;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return "TestOutput{d=" + d + '}';
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java
new file mode 100644
index 00000000..8d414889
--- /dev/null
+++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java
@@ -0,0 +1,100 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.languages.javascript;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TestUtils
+{
+ public static final String DEFAULT_BACKEND_NAME = "memoryBackend";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QInstance defineInstance()
+ {
+ QInstance qInstance = new QInstance();
+ qInstance.addBackend(defineBackend());
+ qInstance.addTable(defineTablePerson());
+ qInstance.setAuthentication(defineAuthentication());
+ return (qInstance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Define the authentication used in standard tests - using 'mock' type.
+ **
+ *******************************************************************************/
+ public static QAuthenticationMetaData defineAuthentication()
+ {
+ return new QAuthenticationMetaData()
+ .withName("mock")
+ .withType(QAuthenticationType.MOCK);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QBackendMetaData defineBackend()
+ {
+ return (new QBackendMetaData()
+ .withName(DEFAULT_BACKEND_NAME)
+ .withBackendType("memory"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static QTableMetaData defineTablePerson()
+ {
+ return new QTableMetaData()
+ .withName("person")
+ .withLabel("Person")
+ .withBackendName(DEFAULT_BACKEND_NAME)
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
+ .withField(new QFieldMetaData("firstName", QFieldType.STRING))
+ .withField(new QFieldMetaData("lastName", QFieldType.STRING))
+ .withField(new QFieldMetaData("birthDate", QFieldType.DATE))
+ .withField(new QFieldMetaData("email", QFieldType.STRING));
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
index 1f1cbe7a..9af73f36 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -42,8 +42,11 @@ import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction;
+import com.kingsrook.qqq.backend.core.actions.scripts.StoreAssociatedScriptAction;
+import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptAction;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
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.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
@@ -65,14 +68,21 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
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.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@@ -85,11 +95,13 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@@ -270,6 +282,7 @@ public class QJavalinImplementation
{
get("", QJavalinImplementation::processMetaData);
});
+ get("/authentication", QJavalinImplementation::authenticationMetaData);
});
/////////////////////////
@@ -293,6 +306,11 @@ public class QJavalinImplementation
patch("", QJavalinImplementation::dataUpdate);
put("", QJavalinImplementation::dataUpdate); // todo - want different semantics??
delete("", QJavalinImplementation::dataDelete);
+
+ get("/developer", QJavalinImplementation::getRecordDeveloperMode);
+ post("/developer/associatedScript/{fieldName}", QJavalinImplementation::storeRecordAssociatedScript);
+ get("/developer/associatedScript/{fieldName}/{scriptRevisionId}/logs", QJavalinImplementation::getAssociatedScriptLogs);
+ post("/developer/associatedScript/{fieldName}/test", QJavalinImplementation::testAssociatedScript);
});
});
@@ -307,6 +325,16 @@ public class QJavalinImplementation
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void authenticationMetaData(Context context)
+ {
+ context.result(JsonUtils.toJson(qInstance.getAuthentication()));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -501,38 +529,31 @@ public class QJavalinImplementation
String tableName = context.pathParam("table");
QTableMetaData table = qInstance.getTable(tableName);
String primaryKey = context.pathParam("primaryKey");
- QueryInput queryInput = new QueryInput(qInstance);
+ GetInput getInput = new GetInput(qInstance);
- setupSession(context, queryInput);
- queryInput.setTableName(tableName);
- queryInput.setShouldGenerateDisplayValues(true);
- queryInput.setShouldTranslatePossibleValues(true);
+ setupSession(context, getInput);
+ getInput.setTableName(tableName);
+ getInput.setShouldGenerateDisplayValues(true);
+ getInput.setShouldTranslatePossibleValues(true);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
- ///////////////////////////////////////////////////////
- // setup a filter for the primaryKey = the path-pram //
- ///////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter()
- .withCriteria(new QFilterCriteria()
- .withFieldName(table.getPrimaryKeyField())
- .withOperator(QCriteriaOperator.EQUALS)
- .withValues(List.of(primaryKey))));
+ getInput.setPrimaryKey(primaryKey);
- QueryAction queryAction = new QueryAction();
- QueryOutput queryOutput = queryAction.execute(queryInput);
+ GetAction getAction = new GetAction();
+ GetOutput getOutput = getAction.execute(getInput);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
- if(queryOutput.getRecords().isEmpty())
+ if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
- context.result(JsonUtils.toJson(queryOutput.getRecords().get(0)));
+ context.result(JsonUtils.toJson(getOutput.getRecord()));
}
catch(Exception e)
{
@@ -938,6 +959,211 @@ public class QJavalinImplementation
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void getRecordDeveloperMode(Context context)
+ {
+ try
+ {
+ String tableName = context.pathParam("table");
+ QTableMetaData table = qInstance.getTable(tableName);
+ String primaryKey = context.pathParam("primaryKey");
+ GetInput getInput = new GetInput(qInstance);
+
+ setupSession(context, getInput);
+ getInput.setTableName(tableName);
+ getInput.setShouldGenerateDisplayValues(true);
+ getInput.setShouldTranslatePossibleValues(true);
+
+ // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
+ // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
+
+ getInput.setPrimaryKey(primaryKey);
+
+ GetAction getAction = new GetAction();
+ GetOutput getOutput = getAction.execute(getInput);
+
+ ///////////////////////////////////////////////////////
+ // throw a not found error if the record isn't found //
+ ///////////////////////////////////////////////////////
+ QRecord record = getOutput.getRecord();
+ if(record == null)
+ {
+ throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
+ }
+
+ Map rs = new HashMap<>();
+ rs.put("record", record);
+
+ ArrayList> associatedScripts = new ArrayList<>();
+ rs.put("associatedScripts", associatedScripts);
+
+ ///////////////////////////////////////////////////////
+ // process each associated script type for the table //
+ ///////////////////////////////////////////////////////
+ for(AssociatedScript associatedScript : CollectionUtils.nonNullList(table.getAssociatedScripts()))
+ {
+ HashMap thisScriptData = new HashMap<>();
+ associatedScripts.add(thisScriptData);
+ thisScriptData.put("associatedScript", associatedScript);
+
+ String fieldName = associatedScript.getFieldName();
+ Serializable scriptId = record.getValue(fieldName);
+ if(scriptId != null)
+ {
+ GetInput getScriptInput = new GetInput(qInstance);
+ setupSession(context, getScriptInput);
+ getScriptInput.setTableName("script");
+ getScriptInput.setPrimaryKey(scriptId);
+ GetOutput getScriptOutput = new GetAction().execute(getScriptInput);
+ if(getScriptOutput.getRecord() != null)
+ {
+ thisScriptData.put("script", getScriptOutput.getRecord());
+
+ QueryInput queryInput = new QueryInput(qInstance);
+ setupSession(context, queryInput);
+ queryInput.setTableName("scriptRevision");
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(getScriptOutput.getRecord().getValue("id"))))
+ .withOrderBy(new QFilterOrderBy("id", false))
+ );
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ thisScriptData.put("scriptRevisions", new ArrayList<>(queryOutput.getRecords()));
+ }
+ }
+ }
+
+ context.result(JsonUtils.toJson(rs));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void getAssociatedScriptLogs(Context context)
+ {
+ try
+ {
+ String scriptRevisionId = context.pathParam("scriptRevisionId");
+
+ QueryInput queryInput = new QueryInput(qInstance);
+ setupSession(context, queryInput);
+ queryInput.setTableName("scriptLog");
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, List.of(scriptRevisionId)))
+ .withOrderBy(new QFilterOrderBy("id", false))
+ );
+ queryInput.setLimit(100);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
+ {
+ GeneralProcessUtils.addForeignRecordsListToRecordList(queryInput, queryOutput.getRecords(), "id", "scriptLogLine", "scriptLogId");
+ }
+
+ Map rs = new HashMap<>();
+ rs.put("scriptLogRecords", new ArrayList<>(queryOutput.getRecords()));
+
+ context.result(JsonUtils.toJson(rs));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void storeRecordAssociatedScript(Context context)
+ {
+ try
+ {
+ StoreAssociatedScriptInput input = new StoreAssociatedScriptInput(qInstance);
+ setupSession(context, input);
+ input.setCode(context.formParam("contents"));
+ input.setCommitMessage(context.formParam("commitMessage"));
+ input.setFieldName(context.pathParam("fieldName"));
+ input.setTableName(context.pathParam("table"));
+ input.setRecordPrimaryKey(context.pathParam("primaryKey"));
+
+ StoreAssociatedScriptOutput output = new StoreAssociatedScriptOutput();
+
+ StoreAssociatedScriptAction storeAssociatedScriptAction = new StoreAssociatedScriptAction();
+ storeAssociatedScriptAction.run(input, output);
+
+ context.result(JsonUtils.toJson("OK"));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void testAssociatedScript(Context context)
+ {
+ try
+ {
+ TestScriptInput input = new TestScriptInput(qInstance);
+ setupSession(context, input);
+ input.setTableName(context.pathParam("table"));
+ input.setRecordPrimaryKey(context.pathParam("primaryKey"));
+ // context.pathParam("fieldName");
+ Map inputValues = new HashMap<>();
+ input.setInputValues(inputValues);
+
+ for(Map.Entry> entry : context.formParamMap().entrySet())
+ {
+ String key = entry.getKey();
+ String value = entry.getValue().get(0);
+
+ if(key.equals("code"))
+ {
+ input.setCode(value);
+ }
+ else if(key.equals("scriptTypeId"))
+ {
+ input.setScriptTypeId(ValueUtils.getValueAsInteger(value));
+ }
+ else
+ {
+ inputValues.put(key, value);
+ }
+ }
+
+ TestScriptOutput output = new TestScriptOutput();
+
+ new TestScriptAction().run(input, output);
+
+ //////////////////////////////////////////////////////////////
+ // todo - output to frontend - then add assertions in test. //
+ //////////////////////////////////////////////////////////////
+
+ context.result(JsonUtils.toJson("OK"));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
index 07a37be7..df3bb744 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -26,9 +26,23 @@ import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
+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.JsonUtils;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
@@ -68,7 +82,7 @@ class QJavalinImplementationTest extends QJavalinTestBase
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertTrue(jsonObject.has("tables"));
JSONObject tables = jsonObject.getJSONObject("tables");
- assertEquals(1, tables.length());
+ assertEquals(6, tables.length()); // person + 5 script tables
JSONObject personTable = tables.getJSONObject("person");
assertTrue(personTable.has("name"));
assertEquals("person", personTable.getString("name"));
@@ -616,4 +630,135 @@ class QJavalinImplementationTest extends QJavalinTestBase
assertEquals(5, jsonObject.getJSONArray("options").getJSONObject(1).getInt("id"));
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetRecordDeveloperMode() throws QException
+ {
+ UpdateInput updateInput = new UpdateInput(TestUtils.defineInstance());
+ updateInput.setSession(new QSession());
+ updateInput.setTableName("person");
+ updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("testScriptId", 47)));
+ new UpdateAction().execute(updateInput);
+
+ InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
+ insertInput.setSession(new QSession());
+ insertInput.setTableName("script");
+ insertInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("currentScriptRevisionId", 100)));
+ new InsertAction().execute(insertInput);
+
+ insertInput.setTableName("scriptRevision");
+ insertInput.setRecords(List.of(new QRecord().withValue("id", 1000).withValue("scriptId", 47).withValue("content", "var i;")));
+ new InsertAction().execute(insertInput);
+
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/1/developer").asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ System.out.println(jsonObject.toString(3));
+ assertNotNull(jsonObject);
+ assertNotNull(jsonObject.getJSONObject("record"));
+ assertEquals("Darin", jsonObject.getJSONObject("record").getJSONObject("values").getString("firstName"));
+ assertEquals("Darin", jsonObject.getJSONObject("record").getJSONObject("displayValues").getString("firstName"));
+ assertNotNull(jsonObject.getJSONArray("associatedScripts"));
+ assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0));
+ assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONArray("scriptRevisions"));
+ assertEquals("var i;", jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONArray("scriptRevisions").getJSONObject(0).getJSONObject("values").getString("content"));
+ assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("script"));
+ assertEquals(100, jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("script").getJSONObject("values").getInt("currentScriptRevisionId"));
+ assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("associatedScript"));
+ assertEquals("testScriptId", jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("associatedScript").getString("fieldName"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStoreRecordAssociatedScript() throws QException
+ {
+ InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
+ insertInput.setSession(new QSession());
+ insertInput.setTableName("scriptType");
+ insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("name", "Test")));
+ new InsertAction().execute(insertInput);
+
+ HttpResponse response = Unirest.post(BASE_URL + "/data/person/1/developer/associatedScript/testScriptId")
+ .field("contents", "var j = 0;")
+ .field("commitMessage", "Javalin Commit")
+ .asString();
+
+ QueryInput queryInput = new QueryInput(TestUtils.defineInstance());
+ queryInput.setSession(new QSession());
+ queryInput.setTableName("scriptRevision");
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("contents", QCriteriaOperator.EQUALS, List.of("var j = 0;")))
+ .withCriteria(new QFilterCriteria("commitMessage", QCriteriaOperator.EQUALS, List.of("Javalin Commit")))
+ );
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTestAssociatedScript() throws QException
+ {
+ InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
+ insertInput.setSession(new QSession());
+ insertInput.setTableName("scriptType");
+ insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("name", "Test")));
+ new InsertAction().execute(insertInput);
+
+ HttpResponse response = Unirest.post(BASE_URL + "/data/person/1/developer/associatedScript/testScriptId/test")
+ .field("code", "var j = 0;")
+ .field("scriptTypeId", "1")
+ .field("x", "47")
+ .asString();
+
+ /////////////////////////////////////////
+ // todo - assertions after implemented //
+ /////////////////////////////////////////
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetAssociatedScriptLogs() throws QException
+ {
+ InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
+ insertInput.setSession(new QSession());
+ insertInput.setTableName("scriptLog");
+ insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("output", "testOutput").withValue("scriptRevisionId", 100)));
+ new InsertAction().execute(insertInput);
+
+ insertInput.setTableName("scriptLogLine");
+ insertInput.setRecords(List.of(
+ new QRecord().withValue("scriptLogId", 1).withValue("text", "line one"),
+ new QRecord().withValue("scriptLogId", 1).withValue("text", "line two")
+ ));
+ new InsertAction().execute(insertInput);
+
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/1/developer/associatedScript/testScriptId/100/logs").asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject.getJSONArray("scriptLogRecords"));
+ assertEquals(1, jsonObject.getJSONArray("scriptLogRecords").length());
+ assertNotNull(jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values"));
+ assertEquals("testOutput", jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getString("output"));
+ assertNotNull(jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine"));
+ assertEquals(2, jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine").length());
+ assertEquals("line one", jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine").getJSONObject(0).getJSONObject("values").getString("text"));
+ assertEquals("line two", jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine").getJSONObject(1).getJSONObject("values").getString("text"));
+ }
+
}
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
index 98071fc7..89da7217 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QValueException;
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.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
@@ -47,7 +48,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
@@ -84,7 +87,7 @@ public class TestUtils
public static void primeTestDatabase() throws Exception
{
ConnectionManager connectionManager = new ConnectionManager();
- Connection connection = connectionManager.getConnection(TestUtils.defineBackend());
+ Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend());
InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
assertNotNull(primeTestDatabaseSqlStream);
List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream);
@@ -105,7 +108,7 @@ public class TestUtils
public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
{
ConnectionManager connectionManager = new ConnectionManager();
- Connection connection = connectionManager.getConnection(defineBackend());
+ Connection connection = connectionManager.getConnection(defineDefaultH2Backend());
QueryManager.executeStatement(connection, sql, resultSetProcessor);
}
@@ -119,7 +122,7 @@ public class TestUtils
{
QInstance qInstance = new QInstance();
qInstance.setAuthentication(defineAuthentication());
- qInstance.addBackend(defineBackend());
+ qInstance.addBackend(defineDefaultH2Backend());
qInstance.addTable(defineTablePerson());
qInstance.addProcess(defineProcessGreetPeople());
qInstance.addProcess(defineProcessGreetPeopleInteractive());
@@ -128,6 +131,17 @@ public class TestUtils
qInstance.addProcess(defineProcessSimpleThrow());
qInstance.addPossibleValueSource(definePossibleValueSourcePerson());
defineWidgets(qInstance);
+
+ qInstance.addBackend(defineMemoryBackend());
+ try
+ {
+ new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null);
+ }
+ catch(Exception e)
+ {
+ throw new IllegalStateException("Error adding script tables to instance");
+ }
+
return (qInstance);
}
@@ -162,7 +176,7 @@ public class TestUtils
** Define the h2 rdbms backend
**
*******************************************************************************/
- public static RDBMSBackendMetaData defineBackend()
+ public static RDBMSBackendMetaData defineDefaultH2Backend()
{
RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData()
.withVendor("h2")
@@ -176,6 +190,19 @@ public class TestUtils
+ /*******************************************************************************
+ ** Define the memory-only backend
+ **
+ *******************************************************************************/
+ public static QBackendMetaData defineMemoryBackend()
+ {
+ return new QBackendMetaData()
+ .withBackendType("memory")
+ .withName("memory");
+ }
+
+
+
/*******************************************************************************
** Define the person table
**
@@ -187,7 +214,7 @@ public class TestUtils
.withLabel("Person")
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("firstName", "lastName")
- .withBackendName(defineBackend().getName())
+ .withBackendName(defineDefaultH2Backend().getName())
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date"))
@@ -196,7 +223,11 @@ public class TestUtils
.withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name"))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
.withField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withBackendName("partner_person_id").withPossibleValueSourceName("person"))
- .withField(new QFieldMetaData("email", QFieldType.STRING));
+ .withField(new QFieldMetaData("email", QFieldType.STRING))
+ .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER).withBackendName("test_script_id"))
+ .withAssociatedScript(new AssociatedScript()
+ .withFieldName("testScriptId")
+ .withScriptTypeId(1));
}
diff --git a/qqq-middleware-javalin/src/test/resources/prime-test-database.sql b/qqq-middleware-javalin/src/test/resources/prime-test-database.sql
index 22e88d10..c0a67766 100644
--- a/qqq-middleware-javalin/src/test/resources/prime-test-database.sql
+++ b/qqq-middleware-javalin/src/test/resources/prime-test-database.sql
@@ -30,7 +30,8 @@ CREATE TABLE person
last_name VARCHAR(80) NOT NULL,
birth_date DATE,
email VARCHAR(250) NOT NULL,
- partner_person_id INT
+ partner_person_id INT,
+ test_script_id INT
);
INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com');