diff --git a/.circleci/config.yml b/.circleci/config.yml
index b783fc28..7df4090c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -79,6 +79,19 @@ commands:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
+ check_middleware_api_versions:
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - v1-dependencies-{{ checksum "pom.xml" }}
+ - run:
+ name: Build and Run ValidateApiVersions
+ command: |
+ mvn -s .circleci/mvn-settings.xml -T4 install -DskipTests
+ mvn -s .circleci/mvn-settings.xml -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
+ qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd)
+
mvn_jar_deploy:
steps:
- checkout
@@ -130,6 +143,7 @@ jobs:
## - localstack/startup
- install_java17
- mvn_verify
+ - check_middleware_api_versions
mvn_deploy:
executor: localstack/default
@@ -137,6 +151,7 @@ jobs:
## - localstack/startup
- install_java17
- mvn_verify
+ - check_middleware_api_versions
- mvn_jar_deploy
publish_asciidoc:
diff --git a/pom.xml b/pom.xml
index 89f4c2b0..56abaec9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
qqq-backend-module-rdbmsqqq-backend-module-mongodbqqq-language-support-javascript
+ qqq-openapiqqq-middleware-picocliqqq-middleware-javalinqqq-middleware-lambda
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java
index ebf26e2d..3fa50066 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java
@@ -23,11 +23,11 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.AlertData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
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.dashboard.QWidgetMetaData;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java
new file mode 100644
index 00000000..c64c8954
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java
@@ -0,0 +1,92 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.metadata;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
+import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
+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;
+
+
+/*******************************************************************************
+ ** a default implementation of MetaDataFilterInterface, that allows all the things
+ *******************************************************************************/
+public class AllowAllMetaDataFilter implements MetaDataFilterInterface
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowTable(MetaDataInput input, QTableMetaData table)
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowProcess(MetaDataInput input, QProcessMetaData process)
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowReport(MetaDataInput input, QReportMetaData report)
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowApp(MetaDataInput input, QAppMetaData app)
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget)
+ {
+ return (true);
+ }
+
+}
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 b1f6b52b..56c76928 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
@@ -28,13 +28,18 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
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.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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;
@@ -49,6 +54,7 @@ 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;
+import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
/*******************************************************************************
@@ -57,6 +63,12 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*******************************************************************************/
public class MetaDataAction
{
+ private static final QLogger LOG = QLogger.getLogger(MetaDataAction.class);
+
+ private static Memoization metaDataFilterMemoization = new Memoization<>();
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -64,10 +76,10 @@ public class MetaDataAction
{
ActionHelper.validateSession(metaDataInput);
- // todo pre-customization - just get to modify the request?
- MetaDataOutput metaDataOutput = new MetaDataOutput();
+ MetaDataOutput metaDataOutput = new MetaDataOutput();
+ Map treeNodes = new LinkedHashMap<>();
- Map treeNodes = new LinkedHashMap<>();
+ MetaDataFilterInterface filter = getMetaDataFilter();
/////////////////////////////////////
// map tables to frontend metadata //
@@ -78,6 +90,11 @@ public class MetaDataAction
String tableName = entry.getKey();
QTableMetaData table = entry.getValue();
+ if(!filter.allowTable(metaDataInput, table))
+ {
+ continue;
+ }
+
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@@ -102,6 +119,11 @@ public class MetaDataAction
String processName = entry.getKey();
QProcessMetaData process = entry.getValue();
+ if(!filter.allowProcess(metaDataInput, process))
+ {
+ continue;
+ }
+
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@@ -122,6 +144,11 @@ public class MetaDataAction
String reportName = entry.getKey();
QReportMetaData report = entry.getValue();
+ if(!filter.allowReport(metaDataInput, report))
+ {
+ continue;
+ }
+
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@@ -142,6 +169,11 @@ public class MetaDataAction
String widgetName = entry.getKey();
QWidgetMetaDataInterface widget = entry.getValue();
+ if(!filter.allowWidget(metaDataInput, widget))
+ {
+ continue;
+ }
+
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@@ -174,9 +206,19 @@ public class MetaDataAction
continue;
}
- apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput));
- treeNodes.put(appName, new AppTreeNode(app));
+ if(!filter.allowApp(metaDataInput, app))
+ {
+ continue;
+ }
+ //////////////////////////////////////
+ // build the frontend-app meta-data //
+ //////////////////////////////////////
+ QFrontendAppMetaData frontendAppMetaData = new QFrontendAppMetaData(app, metaDataOutput);
+
+ /////////////////////////////////////////
+ // add children (if they're permitted) //
+ /////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
{
for(QAppChildMetaData child : app.getChildren())
@@ -190,9 +232,42 @@ public class MetaDataAction
}
}
- apps.get(appName).addChild(new AppTreeNode(child));
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the child was filtered away, so it isn't in its corresponding map, then don't include it here //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(child instanceof QTableMetaData table && !tables.containsKey(table.getName()))
+ {
+ continue;
+ }
+ if(child instanceof QProcessMetaData process && !processes.containsKey(process.getName()))
+ {
+ continue;
+ }
+ if(child instanceof QReportMetaData report && !reports.containsKey(report.getName()))
+ {
+ continue;
+ }
+ if(child instanceof QAppMetaData childApp && !apps.containsKey(childApp.getName()))
+ {
+ // continue;
+ }
+
+ frontendAppMetaData.addChild(new AppTreeNode(child));
}
}
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the app ended up having no children, then discard it //
+ // todo - i think this was wrong, because it didn't take into account ... something nested maybe... //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getChildren()) && CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getWidgets()))
+ {
+ // LOG.debug("Discarding empty app", logPair("name", frontendAppMetaData.getName()));
+ // continue;
+ }
+
+ apps.put(appName, frontendAppMetaData);
+ treeNodes.put(appName, new AppTreeNode(app));
}
metaDataOutput.setApps(apps);
@@ -228,6 +303,33 @@ public class MetaDataAction
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private MetaDataFilterInterface getMetaDataFilter()
+ {
+ return metaDataFilterMemoization.getResult(QContext.getQInstance(), i ->
+ {
+ MetaDataFilterInterface filter = null;
+ QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter();
+ if(metaDataFilterReference != null)
+ {
+ filter = QCodeLoader.getAdHoc(MetaDataFilterInterface.class, metaDataFilterReference);
+ LOG.debug("Using new meta-data filter of type: " + filter.getClass().getSimpleName());
+ }
+
+ if(filter == null)
+ {
+ filter = new AllowAllMetaDataFilter();
+ LOG.debug("Using new default (allow-all) meta-data filter");
+ }
+
+ return (filter);
+ }).orElseThrow(() -> new QRuntimeException("Error getting metaDataFilter"));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java
new file mode 100644
index 00000000..a7abb74d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java
@@ -0,0 +1,64 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.metadata;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
+import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
+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;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface MetaDataFilterInterface
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean allowTable(MetaDataInput input, QTableMetaData table);
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean allowProcess(MetaDataInput input, QProcessMetaData process);
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean allowReport(MetaDataInput input, QReportMetaData report);
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean allowApp(MetaDataInput input, QAppMetaData app);
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget);
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
index 6f97ec1a..119df559 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
@@ -40,6 +40,7 @@ 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.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
@@ -257,11 +258,20 @@ public class RunBackendStepAction
{
runBackendStepOutput.seedFromRequest(runBackendStepInput);
- Class> codeClass = Class.forName(code.getName());
- Object codeObject = codeClass.getConstructor().newInstance();
+ Object codeObject;
+ if(code instanceof QCodeReferenceLambda> qCodeReferenceLambda)
+ {
+ codeObject = qCodeReferenceLambda.getLambda();
+ }
+ else
+ {
+ Class> codeClass = Class.forName(code.getName());
+ codeObject = codeClass.getConstructor().newInstance();
+ }
+
if(!(codeObject instanceof BackendStep backendStepCodeObject))
{
- throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep"));
+ throw (new QException("The supplied codeReference [" + code + "] is not a reference to a BackendStep"));
}
backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput);
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 ada00f9a..481201e3 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
@@ -28,6 +28,7 @@ import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
@@ -58,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
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.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
@@ -134,90 +136,11 @@ public class RunProcessAction
try
{
- String lastStepName = runProcessInput.getStartAfterStep();
-
- STEP_LOOP:
- while(true)
+ switch(Objects.requireNonNull(process.getStepFlow(), "Process [" + process.getName() + "] has a null stepFlow."))
{
- ///////////////////////////////////////////////////////////////////////////////////////////////////////
- // always refresh the step list - as any step that runs can modify it (in the process state). //
- // this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////
- List stepList = getAvailableStepList(processState, process, lastStepName);
- if(stepList.isEmpty())
- {
- break;
- }
-
- QStepMetaData step = stepList.get(0);
- lastStepName = step.getName();
-
- if(step instanceof QFrontendStepMetaData frontendStep)
- {
- ////////////////////////////////////////////////////////////////
- // Handle what to do with frontend steps, per request setting //
- ////////////////////////////////////////////////////////////////
- switch(runProcessInput.getFrontendStepBehavior())
- {
- case BREAK ->
- {
- LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
- processFrontendStepFieldDefaultValues(processState, frontendStep);
- processFrontendComponents(processState, frontendStep);
- processState.setNextStepName(step.getName());
- break STEP_LOOP;
- }
- case SKIP ->
- {
- LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
-
- //////////////////////////////////////////////////////////////////////
- // much less error prone in case this code changes in the future... //
- //////////////////////////////////////////////////////////////////////
- // noinspection UnnecessaryContinue
- continue;
- }
- case FAIL ->
- {
- LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
- throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
- }
- default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
- }
- }
- else if(step instanceof QBackendStepMetaData backendStepMetaData)
- {
- ///////////////////////
- // Run backend steps //
- ///////////////////////
- LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
- RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
-
- /////////////////////////////////////////////////////////////////////////////////////////
- // if the step returned an override lastStepName, use that to determine how we proceed //
- /////////////////////////////////////////////////////////////////////////////////////////
- if(runBackendStepOutput.getOverrideLastStepName() != null)
- {
- LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
- lastStepName = runBackendStepOutput.getOverrideLastStepName();
- }
-
- /////////////////////////////////////////////////////////////////////////////////////////////
- // similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
- /////////////////////////////////////////////////////////////////////////////////////////////
- if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
- {
- LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
- runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
- }
- }
- else
- {
- //////////////////////////////////////////////////
- // in case we have a different step type, throw //
- //////////////////////////////////////////////////
- throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
- }
+ case LINEAR -> runLinearStepLoop(process, processState, stateKey, runProcessInput, runProcessOutput);
+ case STATE_MACHINE -> runStateMachineStep(runProcessInput.getStartAfterStep(), process, processState, stateKey, runProcessInput, runProcessOutput, 0);
+ default -> throw (new QException("Unhandled process step flow: " + process.getStepFlow()));
}
///////////////////////////////////////////////////////////////////////////
@@ -259,6 +182,270 @@ public class RunProcessAction
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception
+ {
+ String lastStepName = runProcessInput.getStartAfterStep();
+
+ while(true)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ // always refresh the step list - as any step that runs can modify it (in the process state). //
+ // this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ List stepList = getAvailableStepList(processState, process, lastStepName);
+ if(stepList.isEmpty())
+ {
+ break;
+ }
+
+ QStepMetaData step = stepList.get(0);
+ lastStepName = step.getName();
+
+ if(step instanceof QFrontendStepMetaData frontendStep)
+ {
+ LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
+ if(loopTodo == LoopTodo.BREAK)
+ {
+ break;
+ }
+ }
+ else if(step instanceof QBackendStepMetaData backendStepMetaData)
+ {
+ RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // if the step returned an override lastStepName, use that to determine how we proceed //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ if(runBackendStepOutput.getOverrideLastStepName() != null)
+ {
+ LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
+ lastStepName = runBackendStepOutput.getOverrideLastStepName();
+ }
+ }
+ else
+ {
+ //////////////////////////////////////////////////
+ // in case we have a different step type, throw //
+ //////////////////////////////////////////////////
+ throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private enum LoopTodo
+ {
+ BREAK,
+ CONTINUE
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private LoopTodo prepareForFrontendStep(RunProcessInput runProcessInput, QProcessMetaData process, QFrontendStepMetaData step, ProcessState processState) throws QException
+ {
+ ////////////////////////////////////////////////////////////////
+ // Handle what to do with frontend steps, per request setting //
+ ////////////////////////////////////////////////////////////////
+ switch(runProcessInput.getFrontendStepBehavior())
+ {
+ case BREAK ->
+ {
+ LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
+ processFrontendStepFieldDefaultValues(processState, step);
+ processFrontendComponents(processState, step);
+ processState.setNextStepName(step.getName());
+ return LoopTodo.BREAK;
+ }
+ case SKIP ->
+ {
+ LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
+ return LoopTodo.CONTINUE;
+ }
+ case FAIL ->
+ {
+ LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
+ throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
+ }
+ default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void runStateMachineStep(String lastStepName, QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, int stackDepth) throws Exception
+ {
+ //////////////////////////////
+ // check for stack-overflow //
+ //////////////////////////////
+ Integer maxStateMachineProcessStepFlowStackDepth = Objects.requireNonNullElse(runProcessInput.getValueInteger("maxStateMachineProcessStepFlowStackDepth"), 20);
+ if(stackDepth > maxStateMachineProcessStepFlowStackDepth)
+ {
+ throw (new QException("StateMachine process recurred too many times (exceeded maxStateMachineProcessStepFlowStackDepth of " + maxStateMachineProcessStepFlowStackDepth + ")"));
+ }
+
+ //////////////////////////////////
+ // figure out what step to run: //
+ //////////////////////////////////
+ QStepMetaData step = null;
+ if(!StringUtils.hasContent(lastStepName))
+ {
+ ////////////////////////////////////////////////////////////////////
+ // if no lastStepName is given, start at the process's first step //
+ ////////////////////////////////////////////////////////////////////
+ if(CollectionUtils.nullSafeIsEmpty(process.getStepList()))
+ {
+ throw (new QException("Process [" + process.getName() + "] does not have a step list defined."));
+ }
+ step = process.getStepList().get(0);
+ }
+ else
+ {
+ /////////////////////////////////////
+ // else run the given lastStepName //
+ /////////////////////////////////////
+ processState.clearNextStepName();
+ step = process.getStep(lastStepName);
+ if(step == null)
+ {
+ throw (new QException("Could not find step by name [" + lastStepName + "]"));
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+ // for the flow of: //
+ // we were on a frontend step (as a sub-step of a state machine step), //
+ // and now we're here to run that state-step's backend step - //
+ // find the state-machine step containing this frontend step. //
+ /////////////////////////////////////////////////////////////////////////
+ String skipSubStepsUntil = null;
+ if(step instanceof QFrontendStepMetaData frontendStepMetaData)
+ {
+ QStateMachineStep stateMachineStep = getStateMachineStepContainingSubStep(process, frontendStepMetaData.getName());
+ if(stateMachineStep == null)
+ {
+ throw (new QException("Could not find stateMachineStep that contains last-frontend step: " + frontendStepMetaData.getName()));
+ }
+ step = stateMachineStep;
+
+ //////////////////////////////////////////////////////////////////////////////////
+ // set this flag, to know to skip this frontend step in the sub-step loop below //
+ //////////////////////////////////////////////////////////////////////////////////
+ skipSubStepsUntil = frontendStepMetaData.getName();
+ }
+
+ if(!(step instanceof QStateMachineStep stateMachineStep))
+ {
+ throw (new QException("Have a non-stateMachineStep in a process using stateMachine flow... " + step.getClass().getName()));
+ }
+
+ ///////////////////////
+ // run the sub-steps //
+ ///////////////////////
+ boolean ranAnySubSteps = false;
+ for(QStepMetaData subStep : stateMachineStep.getSubSteps())
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ok, well, skip them if this flag is set (and clear the flag once we've hit this sub-step) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ if(skipSubStepsUntil != null)
+ {
+ if(skipSubStepsUntil.equals(subStep.getName()))
+ {
+ skipSubStepsUntil = null;
+ }
+ continue;
+ }
+
+ ranAnySubSteps = true;
+ if(subStep instanceof QFrontendStepMetaData frontendStep)
+ {
+ LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
+ if(loopTodo == LoopTodo.BREAK)
+ {
+ return;
+ }
+ }
+ else if(subStep instanceof QBackendStepMetaData backendStepMetaData)
+ {
+ RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
+ Optional nextStepName = runBackendStepOutput.getProcessState().getNextStepName();
+
+ if(nextStepName.isEmpty() && StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
+ {
+ nextStepName = Optional.of(stateMachineStep.getDefaultNextStepName());
+ }
+
+ if(nextStepName.isPresent())
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we've been given a next-step-name, go to that step now. //
+ // it might be a backend-only stateMachineStep, in which case, we should run that backend step now. //
+ // or it might be a frontend-then-backend step, in which case, we want to go to that frontend step. //
+ // if we weren't given a next-step-name, then we should stay in the same state - either to finish //
+ // its sub-steps, or, to fall out of the loop and end the process. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ processState.clearNextStepName();
+ runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
+ return;
+ }
+ }
+ else
+ {
+ //////////////////////////////////////////////////
+ // in case we have a different step type, throw //
+ //////////////////////////////////////////////////
+ throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
+ }
+ }
+
+ if(!ranAnySubSteps)
+ {
+ if(StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
+ {
+ runStateMachineStep(stateMachineStep.getDefaultNextStepName(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QStateMachineStep getStateMachineStepContainingSubStep(QProcessMetaData process, String stepName)
+ {
+ for(QStepMetaData step : process.getAllSteps().values())
+ {
+ if(step instanceof QStateMachineStep stateMachineStep)
+ {
+ for(QStepMetaData subStep : stateMachineStep.getSubSteps())
+ {
+ if(subStep.getName().equals(stepName))
+ {
+ return (stateMachineStep);
+ }
+ }
+ }
+ }
+
+ return (null);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -336,12 +523,12 @@ public class RunProcessAction
///////////////////////////////////////////////////
runProcessInput.seedFromProcessState(optionalProcessState.get());
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- // if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - //
- // it is only needed on the transitional edge from a backend-step to a frontend step, but not //
- // in the other directly //
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- optionalProcessState.get().setUpdatedFrontendStepList(null);
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we're restoring an old state, we can discard a previously stored processMetaDataAdjustment - //
+ // it is only needed on the transitional edge from a backend-step to a frontend step, but not //
+ // in the other directly //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ optionalProcessState.get().setProcessMetaDataAdjustment(null);
///////////////////////////////////////////////////////////////////////////
// if there were values from the caller, put those (back) in the request //
@@ -356,16 +543,40 @@ public class RunProcessAction
}
ProcessState processState = optionalProcessState.get();
- processState.clearNextStepName();
return processState;
}
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private RunBackendStepOutput runBackendStep(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, QBackendStepMetaData backendStepMetaData, QStepMetaData step) throws Exception
+ {
+ ///////////////////////
+ // Run backend steps //
+ ///////////////////////
+ LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
+ RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // similarly, if the step produced a processMetaDataAdjustment, propagate that data outward //
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ if(runBackendStepOutput.getProcessMetaDataAdjustment() != null)
+ {
+ LOG.debug("Process step [" + step.getName() + "] generated a ProcessMetaDataAdjustment [" + runBackendStepOutput.getProcessMetaDataAdjustment() + "]!");
+ runProcessOutput.setProcessMetaDataAdjustment(runBackendStepOutput.getProcessMetaDataAdjustment());
+ }
+
+ return runBackendStepOutput;
+ }
+
+
+
/*******************************************************************************
** Run a single backend step.
*******************************************************************************/
- protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
+ RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java
new file mode 100644
index 00000000..9b7fd619
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java
@@ -0,0 +1,55 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.instances;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+
+
+/*******************************************************************************
+ ** Version of AbstractQQQApplication that assumes all meta-data is produced
+ ** by MetaDataProducers in a single package.
+ *******************************************************************************/
+public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public abstract String getMetaDataPackageName();
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QInstance defineQInstance() throws QException
+ {
+ QInstance qInstance = new QInstance();
+ MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, getMetaDataPackageName());
+ return (qInstance);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplication.java
new file mode 100644
index 00000000..03bd0b61
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplication.java
@@ -0,0 +1,78 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.instances;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
+import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+
+
+/*******************************************************************************
+ ** Base class to provide the definition of a QQQ-based application.
+ **
+ ** Essentially, just how to define its meta-data - in the form of a QInstance.
+ **
+ ** Also provides means to define the instance validation plugins to be used.
+ *******************************************************************************/
+public abstract class AbstractQQQApplication
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public abstract QInstance defineQInstance() throws QException;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public QInstance defineValidatedQInstance() throws QException, QInstanceValidationException
+ {
+ QInstance qInstance = defineQInstance();
+
+ QInstanceValidator.removeAllValidatorPlugins();
+ for(QInstanceValidatorPluginInterface> validatorPlugin : CollectionUtils.nonNullList(getValidatorPlugins()))
+ {
+ QInstanceValidator.addValidatorPlugin(validatorPlugin);
+ }
+
+ QInstanceValidator qInstanceValidator = new QInstanceValidator();
+ qInstanceValidator.validate(qInstance);
+ return (qInstance);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected List> getValidatorPlugins()
+ {
+ return new ArrayList<>();
+ }
+}
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 0e101379..0e7949eb 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
@@ -58,6 +58,7 @@ 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.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
@@ -410,10 +411,27 @@ public class QInstanceEnricher
**
*******************************************************************************/
private void enrichStep(QStepMetaData step)
+ {
+ enrichStep(step, false);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void enrichStep(QStepMetaData step, boolean isSubStep)
{
if(!StringUtils.hasContent(step.getLabel()))
{
- step.setLabel(nameToLabel(step.getName()));
+ if(isSubStep && (step.getName().endsWith(".backend") || step.getName().endsWith(".frontend")))
+ {
+ step.setLabel(nameToLabel(step.getName().replaceFirst("\\.(backend|frontend)", "")));
+ }
+ else
+ {
+ step.setLabel(nameToLabel(step.getName()));
+ }
}
step.getInputFields().forEach(this::enrichField);
@@ -434,6 +452,13 @@ public class QInstanceEnricher
frontendStepMetaData.getRecordListFields().forEach(this::enrichField);
}
}
+ else if(step instanceof QStateMachineStep stateMachineStep)
+ {
+ for(QStepMetaData subStep : CollectionUtils.nonNullList(stateMachineStep.getSubSteps()))
+ {
+ enrichStep(subStep, true);
+ }
+ }
}
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 41f7ff2b..58618c3e 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
@@ -43,6 +43,7 @@ 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.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
+import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataFilterInterface;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
@@ -74,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
@@ -185,6 +187,7 @@ public class QInstanceValidator
//////////////////////////////////////////////////////////////////////////
try
{
+ validateInstanceAttributes(qInstance);
validateBackends(qInstance);
validateAuthentication(qInstance);
validateAutomationProviders(qInstance);
@@ -225,6 +228,19 @@ public class QInstanceValidator
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void validateInstanceAttributes(QInstance qInstance)
+ {
+ if(qInstance.getMetaDataFilter() != null)
+ {
+ validateSimpleCodeReference("Instance metaDataFilter ", qInstance.getMetaDataFilter(), MetaDataFilterInterface.class);
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -928,13 +944,8 @@ public class QInstanceValidator
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
- if(field.getPossibleValueSourceName() != null)
- {
- assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
- "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
- }
-
String prefix = "Field " + fieldName + " in table " + tableName + " ";
+ validateFieldPossibleValueSourceAttributes(qInstance, field, prefix);
///////////////////////////////////////////////////
// validate things we know about field behaviors //
@@ -1039,6 +1050,31 @@ public class QInstanceValidator
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void validateFieldPossibleValueSourceAttributes(QInstance qInstance, QFieldMetaData field, String prefix)
+ {
+ if(field.getPossibleValueSourceName() != null)
+ {
+ assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
+ prefix + "has an unrecognized possibleValueSourceName " + field.getPossibleValueSourceName());
+
+ assertCondition(field.getInlinePossibleValueSource() == null, prefix.trim() + " has both a possibleValueSourceName and an inlinePossibleValueSource, which is not allowed.");
+ }
+
+ if(field.getInlinePossibleValueSource() != null)
+ {
+ String name = "inlinePossibleValueSource for " + prefix.trim();
+ if(assertCondition(QPossibleValueSourceType.ENUM.equals(field.getInlinePossibleValueSource().getType()), name + " must have a type of ENUM."))
+ {
+ validatePossibleValueSource(qInstance, name, field.getInlinePossibleValueSource());
+ }
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -1546,6 +1582,16 @@ public class QInstanceValidator
}
}
+ for(QFieldMetaData field : process.getInputFields())
+ {
+ validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName());
+ }
+
+ for(QFieldMetaData field : process.getOutputFields())
+ {
+ validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName());
+ }
+
if(process.getCancelStep() != null)
{
if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
@@ -1948,78 +1994,88 @@ public class QInstanceValidator
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
{
assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
- if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
+ validatePossibleValueSource(qInstance, pvsName, possibleValueSource);
+ });
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void validatePossibleValueSource(QInstance qInstance, String name, QPossibleValueSource possibleValueSource)
+ {
+ if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + name))
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // assert about fields that should and should not be set, based on possible value source type //
+ // do additional type-specific validations as well //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ switch(possibleValueSource.getType())
+ {
+ case ENUM ->
{
- ////////////////////////////////////////////////////////////////////////////////////////////////
- // assert about fields that should and should not be set, based on possible value source type //
- // do additional type-specific validations as well //
- ////////////////////////////////////////////////////////////////////////////////////////////////
- switch(possibleValueSource.getType())
+ assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + name + " should not have a tableName.");
+ assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + name + " should not have searchFields.");
+ assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + name + " should not have orderByFields.");
+ assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + name + " should not have a customCodeReference.");
+
+ assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + name + " is missing enum values");
+ }
+ case TABLE ->
+ {
+ assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + name + " should not have enum values.");
+ assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + name + " should not have a customCodeReference.");
+
+ QTableMetaData tableMetaData = null;
+ if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + name + " is missing a tableName."))
{
- case ENUM ->
- {
- assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
- assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + pvsName + " should not have searchFields.");
- assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + pvsName + " should not have orderByFields.");
- assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
-
- assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
- }
- case TABLE ->
- {
- assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
- assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
-
- QTableMetaData tableMetaData = null;
- if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
- {
- tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
- assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
- }
-
- if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + pvsName + " is missing searchFields."))
- {
- if(tableMetaData != null)
- {
- QTableMetaData finalTableMetaData = tableMetaData;
- for(String searchField : possibleValueSource.getSearchFields())
- {
- assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + pvsName + " has an unrecognized searchField: " + searchField);
- }
- }
- }
-
- if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + pvsName + " is missing orderByFields."))
- {
- if(tableMetaData != null)
- {
- QTableMetaData finalTableMetaData = tableMetaData;
-
- for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
- {
- assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + pvsName + " has an unrecognized orderByField: " + orderByField.getFieldName());
- }
- }
- }
- }
- case CUSTOM ->
- {
- assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
- assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
- assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + pvsName + " should not have searchFields.");
- assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + pvsName + " should not have orderByFields.");
-
- if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
- {
- validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
- }
- }
- default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
+ tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
+ assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + name + ".");
}
- runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
+ if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + name + " is missing searchFields."))
+ {
+ if(tableMetaData != null)
+ {
+ QTableMetaData finalTableMetaData = tableMetaData;
+ for(String searchField : possibleValueSource.getSearchFields())
+ {
+ assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + name + " has an unrecognized searchField: " + searchField);
+ }
+ }
+ }
+
+ if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + name + " is missing orderByFields."))
+ {
+ if(tableMetaData != null)
+ {
+ QTableMetaData finalTableMetaData = tableMetaData;
+
+ for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
+ {
+ assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + name + " has an unrecognized orderByField: " + orderByField.getFieldName());
+ }
+ }
+ }
}
- });
+ case CUSTOM ->
+ {
+ assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + name + " should not have enum values.");
+ assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + name + " should not have a tableName.");
+ assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + name + " should not have searchFields.");
+ assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + name + " should not have orderByFields.");
+
+ if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + name + " is missing a customCodeReference."))
+ {
+ validateSimpleCodeReference("PossibleValueSource " + name + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
+ }
+ }
+ default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
+ }
+
+ runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java
index f450493d..1ac0626b 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java
@@ -31,6 +31,16 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
*******************************************************************************/
public class MetaDataInput extends AbstractActionInput
{
+ private String frontendName;
+ private String frontendVersion;
+
+ private String middlewareName;
+ private String middlewareVersion;
+
+ private String applicationName;
+ private String applicationVersion;
+
+
/*******************************************************************************
**
@@ -39,4 +49,190 @@ public class MetaDataInput extends AbstractActionInput
{
}
+
+
+ /*******************************************************************************
+ ** Getter for frontendName
+ *******************************************************************************/
+ public String getFrontendName()
+ {
+ return (this.frontendName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for frontendName
+ *******************************************************************************/
+ public void setFrontendName(String frontendName)
+ {
+ this.frontendName = frontendName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendName
+ *******************************************************************************/
+ public MetaDataInput withFrontendName(String frontendName)
+ {
+ this.frontendName = frontendName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for frontendVersion
+ *******************************************************************************/
+ public String getFrontendVersion()
+ {
+ return (this.frontendVersion);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for frontendVersion
+ *******************************************************************************/
+ public void setFrontendVersion(String frontendVersion)
+ {
+ this.frontendVersion = frontendVersion;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendVersion
+ *******************************************************************************/
+ public MetaDataInput withFrontendVersion(String frontendVersion)
+ {
+ this.frontendVersion = frontendVersion;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for middlewareName
+ *******************************************************************************/
+ public String getMiddlewareName()
+ {
+ return (this.middlewareName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for middlewareName
+ *******************************************************************************/
+ public void setMiddlewareName(String middlewareName)
+ {
+ this.middlewareName = middlewareName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for middlewareName
+ *******************************************************************************/
+ public MetaDataInput withMiddlewareName(String middlewareName)
+ {
+ this.middlewareName = middlewareName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for middlewareVersion
+ *******************************************************************************/
+ public String getMiddlewareVersion()
+ {
+ return (this.middlewareVersion);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for middlewareVersion
+ *******************************************************************************/
+ public void setMiddlewareVersion(String middlewareVersion)
+ {
+ this.middlewareVersion = middlewareVersion;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for middlewareVersion
+ *******************************************************************************/
+ public MetaDataInput withMiddlewareVersion(String middlewareVersion)
+ {
+ this.middlewareVersion = middlewareVersion;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for applicationName
+ *******************************************************************************/
+ public String getApplicationName()
+ {
+ return (this.applicationName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for applicationName
+ *******************************************************************************/
+ public void setApplicationName(String applicationName)
+ {
+ this.applicationName = applicationName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for applicationName
+ *******************************************************************************/
+ public MetaDataInput withApplicationName(String applicationName)
+ {
+ this.applicationName = applicationName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for applicationVersion
+ *******************************************************************************/
+ public String getApplicationVersion()
+ {
+ return (this.applicationVersion);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for applicationVersion
+ *******************************************************************************/
+ public void setApplicationVersion(String applicationVersion)
+ {
+ this.applicationVersion = applicationVersion;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for applicationVersion
+ *******************************************************************************/
+ public MetaDataInput withApplicationVersion(String applicationVersion)
+ {
+ this.applicationVersion = applicationVersion;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessMetaDataAdjustment.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessMetaDataAdjustment.java
new file mode 100644
index 00000000..ab8c30b5
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessMetaDataAdjustment.java
@@ -0,0 +1,139 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.processes;
+
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** Object that stores adjustments that a process wants to make, at run-time,
+ ** to its meta-data.
+ **
+ ** e.g., changing the steps; updating fields (e.g., changing an inline PVS,
+ ** or an isRequired attribute)
+ *******************************************************************************/
+public class ProcessMetaDataAdjustment
+{
+ private static final QLogger LOG = QLogger.getLogger(ProcessMetaDataAdjustment.class);
+
+ private List updatedFrontendStepList = null;
+ private Map updatedFields = null;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public ProcessMetaDataAdjustment withUpdatedField(QFieldMetaData field)
+ {
+ if(updatedFields == null)
+ {
+ updatedFields = new LinkedHashMap<>();
+ }
+
+ if(!StringUtils.hasContent(field.getName()))
+ {
+ LOG.warn("Missing name on field in withUpdatedField - no update will happen.");
+ }
+ else
+ {
+ if(updatedFields.containsKey(field.getName()))
+ {
+ LOG.info("UpdatedFields map already contained a field with this name - overwriting it.", logPair("fieldName", field.getName()));
+ }
+
+ updatedFields.put(field.getName(), field);
+ }
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for updatedFrontendStepList
+ *******************************************************************************/
+ public List getUpdatedFrontendStepList()
+ {
+ return (this.updatedFrontendStepList);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for updatedFrontendStepList
+ *******************************************************************************/
+ public void setUpdatedFrontendStepList(List updatedFrontendStepList)
+ {
+ this.updatedFrontendStepList = updatedFrontendStepList;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for updatedFrontendStepList
+ *******************************************************************************/
+ public ProcessMetaDataAdjustment withUpdatedFrontendStepList(List updatedFrontendStepList)
+ {
+ this.updatedFrontendStepList = updatedFrontendStepList;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for updatedFields
+ *******************************************************************************/
+ public Map getUpdatedFields()
+ {
+ return (this.updatedFields);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for updatedFields
+ *******************************************************************************/
+ public void setUpdatedFields(Map updatedFields)
+ {
+ this.updatedFields = updatedFields;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for updatedFields
+ *******************************************************************************/
+ public ProcessMetaDataAdjustment withUpdatedFields(Map updatedFields)
+ {
+ this.updatedFields = updatedFields;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java
index c418bd07..ad7a0827 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java
@@ -29,7 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
/*******************************************************************************
@@ -42,10 +41,7 @@ public class ProcessState implements Serializable
private List stepList = new ArrayList<>();
private Optional nextStepName = Optional.empty();
- /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
- /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- private List updatedFrontendStepList = null;
+ private ProcessMetaDataAdjustment processMetaDataAdjustment = null;
@@ -148,33 +144,36 @@ public class ProcessState implements Serializable
+
+
/*******************************************************************************
- ** Getter for updatedFrontendStepList
+ ** Getter for processMetaDataAdjustment
*******************************************************************************/
- public List getUpdatedFrontendStepList()
+ public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
- return (this.updatedFrontendStepList);
+ return (this.processMetaDataAdjustment);
}
/*******************************************************************************
- ** Setter for updatedFrontendStepList
+ ** Setter for processMetaDataAdjustment
*******************************************************************************/
- public void setUpdatedFrontendStepList(List updatedFrontendStepList)
+ public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
- this.updatedFrontendStepList = updatedFrontendStepList;
+ this.processMetaDataAdjustment = processMetaDataAdjustment;
}
/*******************************************************************************
- ** Fluent setter for updatedFrontendStepList
+ ** Fluent setter for processMetaDataAdjustment
*******************************************************************************/
- public ProcessState withUpdatedFrontendStepList(List updatedFrontendStepList)
+ public ProcessState withProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
- this.updatedFrontendStepList = updatedFrontendStepList;
+ this.processMetaDataAdjustment = processMetaDataAdjustment;
return (this);
}
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
index 4754fcaa..7c89b98a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
@@ -374,7 +374,13 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
.map(step -> (QFrontendStepMetaData) step)
.toList());
- setUpdatedFrontendStepList(updatedFrontendStepList);
+ ProcessMetaDataAdjustment processMetaDataAdjustment = getProcessMetaDataAdjustment();
+ if(processMetaDataAdjustment == null)
+ {
+ processMetaDataAdjustment = new ProcessMetaDataAdjustment();
+ }
+ processMetaDataAdjustment.setUpdatedFrontendStepList(updatedFrontendStepList);
+ setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
@@ -411,21 +417,21 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/*******************************************************************************
- ** Getter for updatedFrontendStepList
+ ** Getter for ProcessMetaDataAdjustment (pass-through to processState)
*******************************************************************************/
- public List getUpdatedFrontendStepList()
+ public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
- return (this.processState.getUpdatedFrontendStepList());
+ return (this.processState.getProcessMetaDataAdjustment());
}
/*******************************************************************************
- ** Setter for updatedFrontendStepList
+ ** Setter for updatedFrontendStepList (pass-through to processState)
*******************************************************************************/
- public void setUpdatedFrontendStepList(List updatedFrontendStepList)
+ public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
- this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
+ this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java
index 30a5642a..f71a7137 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java
@@ -33,6 +33,7 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -336,7 +337,12 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
*******************************************************************************/
public void setUpdatedFrontendStepList(List updatedFrontendStepList)
{
- this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
+ if(this.processState.getProcessMetaDataAdjustment() == null)
+ {
+ this.processState.setProcessMetaDataAdjustment(new ProcessMetaDataAdjustment());
+ }
+
+ this.processState.getProcessMetaDataAdjustment().setUpdatedFrontendStepList(updatedFrontendStepList);
}
@@ -346,7 +352,27 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
*******************************************************************************/
public List getUpdatedFrontendStepList()
{
- return this.processState.getUpdatedFrontendStepList();
+ return ObjectUtils.tryElse(() -> this.processState.getProcessMetaDataAdjustment().getUpdatedFrontendStepList(), null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processMetaDataAdjustment
+ *******************************************************************************/
+ public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
+ {
+ return (this.processState.getProcessMetaDataAdjustment());
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processMetaDataAdjustment
+ *******************************************************************************/
+ public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
+ {
+ this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java
index 2cbae738..7a8756af 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java
@@ -40,6 +40,20 @@ public class CompositeWidgetData extends AbstractBlockWidgetData> blocks = new ArrayList<>();
+ private ModalMode modalMode;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum ModalMode
+ {
+ MODAL
+ }
+
+
+
private Layout layout;
private Map styleOverrides = new HashMap<>();
private String overlayHtml;
@@ -52,12 +66,14 @@ public class CompositeWidgetData extends AbstractBlockWidgetData.
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
+
+
+/*******************************************************************************
+ ** block that plays an audio file
+ *******************************************************************************/
+public class AudioBlockData extends AbstractBlockWidgetData
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getBlockTypeName()
+ {
+ return "AUDIO";
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/audio/AudioValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/audio/AudioValues.java
new file mode 100644
index 00000000..bffa3f03
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/audio/AudioValues.java
@@ -0,0 +1,130 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AudioValues implements BlockValuesInterface
+{
+ private String path;
+ private boolean showControls = false;
+ private boolean autoPlay = true;
+
+
+
+ /*******************************************************************************
+ ** Getter for path
+ *******************************************************************************/
+ public String getPath()
+ {
+ return (this.path);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for path
+ *******************************************************************************/
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for path
+ *******************************************************************************/
+ public AudioValues withPath(String path)
+ {
+ this.path = path;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for showControls
+ *******************************************************************************/
+ public boolean getShowControls()
+ {
+ return (this.showControls);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for showControls
+ *******************************************************************************/
+ public void setShowControls(boolean showControls)
+ {
+ this.showControls = showControls;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for showControls
+ *******************************************************************************/
+ public AudioValues withShowControls(boolean showControls)
+ {
+ this.showControls = showControls;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for autoPlay
+ *******************************************************************************/
+ public boolean getAutoPlay()
+ {
+ return (this.autoPlay);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for autoPlay
+ *******************************************************************************/
+ public void setAutoPlay(boolean autoPlay)
+ {
+ this.autoPlay = autoPlay;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for autoPlay
+ *******************************************************************************/
+ public AudioValues withAutoPlay(boolean autoPlay)
+ {
+ this.autoPlay = autoPlay;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java
index 1aab7064..c4ed7803 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java
@@ -30,4 +30,335 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles
*******************************************************************************/
public class BaseStyles implements BlockStylesInterface
{
+ private Directional padding;
+
+ private String backgroundColor;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static class Directional
+ {
+ private T top;
+ private T bottom;
+ private T left;
+ private T right;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Directional()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Directional(T top, T right, T bottom, T left)
+ {
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ this.left = left;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional of(T top, T right, T bottom, T left)
+ {
+ return (new Directional<>(top, right, bottom, left));
+ }
+
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional of(T value)
+ {
+ return (new Directional<>(value, value, value, value));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofTop(T top)
+ {
+ return (new Directional<>(top, null, null, null));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofRight(T right)
+ {
+ return (new Directional<>(null, right, null, null));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofBottom(T bottom)
+ {
+ return (new Directional<>(null, null, bottom, null));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofLeft(T left)
+ {
+ return (new Directional<>(null, null, null, left));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofX(T x)
+ {
+ return (new Directional<>(null, x, null, x));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofY(T y)
+ {
+ return (new Directional<>(y, null, y, null));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Directional ofXY(T x, T y)
+ {
+ return (new Directional<>(y, x, y, x));
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for top
+ **
+ *******************************************************************************/
+ public T getTop()
+ {
+ return top;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for top
+ **
+ *******************************************************************************/
+ public void setTop(T top)
+ {
+ this.top = top;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for top
+ **
+ *******************************************************************************/
+ public Directional withTop(T top)
+ {
+ this.top = top;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for bottom
+ **
+ *******************************************************************************/
+ public T getBottom()
+ {
+ return bottom;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for bottom
+ **
+ *******************************************************************************/
+ public void setBottom(T bottom)
+ {
+ this.bottom = bottom;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for bottom
+ **
+ *******************************************************************************/
+ public Directional withBottom(T bottom)
+ {
+ this.bottom = bottom;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for left
+ **
+ *******************************************************************************/
+ public T getLeft()
+ {
+ return left;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for left
+ **
+ *******************************************************************************/
+ public void setLeft(T left)
+ {
+ this.left = left;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for left
+ **
+ *******************************************************************************/
+ public Directional withLeft(T left)
+ {
+ this.left = left;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for right
+ **
+ *******************************************************************************/
+ public T getRight()
+ {
+ return right;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for right
+ **
+ *******************************************************************************/
+ public void setRight(T right)
+ {
+ this.right = right;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for right
+ **
+ *******************************************************************************/
+ public Directional withRight(T right)
+ {
+ this.right = right;
+ return (this);
+ }
+
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for padding
+ *******************************************************************************/
+ public Directional getPadding()
+ {
+ return (this.padding);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for padding
+ *******************************************************************************/
+ public void setPadding(Directional padding)
+ {
+ this.padding = padding;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for padding
+ *******************************************************************************/
+ public BaseStyles withPadding(Directional padding)
+ {
+ this.padding = padding;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for backgroundColor
+ *******************************************************************************/
+ public String getBackgroundColor()
+ {
+ return (this.backgroundColor);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for backgroundColor
+ *******************************************************************************/
+ public void setBackgroundColor(String backgroundColor)
+ {
+ this.backgroundColor = backgroundColor;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for backgroundColor
+ *******************************************************************************/
+ public BaseStyles withBackgroundColor(String backgroundColor)
+ {
+ this.backgroundColor = backgroundColor;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonBlockData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonBlockData.java
new file mode 100644
index 00000000..cd043ed0
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonBlockData.java
@@ -0,0 +1,46 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
+
+
+/*******************************************************************************
+ ** a button (for a process - not sure yet what this could do in a standalone
+ ** widget?) to submit the process screen to run a specific action (e.g., not just
+ ** 'next'), or do other control-ish things
+ *******************************************************************************/
+public class ButtonBlockData extends AbstractBlockWidgetData
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getBlockTypeName()
+ {
+ return "BUTTON";
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonStyles.java
new file mode 100644
index 00000000..64e834a6
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonStyles.java
@@ -0,0 +1,143 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ButtonStyles implements BlockStylesInterface
+{
+ private String color;
+ private String format;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum StandardColor
+ {
+ SUCCESS,
+ WARNING,
+ ERROR,
+ INFO,
+ MUTED
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum StandardFormat
+ {
+ OUTLINED,
+ FILLED,
+ TEXT
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for color
+ *******************************************************************************/
+ public String getColor()
+ {
+ return (this.color);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for color
+ *******************************************************************************/
+ public void setColor(String color)
+ {
+ this.color = color;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for color
+ *******************************************************************************/
+ public ButtonStyles withColor(String color)
+ {
+ this.color = color;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for format
+ *******************************************************************************/
+ public String getFormat()
+ {
+ return (this.format);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for format
+ *******************************************************************************/
+ public void setFormat(String format)
+ {
+ this.format = format;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for format
+ *******************************************************************************/
+ public ButtonStyles withFormat(String format)
+ {
+ this.format = format;
+ return (this);
+ }
+
+ /*******************************************************************************
+ ** Setter for format
+ *******************************************************************************/
+ public void setFormat(StandardFormat format)
+ {
+ this.format = (format == null ? null : format.name().toLowerCase());
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for format
+ *******************************************************************************/
+ public ButtonStyles withFormat(StandardFormat format)
+ {
+ setFormat(format);
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonValues.java
new file mode 100644
index 00000000..5faff9e4
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonValues.java
@@ -0,0 +1,218 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ButtonValues implements BlockValuesInterface
+{
+ private String label;
+ private String actionCode;
+ private String controlCode;
+
+ private QIcon startIcon;
+ private QIcon endIcon;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ButtonValues()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ButtonValues(String label, String actionCode)
+ {
+ setLabel(label);
+ setActionCode(actionCode);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return (this.label);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for label
+ *******************************************************************************/
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for label
+ *******************************************************************************/
+ public ButtonValues withLabel(String label)
+ {
+ this.label = label;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for actionCode
+ *******************************************************************************/
+ public String getActionCode()
+ {
+ return (this.actionCode);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for actionCode
+ *******************************************************************************/
+ public void setActionCode(String actionCode)
+ {
+ this.actionCode = actionCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for actionCode
+ *******************************************************************************/
+ public ButtonValues withActionCode(String actionCode)
+ {
+ this.actionCode = actionCode;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for startIcon
+ *******************************************************************************/
+ public QIcon getStartIcon()
+ {
+ return (this.startIcon);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for startIcon
+ *******************************************************************************/
+ public void setStartIcon(QIcon startIcon)
+ {
+ this.startIcon = startIcon;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for startIcon
+ *******************************************************************************/
+ public ButtonValues withStartIcon(QIcon startIcon)
+ {
+ this.startIcon = startIcon;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for endIcon
+ *******************************************************************************/
+ public QIcon getEndIcon()
+ {
+ return (this.endIcon);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for endIcon
+ *******************************************************************************/
+ public void setEndIcon(QIcon endIcon)
+ {
+ this.endIcon = endIcon;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for endIcon
+ *******************************************************************************/
+ public ButtonValues withEndIcon(QIcon endIcon)
+ {
+ this.endIcon = endIcon;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for controlCode
+ *******************************************************************************/
+ public String getControlCode()
+ {
+ return (this.controlCode);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for controlCode
+ *******************************************************************************/
+ public void setControlCode(String controlCode)
+ {
+ this.controlCode = controlCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for controlCode
+ *******************************************************************************/
+ public ButtonValues withControlCode(String controlCode)
+ {
+ this.controlCode = controlCode;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageBlockData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageBlockData.java
new file mode 100644
index 00000000..823c1fa9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageBlockData.java
@@ -0,0 +1,44 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
+
+
+/*******************************************************************************
+ ** block to display an image
+ *******************************************************************************/
+public class ImageBlockData extends AbstractBlockWidgetData
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getBlockTypeName()
+ {
+ return "IMAGE";
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageStyles.java
new file mode 100644
index 00000000..24f252b5
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageStyles.java
@@ -0,0 +1,108 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ImageStyles extends BaseStyles
+{
+ private String width;
+ private String height;
+
+
+ /*******************************************************************************
+ ** Fluent setter for padding
+ *******************************************************************************/
+ @Override
+ public ImageStyles withPadding(Directional padding)
+ {
+ super.setPadding(padding);
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for width
+ *******************************************************************************/
+ public String getWidth()
+ {
+ return (this.width);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for width
+ *******************************************************************************/
+ public void setWidth(String width)
+ {
+ this.width = width;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for width
+ *******************************************************************************/
+ public ImageStyles withWidth(String width)
+ {
+ this.width = width;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for height
+ *******************************************************************************/
+ public String getHeight()
+ {
+ return (this.height);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for height
+ *******************************************************************************/
+ public void setHeight(String height)
+ {
+ this.height = height;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for height
+ *******************************************************************************/
+ public ImageStyles withHeight(String height)
+ {
+ this.height = height;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageValues.java
new file mode 100644
index 00000000..8cffaacc
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageValues.java
@@ -0,0 +1,98 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ImageValues implements BlockValuesInterface
+{
+ private String path;
+ private String alt;
+
+
+
+ /*******************************************************************************
+ ** Getter for path
+ *******************************************************************************/
+ public String getPath()
+ {
+ return (this.path);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for path
+ *******************************************************************************/
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for path
+ *******************************************************************************/
+ public ImageValues withPath(String path)
+ {
+ this.path = path;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for alt
+ *******************************************************************************/
+ public String getAlt()
+ {
+ return (this.alt);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for alt
+ *******************************************************************************/
+ public void setAlt(String alt)
+ {
+ this.alt = alt;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for alt
+ *******************************************************************************/
+ public ImageValues withAlt(String alt)
+ {
+ this.alt = alt;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldBlockData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldBlockData.java
new file mode 100644
index 00000000..feea9c67
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldBlockData.java
@@ -0,0 +1,45 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
+
+
+/*******************************************************************************
+ ** block to display an input field - initially targeted at widgets-in-processes
+ *******************************************************************************/
+public class InputFieldBlockData extends AbstractBlockWidgetData
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getBlockTypeName()
+ {
+ return "INPUT_FIELD";
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldValues.java
new file mode 100644
index 00000000..52a82f60
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldValues.java
@@ -0,0 +1,217 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield;
+
+
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class InputFieldValues implements BlockValuesInterface
+{
+ private QFieldMetaData fieldMetaData;
+
+ private Boolean autoFocus;
+ private Boolean submitOnEnter;
+ private Boolean hideSoftKeyboard;
+ private String placeholder;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public InputFieldValues()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public InputFieldValues(QFieldMetaData fieldMetaData)
+ {
+ setFieldMetaData(fieldMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldMetaData
+ *******************************************************************************/
+ public QFieldMetaData getFieldMetaData()
+ {
+ return (this.fieldMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldMetaData
+ *******************************************************************************/
+ public void setFieldMetaData(QFieldMetaData fieldMetaData)
+ {
+ this.fieldMetaData = fieldMetaData;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldMetaData
+ *******************************************************************************/
+ public InputFieldValues withFieldMetaData(QFieldMetaData fieldMetaData)
+ {
+ this.fieldMetaData = fieldMetaData;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for autoFocus
+ *******************************************************************************/
+ public Boolean getAutoFocus()
+ {
+ return (this.autoFocus);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for autoFocus
+ *******************************************************************************/
+ public void setAutoFocus(Boolean autoFocus)
+ {
+ this.autoFocus = autoFocus;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for autoFocus
+ *******************************************************************************/
+ public InputFieldValues withAutoFocus(Boolean autoFocus)
+ {
+ this.autoFocus = autoFocus;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for submitOnEnter
+ *******************************************************************************/
+ public Boolean getSubmitOnEnter()
+ {
+ return (this.submitOnEnter);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for submitOnEnter
+ *******************************************************************************/
+ public void setSubmitOnEnter(Boolean submitOnEnter)
+ {
+ this.submitOnEnter = submitOnEnter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for submitOnEnter
+ *******************************************************************************/
+ public InputFieldValues withSubmitOnEnter(Boolean submitOnEnter)
+ {
+ this.submitOnEnter = submitOnEnter;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for placeholder
+ *******************************************************************************/
+ public String getPlaceholder()
+ {
+ return (this.placeholder);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for placeholder
+ *******************************************************************************/
+ public void setPlaceholder(String placeholder)
+ {
+ this.placeholder = placeholder;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for placeholder
+ *******************************************************************************/
+ public InputFieldValues withPlaceholder(String placeholder)
+ {
+ this.placeholder = placeholder;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for hideSoftKeyboard
+ *******************************************************************************/
+ public Boolean getHideSoftKeyboard()
+ {
+ return (this.hideSoftKeyboard);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for hideSoftKeyboard
+ *******************************************************************************/
+ public void setHideSoftKeyboard(Boolean hideSoftKeyboard)
+ {
+ this.hideSoftKeyboard = hideSoftKeyboard;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for hideSoftKeyboard
+ *******************************************************************************/
+ public InputFieldValues withHideSoftKeyboard(Boolean hideSoftKeyboard)
+ {
+ this.hideSoftKeyboard = hideSoftKeyboard;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java
index f735798d..c72e1e25 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java
@@ -30,4 +30,325 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles
*******************************************************************************/
public class TextStyles implements BlockStylesInterface
{
+ private String color;
+ private String format;
+ private String weight;
+ private String size;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum StandardColor
+ {
+ SUCCESS,
+ WARNING,
+ ERROR,
+ INFO,
+ MUTED
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum StandardFormat
+ {
+ DEFAULT,
+ ALERT,
+ BANNER
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum StandardSize
+ {
+ LARGEST,
+ HEADLINE,
+ TITLE,
+ BODY,
+ SMALLEST
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum StandardWeight
+ {
+ EXTRA_LIGHT("extralight"),
+ THIN("thin"),
+ MEDIUM("medium"),
+ SEMI_BOLD("semibold"),
+ BLACK("black"),
+ BOLD("bold"),
+ EXTRA_BOLD("extrabold"),
+ W100("100"),
+ W200("200"),
+ W300("300"),
+ W400("400"),
+ W500("500"),
+ W600("600"),
+ W700("700"),
+ W800("800"),
+ W900("900");
+
+ private final String value;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ StandardWeight(String value)
+ {
+ this.value = value;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for value
+ **
+ *******************************************************************************/
+ public String getValue()
+ {
+ return value;
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public TextStyles()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public TextStyles(StandardColor standardColor)
+ {
+ setColor(standardColor);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for format
+ *******************************************************************************/
+ public String getFormat()
+ {
+ return (this.format);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for format
+ *******************************************************************************/
+ public void setFormat(String format)
+ {
+ this.format = format;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for format
+ *******************************************************************************/
+ public TextStyles withFormat(String format)
+ {
+ this.format = format;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for format
+ *******************************************************************************/
+ public void setFormat(StandardFormat format)
+ {
+ this.format = format == null ? null : format.name().toLowerCase();
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for format
+ *******************************************************************************/
+ public TextStyles withFormat(StandardFormat format)
+ {
+ this.setFormat(format);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for weight
+ *******************************************************************************/
+ public String getWeight()
+ {
+ return (this.weight);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for weight
+ *******************************************************************************/
+ public void setWeight(String weight)
+ {
+ this.weight = weight;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for weight
+ *******************************************************************************/
+ public TextStyles withWeight(String weight)
+ {
+ this.weight = weight;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for weight
+ *******************************************************************************/
+ public void setWeight(StandardWeight weight)
+ {
+ setWeight(weight == null ? null : weight.getValue());
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for weight
+ *******************************************************************************/
+ public TextStyles withWeight(StandardWeight weight)
+ {
+ setWeight(weight);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for size
+ *******************************************************************************/
+ public String getSize()
+ {
+ return (this.size);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for size
+ *******************************************************************************/
+ public void setSize(String size)
+ {
+ this.size = size;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for size
+ *******************************************************************************/
+ public TextStyles withSize(String size)
+ {
+ this.size = size;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Setter for size
+ *******************************************************************************/
+ public void setSize(StandardSize size)
+ {
+ this.size = (size == null ? null : size.name().toLowerCase());
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for size
+ *******************************************************************************/
+ public TextStyles withSize(StandardSize size)
+ {
+ setSize(size);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for color
+ *******************************************************************************/
+ public String getColor()
+ {
+ return (this.color);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for color
+ *******************************************************************************/
+ public void setColor(String color)
+ {
+ this.color = color;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for color
+ *******************************************************************************/
+ public TextStyles withColor(String color)
+ {
+ this.color = color;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for color
+ *******************************************************************************/
+ public void setColor(StandardColor color)
+ {
+ this.color = color == null ? null : color.name();
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for color
+ *******************************************************************************/
+ public TextStyles withColor(StandardColor color)
+ {
+ setColor(color);
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java
index 3b82f4cb..132933b1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
/*******************************************************************************
@@ -32,6 +33,9 @@ public class TextValues implements BlockValuesInterface
{
private String text;
+ private QIcon startIcon;
+ private QIcon endIcon;
+
/*******************************************************************************
@@ -84,4 +88,66 @@ public class TextValues implements BlockValuesInterface
return (this);
}
+
+ /*******************************************************************************
+ ** Getter for startIcon
+ *******************************************************************************/
+ public QIcon getStartIcon()
+ {
+ return (this.startIcon);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for startIcon
+ *******************************************************************************/
+ public void setStartIcon(QIcon startIcon)
+ {
+ this.startIcon = startIcon;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for startIcon
+ *******************************************************************************/
+ public TextValues withStartIcon(QIcon startIcon)
+ {
+ this.startIcon = startIcon;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for endIcon
+ *******************************************************************************/
+ public QIcon getEndIcon()
+ {
+ return (this.endIcon);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for endIcon
+ *******************************************************************************/
+ public void setEndIcon(QIcon endIcon)
+ {
+ this.endIcon = endIcon;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for endIcon
+ *******************************************************************************/
+ public TextValues withEndIcon(QIcon endIcon)
+ {
+ this.endIcon = endIcon;
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java
index 2756715f..15ae31d7 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java
@@ -22,9 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
-
-
/*******************************************************************************
** Abstract class that knows how to produce meta data objects. Useful with
** MetaDataProducerHelper, to put point at a package full of these, and populate
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java
index 54c7ae4b..ac68cfd2 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java
@@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata;
-import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@@ -30,14 +29,12 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.reflect.ClassPath;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -51,8 +48,6 @@ public class MetaDataProducerHelper
private static Map, Integer> comparatorValuesByType = new HashMap<>();
private static Integer defaultComparatorValue;
- private static ImmutableSet topLevelClasses;
-
static
{
////////////////////////////////////////////////////////////////////////////////////////
@@ -87,7 +82,7 @@ public class MetaDataProducerHelper
////////////////////////////////////////////////////////////////////////
// find all the meta data producer classes in (and under) the package //
////////////////////////////////////////////////////////////////////////
- classesInPackage = getClassesInPackage(packageName);
+ classesInPackage = ClassPathUtils.getClassesInPackage(packageName);
}
catch(Exception e)
{
@@ -176,51 +171,4 @@ public class MetaDataProducerHelper
}
-
-
- /*******************************************************************************
- ** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection
- ** (since the original, from ChatGPT, didn't work in jars, despite GPT hallucinating that it would)
- *******************************************************************************/
- private static List> getClassesInPackage(String packageName) throws IOException
- {
- List> classes = new ArrayList<>();
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
-
- for(ClassPath.ClassInfo info : getTopLevelClasses(loader))
- {
- if(info.getName().startsWith(packageName))
- {
- classes.add(info.load());
- }
- }
-
- return (classes);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static ImmutableSet getTopLevelClasses(ClassLoader loader) throws IOException
- {
- if(topLevelClasses == null)
- {
- topLevelClasses = ClassPath.from(loader).getTopLevelClasses();
- }
-
- return (topLevelClasses);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public static void clearTopLevelClassCache()
- {
- topLevelClasses = null;
- }
-
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerInterface.java
similarity index 93%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerInterface.java
index 9faf3dc4..e91073e5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerInterface.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2023. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,12 +19,10 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.model;
+package com.kingsrook.qqq.backend.core.model.metadata;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
-import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
index fea69209..a61971c8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
@@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.AppTreeNodeType;
@@ -113,6 +114,8 @@ public class QInstance
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
+ private QCodeReference metaDataFilter = null;
+
//////////////////////////////////////////////////////////////////////////////////////
// todo - lock down the object (no more changes allowed) after it's been validated? //
// if doing so, may need to copy all of the collections into read-only versions... //
@@ -1485,4 +1488,35 @@ public class QInstance
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
+
+ /*******************************************************************************
+ ** Getter for metaDataFilter
+ *******************************************************************************/
+ public QCodeReference getMetaDataFilter()
+ {
+ return (this.metaDataFilter);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for metaDataFilter
+ *******************************************************************************/
+ public void setMetaDataFilter(QCodeReference metaDataFilter)
+ {
+ this.metaDataFilter = metaDataFilter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for metaDataFilter
+ *******************************************************************************/
+ public QInstance withMetaDataFilter(QCodeReference metaDataFilter)
+ {
+ this.metaDataFilter = metaDataFilter;
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
index 14a9fbb6..c300e7fa 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
@@ -177,7 +177,7 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
**
*******************************************************************************/
- public QAuthenticationMetaData withVales(Map values)
+ public QAuthenticationMetaData withValues(Map values)
{
this.values = values;
return (this);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceLambda.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceLambda.java
new file mode 100644
index 00000000..5cd3c050
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceLambda.java
@@ -0,0 +1,58 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.code;
+
+
+/*******************************************************************************
+ ** Specialized type of QCodeReference that takes a lambda function object.
+ **
+ ** Originally intended for more concise setup of backend steps in tests - but,
+ ** may be generally useful.
+ *******************************************************************************/
+public class QCodeReferenceLambda extends QCodeReference
+{
+ private final T lambda;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public QCodeReferenceLambda(T lambda)
+ {
+ this.lambda = lambda;
+ this.setCodeType(QCodeType.JAVA);
+ this.setName("[Lambda:" + lambda.toString() + "]");
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for lambda
+ **
+ *******************************************************************************/
+ public T getLambda()
+ {
+ return lambda;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
index d78d469d..5e5e61f0 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
@@ -42,6 +42,7 @@ 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.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -73,10 +74,12 @@ public class QFieldMetaData implements Cloneable
// propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" //
///////////////////////////////////////////////////////////////////////////////////
- private String displayFormat = "%s";
+ private String displayFormat = "%s";
private Serializable defaultValue;
- private String possibleValueSourceName;
- private QQueryFilter possibleValueSourceFilter;
+
+ private String possibleValueSourceName;
+ private QQueryFilter possibleValueSourceFilter;
+ private QPossibleValueSource inlinePossibleValueSource;
private Integer maxLength;
private Set> behaviors;
@@ -1058,4 +1061,35 @@ public class QFieldMetaData implements Cloneable
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents);
}
+
+ /*******************************************************************************
+ ** Getter for inlinePossibleValueSource
+ *******************************************************************************/
+ public QPossibleValueSource getInlinePossibleValueSource()
+ {
+ return (this.inlinePossibleValueSource);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inlinePossibleValueSource
+ *******************************************************************************/
+ public void setInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource)
+ {
+ this.inlinePossibleValueSource = inlinePossibleValueSource;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inlinePossibleValueSource
+ *******************************************************************************/
+ public QFieldMetaData withInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource)
+ {
+ this.inlinePossibleValueSource = inlinePossibleValueSource;
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java
index b4ab8965..e322be68 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
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;
@@ -45,7 +46,7 @@ public class AppTreeNode
private String label;
private List children;
- private String iconName;
+ private QIcon icon;
@@ -82,7 +83,7 @@ public class AppTreeNode
if(appChildMetaData.getIcon() != null)
{
// todo - propagate icons from parents, if they aren't set here...
- this.iconName = appChildMetaData.getIcon().getName();
+ this.icon = appChildMetaData.getIcon();
}
}
@@ -138,7 +139,18 @@ public class AppTreeNode
*******************************************************************************/
public String getIconName()
{
- return iconName;
+ return (icon == null ? null : icon.getName());
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for icon
+ **
+ *******************************************************************************/
+ public QIcon getIcon()
+ {
+ return icon;
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java
index ef73eac8..f549dabf 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java
@@ -32,6 +32,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
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.layout.QSupplementalAppMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -45,7 +46,7 @@ public class QFrontendAppMetaData
{
private String name;
private String label;
- private String iconName;
+ private QIcon icon;
private List widgets = new ArrayList<>();
private List children = new ArrayList<>();
@@ -56,6 +57,7 @@ public class QFrontendAppMetaData
private Map supplementalAppMetaData;
+
/*******************************************************************************
**
*******************************************************************************/
@@ -63,11 +65,7 @@ public class QFrontendAppMetaData
{
this.name = appMetaData.getName();
this.label = appMetaData.getLabel();
-
- if(appMetaData.getIcon() != null)
- {
- this.iconName = appMetaData.getIcon().getName();
- }
+ this.icon = appMetaData.getIcon();
List filteredWidgets = CollectionUtils.nonNullList(appMetaData.getWidgets()).stream().filter(n -> metaDataOutput.getWidgets().containsKey(n)).toList();
if(CollectionUtils.nullSafeHasContents(filteredWidgets))
@@ -81,6 +79,10 @@ public class QFrontendAppMetaData
List filteredTables = CollectionUtils.nonNullList(section.getTables()).stream().filter(n -> metaDataOutput.getTables().containsKey(n)).toList();
List filteredProcesses = CollectionUtils.nonNullList(section.getProcesses()).stream().filter(n -> metaDataOutput.getProcesses().containsKey(n)).toList();
List filteredReports = CollectionUtils.nonNullList(section.getReports()).stream().filter(n -> metaDataOutput.getReports().containsKey(n)).toList();
+
+ //////////////////////////////////////////////////////
+ // only include the section if it has some contents //
+ //////////////////////////////////////////////////////
if(!filteredTables.isEmpty() || !filteredProcesses.isEmpty() || !filteredReports.isEmpty())
{
QAppSection clonedSection = section.clone();
@@ -174,18 +176,7 @@ public class QFrontendAppMetaData
*******************************************************************************/
public String getIconName()
{
- return iconName;
- }
-
-
-
- /*******************************************************************************
- ** Setter for iconName
- **
- *******************************************************************************/
- public void setIconName(String iconName)
- {
- this.iconName = iconName;
+ return (icon == null ? null : icon.getName());
}
@@ -235,4 +226,15 @@ public class QFrontendAppMetaData
{
return supplementalAppMetaData;
}
+
+
+
+ /*******************************************************************************
+ ** Getter for icon
+ **
+ *******************************************************************************/
+ public QIcon getIcon()
+ {
+ return icon;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java
index 54b8aba6..38dfdc54 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java
@@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFron
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.help.QHelpContent;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -56,6 +57,7 @@ public class QFrontendFieldMetaData
private List adornments;
private List helpContents;
+ private QPossibleValueSource inlinePossibleValueSource;
private List behaviors;
@@ -81,6 +83,7 @@ public class QFrontendFieldMetaData
this.adornments = fieldMetaData.getAdornments();
this.defaultValue = fieldMetaData.getDefaultValue();
this.helpContents = fieldMetaData.getHelpContents();
+ this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource();
for(FieldBehavior> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors()))
{
@@ -218,6 +221,17 @@ public class QFrontendFieldMetaData
+ /*******************************************************************************
+ ** Getter for inlinePossibleValueSource
+ **
+ *******************************************************************************/
+ public QPossibleValueSource getInlinePossibleValueSource()
+ {
+ return inlinePossibleValueSource;
+ }
+
+
+
/*******************************************************************************
** Getter for fieldBehaviors
**
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java
index ba2bfa60..c435e0f4 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java
@@ -29,8 +29,10 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
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.QStateMachineStep;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -47,9 +49,10 @@ public class QFrontendProcessMetaData
private String tableName;
private boolean isHidden;
- private String iconName;
+ private QIcon icon;
private List frontendSteps;
+ private String stepFlow;
private boolean hasPermission;
@@ -68,15 +71,27 @@ public class QFrontendProcessMetaData
this.label = processMetaData.getLabel();
this.tableName = processMetaData.getTableName();
this.isHidden = processMetaData.getIsHidden();
+ this.stepFlow = processMetaData.getStepFlow().toString();
if(includeSteps)
{
if(CollectionUtils.nullSafeHasContents(processMetaData.getStepList()))
{
- this.frontendSteps = processMetaData.getStepList().stream()
- .filter(QFrontendStepMetaData.class::isInstance)
- .map(QFrontendStepMetaData.class::cast)
- .collect(Collectors.toList());
+ this.frontendSteps = switch(processMetaData.getStepFlow())
+ {
+ case LINEAR -> processMetaData.getStepList().stream()
+ .filter(QFrontendStepMetaData.class::isInstance)
+ .map(QFrontendStepMetaData.class::cast)
+ .collect(Collectors.toList());
+
+ case STATE_MACHINE -> processMetaData.getAllSteps().values().stream()
+ .filter(QStateMachineStep.class::isInstance)
+ .map(QStateMachineStep.class::cast)
+ .flatMap(step -> step.getSubSteps().stream())
+ .filter(QFrontendStepMetaData.class::isInstance)
+ .map(QFrontendStepMetaData.class::cast)
+ .collect(Collectors.toList());
+ };
}
else
{
@@ -84,10 +99,7 @@ public class QFrontendProcessMetaData
}
}
- if(processMetaData.getIcon() != null)
- {
- this.iconName = processMetaData.getIcon().getName();
- }
+ this.icon = processMetaData.getIcon();
hasPermission = PermissionsHelper.hasProcessPermission(actionInput, name);
}
@@ -166,7 +178,7 @@ public class QFrontendProcessMetaData
*******************************************************************************/
public String getIconName()
{
- return iconName;
+ return icon == null ? null : icon.getName();
}
@@ -180,4 +192,25 @@ public class QFrontendProcessMetaData
return hasPermission;
}
+
+
+ /*******************************************************************************
+ ** Getter for stepFlow
+ **
+ *******************************************************************************/
+ public String getStepFlow()
+ {
+ return stepFlow;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for icon
+ **
+ *******************************************************************************/
+ public QIcon getIcon()
+ {
+ return icon;
+ }
}
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 d98ad463..6e98fdec 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
@@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -40,6 +40,7 @@ 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.help.QHelpContent;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
@@ -61,7 +62,7 @@ public class QFrontendTableMetaData
private String label;
private boolean isHidden;
private String primaryKeyField;
- private String iconName;
+ private QIcon icon;
private Map fields;
private List sections;
@@ -156,10 +157,7 @@ public class QFrontendTableMetaData
}
}
- if(tableMetaData.getIcon() != null)
- {
- this.iconName = tableMetaData.getIcon().getName();
- }
+ this.icon = tableMetaData.getIcon();
setCapabilities(backendForTable, tableMetaData);
@@ -185,7 +183,7 @@ public class QFrontendTableMetaData
*******************************************************************************/
private void setCapabilities(QBackendMetaData backend, QTableMetaData table)
{
- Set enabledCapabilities = new HashSet<>();
+ Set enabledCapabilities = new LinkedHashSet<>();
for(Capability capability : Capability.values())
{
if(table.isCapabilityEnabled(backend, capability))
@@ -275,7 +273,7 @@ public class QFrontendTableMetaData
*******************************************************************************/
public String getIconName()
{
- return iconName;
+ return (icon == null ? null : icon.getName());
}
@@ -397,4 +395,16 @@ public class QFrontendTableMetaData
{
return helpContents;
}
+
+
+
+ /*******************************************************************************
+ ** Getter for icon
+ **
+ *******************************************************************************/
+ public QIcon getIcon()
+ {
+ return icon;
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/ProcessStepFlow.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/ProcessStepFlow.java
new file mode 100644
index 00000000..492496dd
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/ProcessStepFlow.java
@@ -0,0 +1,38 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.processes;
+
+
+/*******************************************************************************
+ ** Possible ways the steps of a process can flow.
+ **
+ ** LINEAR - (the default) - the list of steps in the process are executed in-order
+ **
+ ** STATE_MACHINE - concept of "states", each which has a backend & frontend step;
+ ** a backend step can (must?) set the field "stepState" (or "nextStepName") to
+ ** say what the next (frontend) step is.
+ *******************************************************************************/
+public enum ProcessStepFlow
+{
+ LINEAR,
+ STATE_MACHINE
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java
index 9de7595a..0c07043e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java
@@ -47,6 +47,8 @@ public class QFrontendStepMetaData extends QStepMetaData
private List recordListFields;
private Map formFieldMap;
+ private String format;
+
private List helpContents;
@@ -403,4 +405,35 @@ public class QFrontendStepMetaData extends QStepMetaData
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents);
}
+
+ /*******************************************************************************
+ ** Getter for format
+ *******************************************************************************/
+ public String getFormat()
+ {
+ return (this.format);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for format
+ *******************************************************************************/
+ public void setFormat(String format)
+ {
+ this.format = format;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for format
+ *******************************************************************************/
+ public QFrontendStepMetaData withFormat(String format)
+ {
+ this.format = format;
+ return (this);
+ }
+
+
}
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 fc7c7687..8bb459ef 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
@@ -57,6 +57,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private Integer minInputRecords = null;
private Integer maxInputRecords = null;
+ private ProcessStepFlow stepFlow = ProcessStepFlow.LINEAR;
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
@@ -213,11 +214,10 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
- /*******************************************************************************
- ** add a step to the stepList and map
+ /***************************************************************************
**
- *******************************************************************************/
- public QProcessMetaData addStep(QStepMetaData step)
+ ***************************************************************************/
+ public QProcessMetaData withStep(QStepMetaData step)
{
int index = 0;
if(this.stepList != null)
@@ -231,11 +231,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
+ /*******************************************************************************
+ ** add a step to the stepList and map
+ **
+ *******************************************************************************/
+ @Deprecated(since = "withStep was added")
+ public QProcessMetaData addStep(QStepMetaData step)
+ {
+ return (withStep(step));
+ }
+
+
+
/*******************************************************************************
** add a step to the stepList (at the specified index) and the step map
**
*******************************************************************************/
- public QProcessMetaData addStep(int index, QStepMetaData step)
+ public QProcessMetaData withStep(int index, QStepMetaData step)
{
if(this.stepList == null)
{
@@ -260,11 +272,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
+ /*******************************************************************************
+ ** add a step to the stepList (at the specified index) and the step map
+ **
+ *******************************************************************************/
+ @Deprecated(since = "withStep was added")
+ public QProcessMetaData addStep(int index, QStepMetaData step)
+ {
+ return (withStep(index, step));
+ }
+
+
+
/*******************************************************************************
** add a step ONLY to the step map - NOT the list w/ default execution order.
**
*******************************************************************************/
- public QProcessMetaData addOptionalStep(QStepMetaData step)
+ public QProcessMetaData withOptionalStep(QStepMetaData step)
{
if(this.steps == null)
{
@@ -283,6 +307,18 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
+ /*******************************************************************************
+ ** add a step ONLY to the step map - NOT the list w/ default execution order.
+ **
+ *******************************************************************************/
+ @Deprecated(since = "withOptionalStep was added")
+ public QProcessMetaData addOptionalStep(QStepMetaData step)
+ {
+ return (withOptionalStep(step));
+ }
+
+
+
/*******************************************************************************
** Setter for stepList
**
@@ -299,7 +335,26 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
*******************************************************************************/
public QStepMetaData getStep(String stepName)
{
- return (steps.get(stepName));
+ if(steps.containsKey(stepName))
+ {
+ return steps.get(stepName);
+ }
+
+ for(QStepMetaData step : steps.values())
+ {
+ if(step instanceof QStateMachineStep stateMachineStep)
+ {
+ for(QStepMetaData subStep : stateMachineStep.getSubSteps())
+ {
+ if(subStep.getName().equals(stepName))
+ {
+ return (subStep);
+ }
+ }
+ }
+ }
+
+ return (null);
}
@@ -780,4 +835,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for stepFlow
+ *******************************************************************************/
+ public ProcessStepFlow getStepFlow()
+ {
+ return (this.stepFlow);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for stepFlow
+ *******************************************************************************/
+ public void setStepFlow(ProcessStepFlow stepFlow)
+ {
+ this.stepFlow = stepFlow;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for stepFlow
+ *******************************************************************************/
+ public QProcessMetaData withStepFlow(ProcessStepFlow stepFlow)
+ {
+ this.stepFlow = stepFlow;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java
new file mode 100644
index 00000000..dd370142
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java
@@ -0,0 +1,188 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.processes;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** A step for a state-machine flow based Process.
+ **
+ ** Consists of 1 or 2 sub-steps, which are frontend and/or backend.
+ *******************************************************************************/
+public class QStateMachineStep extends QStepMetaData
+{
+ private List subSteps = new ArrayList<>();
+
+ private String defaultNextStepName;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ private QStateMachineStep(List subSteps)
+ {
+ setStepType("stateMachine");
+ this.subSteps.addAll(subSteps);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static QStateMachineStep frontendOnly(String name, QFrontendStepMetaData frontendStepMetaData)
+ {
+ if(!StringUtils.hasContent(frontendStepMetaData.getName()))
+ {
+ frontendStepMetaData.setName(name + ".frontend");
+ }
+
+ return (new QStateMachineStep(List.of(frontendStepMetaData)).withName(name));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static QStateMachineStep backendOnly(String name, QBackendStepMetaData backendStepMetaData)
+ {
+ if(!StringUtils.hasContent(backendStepMetaData.getName()))
+ {
+ backendStepMetaData.setName(name + ".backend");
+ }
+
+ return (new QStateMachineStep(List.of(backendStepMetaData)).withName(name));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static QStateMachineStep frontendThenBackend(String name, QFrontendStepMetaData frontendStepMetaData, QBackendStepMetaData backendStepMetaData)
+ {
+ if(!StringUtils.hasContent(frontendStepMetaData.getName()))
+ {
+ frontendStepMetaData.setName(name + ".frontend");
+ }
+
+ if(!StringUtils.hasContent(backendStepMetaData.getName()))
+ {
+ backendStepMetaData.setName(name + ".backend");
+ }
+
+ return (new QStateMachineStep(List.of(frontendStepMetaData, backendStepMetaData)).withName(name));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QStateMachineStep withName(String name)
+ {
+ super.withName(name);
+ return (this);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QStateMachineStep withLabel(String label)
+ {
+ super.withLabel(label);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for subSteps
+ **
+ *******************************************************************************/
+ public List getSubSteps()
+ {
+ return subSteps;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for defaultNextStepName
+ *******************************************************************************/
+ public String getDefaultNextStepName()
+ {
+ return (this.defaultNextStepName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for defaultNextStepName
+ *******************************************************************************/
+ public void setDefaultNextStepName(String defaultNextStepName)
+ {
+ this.defaultNextStepName = defaultNextStepName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for defaultNextStepName
+ *******************************************************************************/
+ public QStateMachineStep withDefaultNextStepName(String defaultNextStepName)
+ {
+ this.defaultNextStepName = defaultNextStepName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Get a list of all of the input fields used by this step (all of its sub-steps)
+ *******************************************************************************/
+ @JsonIgnore
+ @Override
+ public List getInputFields()
+ {
+ List rs = new ArrayList<>();
+ for(QStepMetaData subStep : subSteps)
+ {
+ rs.addAll(subStep.getInputFields());
+ }
+ return (rs);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java
index 49e5bb64..33d4479a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java
@@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.sharing;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.processes.implementations.sharing.ShareScope;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java
index 68f0cbd3..b8cc5f4d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java
@@ -26,13 +26,13 @@ import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
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.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
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;
@@ -114,7 +114,7 @@ public class ScheduledReportSyncToScheduledJobProcess extends AbstractTableSyncT
scheduledJob = new ScheduledJob();
scheduledJob.setLabel("Scheduled Report " + scheduledReport.getId());
scheduledJob.setDescription("Job to run Scheduled Report Id " + scheduledReport.getId()
- + " (which runs Report Id " + scheduledReport.getSavedReportId() + ")");
+ + " (which runs Report Id " + scheduledReport.getSavedReportId() + ")");
scheduledJob.setSchedulerName(runBackendStepInput.getValueString(SCHEDULER_NAME_FIELD_NAME));
scheduledJob.setType(ScheduledJobType.PROCESS.name());
scheduledJob.setForeignKeyType(getScheduledJobForeignKeyType());
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java
index 310fafe6..92b68dd6 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java
@@ -36,7 +36,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.query.QCriteriaOperator;
@@ -47,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
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.MetaDataProducerInterface;
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.dashboard.nocode.HtmlWrapper;
@@ -158,8 +158,8 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
- int recordsUpdated = 0;
- boolean isReview = "preview".equals(runBackendStepInput.getStepName());
+ int recordsUpdated = 0;
+ boolean isReview = "preview".equals(runBackendStepInput.getStepName());
////////////////////////////////////////////////////////////////////////
// if a table name is given, validate it, and run for just that table //
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java
index 739619dd..5451ad19 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java
@@ -28,9 +28,9 @@ import com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomati
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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 5c7cc6b0..4dc9d6d3 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
@@ -176,9 +176,9 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(postRunOutput.getUpdatedFrontendStepList() != null)
+ if(postRunOutput.getProcessMetaDataAdjustment() != null)
{
- runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment());
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -281,10 +281,10 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null)
+ if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{
runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList());
- runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
}
////////////////////////////////////////////////
@@ -299,10 +299,10 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null)
+ if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{
runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList());
- runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
}
///////////////////////////////////////////////////////
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 1111a705..aab7331b 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
@@ -148,9 +148,9 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(postRunOutput.getUpdatedFrontendStepList() != null)
+ if(postRunOutput.getProcessMetaDataAdjustment() != null)
{
- runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment());
}
}
@@ -219,10 +219,9 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null)
+ if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{
- runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList());
- runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
}
////////////////////////////////////////////////////
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 c9addb97..6dc65081 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
@@ -145,9 +145,9 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(postRunOutput.getUpdatedFrontendStepList() != null)
+ if(postRunOutput.getProcessMetaDataAdjustment() != null)
{
- runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment());
}
}
@@ -183,10 +183,9 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //
//////////////////////////////////////////////////////////////////////
- if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null)
+ if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{
- runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList());
- runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList());
+ runBackendStepOutput.setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
}
///////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
index 10bc4212..c8693195 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
@@ -23,10 +23,10 @@ package com.kingsrook.qqq.backend.core.processes.implementations.savedreports;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
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.metadata.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
@@ -58,7 +58,7 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
public static final String FIELD_NAME_STORAGE_TABLE_NAME = "storageTableName";
public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat";
public static final String FIELD_NAME_EMAIL_ADDRESS = "reportDestinationEmailAddress";
- public static final String FIELD_NAME_EMAIL_SUBJECT = "emailSubject";
+ public static final String FIELD_NAME_EMAIL_SUBJECT = "emailSubject";
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java
index 15717f22..b23e2575 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java
@@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.savedreports;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java
index ea73e9ff..06fd9ad2 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java
@@ -27,12 +27,12 @@ import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
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;
@@ -87,9 +87,9 @@ public class DeleteSharedRecordProcess implements BackendStep, MetaDataProducerI
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
- String tableName = runBackendStepInput.getValueString("tableName");
- String recordIdString = runBackendStepInput.getValueString("recordId");
- Integer shareId = runBackendStepInput.getValueInteger("shareId");
+ String tableName = runBackendStepInput.getValueString("tableName");
+ String recordIdString = runBackendStepInput.getValueString("recordId");
+ Integer shareId = runBackendStepInput.getValueInteger("shareId");
Objects.requireNonNull(tableName, "Missing required input: tableName");
Objects.requireNonNull(recordIdString, "Missing required input: recordId");
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java
index 406bea55..5158c841 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java
@@ -28,12 +28,12 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.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.MetaDataProducerInterface;
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;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java
index 5bf42978..7dda49a5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java
@@ -37,7 +37,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.query.QCriteriaOperator;
@@ -47,6 +46,7 @@ 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.MetaDataProducerInterface;
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;
@@ -134,7 +134,7 @@ public class GetSharedRecordsProcess implements BackendStep, MetaDataProducerInt
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// iterate results, building QRecords to output - note - we'll need to collect ids, then look them up in audience-source tables //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- ArrayList resultList = new ArrayList<>();
+ ArrayList resultList = new ArrayList<>();
ListingHash audienceIds = new ListingHash<>();
for(QRecord record : queryOutput.getRecords())
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java
index c3ba8688..1705b460 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java
@@ -32,13 +32,13 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
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;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java
index b7382f24..4e9b214a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java
@@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.locks;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java
index 9c3840ca..efae5bab 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java
@@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java
index 408ec1b8..2bade6b4 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java
@@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java
index 11492b06..8b495550 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java
@@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java
index 123b48c9..841c4cd7 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java
@@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java
index 5d141d18..276d62f5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java
@@ -25,10 +25,10 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes;
import java.util.List;
import java.util.function.BiFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java
index 7d81e31f..2c9ff778 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java
@@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.MetaDataProducerInterface;
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.dashboard.nocode.WidgetHtmlLine;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java
index 772e1bd0..33b2caea 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java
@@ -25,10 +25,10 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes;
import java.util.List;
import java.util.function.BiFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
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.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java
new file mode 100644
index 00000000..4b90666f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java
@@ -0,0 +1,87 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.utils;
+
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.ClassPath;
+
+
+/*******************************************************************************
+ ** Utilities for reading classes - e.g., finding all in a package
+ *******************************************************************************/
+@SuppressWarnings("ALL") // the api we're using here, from google, is marked Beta
+public class ClassPathUtils
+{
+ private static ImmutableSet topLevelClasses;
+
+
+
+ /*******************************************************************************
+ ** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection
+ **
+ *******************************************************************************/
+ public static List> getClassesInPackage(String packageName) throws IOException
+ {
+ List> classes = new ArrayList<>();
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+
+ for(ClassPath.ClassInfo info : getTopLevelClasses(loader))
+ {
+ if(info.getName().startsWith(packageName))
+ {
+ classes.add(info.load());
+ }
+ }
+
+ return (classes);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static ImmutableSet getTopLevelClasses(ClassLoader loader) throws IOException
+ {
+ if(topLevelClasses == null)
+ {
+ topLevelClasses = ClassPath.from(loader).getTopLevelClasses();
+ }
+
+ return (topLevelClasses);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void clearTopLevelClassCache()
+ {
+ topLevelClasses = null;
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java
index 44d6e8e6..58d85069 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java
@@ -31,9 +31,13 @@ import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
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.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNodeType;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
@@ -41,9 +45,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMe
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
+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.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
@@ -350,4 +358,118 @@ class MetaDataActionTest extends BaseTest
assertTrue(personMemoryTable.getDeletePermission());
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFilter() throws QException
+ {
+ QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(MetaDataAction.class);
+
+ //////////////////////////////////////////////////////
+ // run default version, and assert tables are found //
+ //////////////////////////////////////////////////////
+ MetaDataOutput result = new MetaDataAction().execute(new MetaDataInput());
+ assertFalse(result.getTables().isEmpty(), "should be some tables");
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // run again (with the same instance as before) to assert about memoization of the filter based on the QInstance //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ new MetaDataAction().execute(new MetaDataInput());
+ assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("Using new default")).hasSize(1);
+
+ /////////////////////////////////////////////////////////////
+ // set up new instance to use a custom filter, to deny all //
+ /////////////////////////////////////////////////////////////
+ QInstance instance = TestUtils.defineInstance();
+ instance.setMetaDataFilter(new QCodeReference(DenyAllFilter.class));
+ reInitInstanceInContext(instance);
+
+ /////////////////////////////////////////////////////
+ // re-run, and assert all tables are filtered away //
+ /////////////////////////////////////////////////////
+ result = new MetaDataAction().execute(new MetaDataInput());
+ assertTrue(result.getTables().isEmpty(), "should be no tables");
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // run again (with the same instance as before) to assert about memoization of the filter based on the QInstance //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ new MetaDataAction().execute(new MetaDataInput());
+ assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("filter of type: DenyAllFilter")).hasSize(1);
+
+ QLogger.deactivateCollectingLoggerForClass(MetaDataAction.class);
+
+ ////////////////////////////////////////////////////////////
+ // run now with the AllowAllFilter, confirm we get tables //
+ ////////////////////////////////////////////////////////////
+ instance = TestUtils.defineInstance();
+ instance.setMetaDataFilter(new QCodeReference(AllowAllMetaDataFilter.class));
+ reInitInstanceInContext(instance);
+ result = new MetaDataAction().execute(new MetaDataInput());
+ assertFalse(result.getTables().isEmpty(), "should be some tables");
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static class DenyAllFilter implements MetaDataFilterInterface
+ {
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowTable(MetaDataInput input, QTableMetaData table)
+ {
+ return false;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowProcess(MetaDataInput input, QProcessMetaData process)
+ {
+ return false;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowReport(MetaDataInput input, QReportMetaData report)
+ {
+ return false;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowApp(MetaDataInput input, QAppMetaData app)
+ {
+ return false;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget)
+ {
+ return false;
+ }
+ }
+
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java
new file mode 100644
index 00000000..1d989f96
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java
@@ -0,0 +1,327 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.processes;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.ProcessStepFlow;
+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.QStateMachineStep;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for RunProcessAction
+ *******************************************************************************/
+class RunProcessActionTest extends BaseTest
+{
+ private static List log = new ArrayList<>();
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ void beforeEach()
+ {
+ log.clear();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStateMachineTwoBackendSteps() throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData().withName("test")
+
+ /////////////////////////////////////////////////////////////////
+ // two-steps - a, points at b; b has no next-step, so it exits //
+ /////////////////////////////////////////////////////////////////
+ .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepA");
+ runBackendStepOutput.getProcessState().setNextStepName("b");
+ }))))
+
+ .addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepB");
+ }))))
+
+ .withStepFlow(ProcessStepFlow.STATE_MACHINE);
+
+ QContext.getQInstance().addProcess(process);
+
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName("test");
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
+ assertEquals(List.of("in StepA", "in StepB"), log);
+ assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStateMachineTwoFrontendOnlySteps() throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData().withName("test")
+
+ .addStep(QStateMachineStep.frontendOnly("a", new QFrontendStepMetaData().withName("aFrontend")).withDefaultNextStepName("b"))
+ .addStep(QStateMachineStep.frontendOnly("b", new QFrontendStepMetaData().withName("bFrontend")))
+
+ .withStepFlow(ProcessStepFlow.STATE_MACHINE);
+
+ QContext.getQInstance().addProcess(process);
+
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName("test");
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
+ assertThat(runProcessOutput.getProcessState().getNextStepName())
+ .isPresent().get()
+ .isEqualTo("aFrontend");
+
+ /////////////////////////////
+ // resume after a, go to b //
+ /////////////////////////////
+ input.setStartAfterStep("aFrontend");
+ runProcessOutput = new RunProcessAction().execute(input);
+ assertThat(runProcessOutput.getProcessState().getNextStepName())
+ .isPresent().get()
+ .isEqualTo("bFrontend");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStateMachineOneBackendStepReferencingItselfDoesNotInfiniteLoop() throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData().withName("test")
+
+ ///////////////////////////////////////////////////////////////
+ // set up step that always points back at itself. //
+ // since it never goes to the frontend, it'll stack overflow //
+ // (though we'll catch it ourselves before JVM does) //
+ ///////////////////////////////////////////////////////////////
+ .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepA");
+ runBackendStepOutput.getProcessState().setNextStepName("a");
+ }))))
+
+ .withStepFlow(ProcessStepFlow.STATE_MACHINE);
+
+ QContext.getQInstance().addProcess(process);
+
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName("test");
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ assertThatThrownBy(() -> new RunProcessAction().execute(input))
+ .isInstanceOf(QException.class)
+ .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 20");
+
+ ///////////////////////////////////////////////////
+ // make sure we can set a custom max-stack-depth //
+ ///////////////////////////////////////////////////
+ input.addValue("maxStateMachineProcessStepFlowStackDepth", 5);
+ assertThatThrownBy(() -> new RunProcessAction().execute(input))
+ .isInstanceOf(QException.class)
+ .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 5");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStateMachineTwoBackendStepsReferencingEachOtherDoesNotInfiniteLoop() throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData().withName("test")
+
+ ///////////////////////////////////////////////////////////////
+ // set up two steps that always points back at each other. //
+ // since it never goes to the frontend, it'll stack overflow //
+ // (though we'll catch it ourselves before JVM does) //
+ ///////////////////////////////////////////////////////////////
+ .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepA");
+ runBackendStepOutput.getProcessState().setNextStepName("b");
+ }))))
+
+ .addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepB");
+ runBackendStepOutput.getProcessState().setNextStepName("a");
+ }))))
+
+ .withStepFlow(ProcessStepFlow.STATE_MACHINE);
+
+ QContext.getQInstance().addProcess(process);
+
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName("test");
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ assertThatThrownBy(() -> new RunProcessAction().execute(input))
+ .isInstanceOf(QException.class)
+ .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 20");
+
+ ///////////////////////////////////////////////////
+ // make sure we can set a custom max-stack-depth //
+ ///////////////////////////////////////////////////
+ input.addValue("maxStateMachineProcessStepFlowStackDepth", 5);
+ assertThatThrownBy(() -> new RunProcessAction().execute(input))
+ .isInstanceOf(QException.class)
+ .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 5");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStateSequenceOfFrontendAndBackendSteps() throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData().withName("test")
+
+ .addStep(QStateMachineStep.frontendThenBackend("a",
+ new QFrontendStepMetaData().withName("aFrontend"),
+ new QBackendStepMetaData().withName("aBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepA");
+ runBackendStepOutput.getProcessState().setNextStepName("b");
+ }))))
+
+ .addStep(QStateMachineStep.frontendThenBackend("b",
+ new QFrontendStepMetaData().withName("bFrontend"),
+ new QBackendStepMetaData().withName("bBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepB");
+ runBackendStepOutput.getProcessState().setNextStepName("c");
+ }))))
+
+ .addStep(QStateMachineStep.frontendThenBackend("c",
+ new QFrontendStepMetaData().withName("cFrontend"),
+ new QBackendStepMetaData().withName("cBackend")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ log.add("in StepC");
+ runBackendStepOutput.getProcessState().setNextStepName("d");
+ }))))
+
+ .addStep(QStateMachineStep.frontendOnly("d",
+ new QFrontendStepMetaData().withName("dFrontend")))
+
+ .withStepFlow(ProcessStepFlow.STATE_MACHINE);
+
+ QContext.getQInstance().addProcess(process);
+
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName("test");
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ ////////////////////////////////////////////////////////
+ // start the process - we should be sent to aFrontend //
+ ////////////////////////////////////////////////////////
+ RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
+ assertThat(runProcessOutput.getProcessState().getNextStepName())
+ .isPresent().get()
+ .isEqualTo("aFrontend");
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // resume after aFrontend - we should run StepA (backend), and then be sent to bFrontend //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ input.setStartAfterStep("aFrontend");
+ runProcessOutput = new RunProcessAction().execute(input);
+ assertEquals(List.of("in StepA"), log);
+ assertThat(runProcessOutput.getProcessState().getNextStepName())
+ .isPresent().get()
+ .isEqualTo("bFrontend");
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // resume after bFrontend - we should run StepB (backend), and then be sent to cFrontend //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ input.setStartAfterStep("bFrontend");
+ runProcessOutput = new RunProcessAction().execute(input);
+ assertEquals(List.of("in StepA", "in StepB"), log);
+ assertThat(runProcessOutput.getProcessState().getNextStepName())
+ .isPresent().get()
+ .isEqualTo("cFrontend");
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // resume after cFrontend - we should run StepC (backend), and then be sent to dFrontend //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ input.setStartAfterStep("cFrontend");
+ runProcessOutput = new RunProcessAction().execute(input);
+ assertEquals(List.of("in StepA", "in StepB", "in StepC"), log);
+ assertThat(runProcessOutput.getProcessState().getNextStepName())
+ .isPresent().get()
+ .isEqualTo("dFrontend");
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // if we resume again here, we'll be past the end of the process, so no next-step //
+ ////////////////////////////////////////////////////////////////////////////////////
+ input.setStartAfterStep("dFrontend");
+ runProcessOutput = new RunProcessAction().execute(input);
+ assertEquals(List.of("in StepA", "in StepB", "in StepC"), log);
+ assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty();
+
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipeTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipeTest.java
new file mode 100644
index 00000000..9d651f2a
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipeTest.java
@@ -0,0 +1,81 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for DistinctFilteringRecordPipe
+ *******************************************************************************/
+class DistinctFilteringRecordPipeTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSingleFieldKey() throws QException
+ {
+ DistinctFilteringRecordPipe pipe = new DistinctFilteringRecordPipe(new UniqueKey("id"));
+ pipe.addRecord(new QRecord().withValue("id", 1));
+ pipe.addRecord(new QRecord().withValue("id", 1));
+ assertEquals(1, pipe.consumeAvailableRecords().size());
+
+ pipe.addRecord(new QRecord().withValue("id", 1));
+ assertEquals(0, pipe.consumeAvailableRecords().size());
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMultiFieldKey() throws QException
+ {
+ DistinctFilteringRecordPipe pipe = new DistinctFilteringRecordPipe(new UniqueKey("type", "name"));
+
+ ////////////////////////////
+ // add 3 distinct records //
+ ////////////////////////////
+ pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "A"));
+ pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "B"));
+ pipe.addRecord(new QRecord().withValue("type", 2).withValue("name", "B"));
+ assertEquals(3, pipe.consumeAvailableRecords().size());
+
+ ///////////////////////////////////////////////////////////////////
+ // now re-add those 3 (should all be discarded) plus one new one //
+ ///////////////////////////////////////////////////////////////////
+ pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "A"));
+ pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "B"));
+ pipe.addRecord(new QRecord().withValue("type", 2).withValue("name", "B"));
+ pipe.addRecord(new QRecord().withValue("type", 2).withValue("name", "A"));
+ assertEquals(1, pipe.consumeAvailableRecords().size());
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java
index 1cf8404a..03db7e79 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java
@@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -80,6 +81,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -340,6 +342,16 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
private String runToString(ReportFormat reportFormat, String reportName) throws Exception
+ {
+ return (runToString(reportFormat, reportName, Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now())));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String runToString(ReportFormat reportFormat, String reportName, Map inputValues) throws Exception
{
String name = "/tmp/report." + reportFormat.getExtension();
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
@@ -347,7 +359,7 @@ public class GenerateReportActionTest extends BaseTest
ReportInput reportInput = new ReportInput();
reportInput.setReportName(reportName);
reportInput.setReportDestination(new ReportDestination().withReportFormat(reportFormat).withReportOutputStream(fileOutputStream));
- reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
+ reportInput.setInputValues(inputValues);
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
return (FileUtils.readFileToString(new File(name), StandardCharsets.UTF_8));
@@ -976,4 +988,55 @@ public class GenerateReportActionTest extends BaseTest
assertThat(row.get("Home State Name")).isEqualTo("IL");
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFilterMissingValue() throws Exception
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // define a report in the instance, with an input field - then we'll run it w/o supplying the value //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ QReportMetaData report = new QReportMetaData()
+ .withName(REPORT_NAME)
+ .withDataSources(List.of(
+ new QReportDataSource()
+ .withName("persons")
+ .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withQueryFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}"))))))
+ .withViews(List.of(
+ new QReportView()
+ .withName("table1")
+ .withLabel("Table 1")
+ .withDataSourceName("persons")
+ .withType(ReportType.TABLE)
+ .withColumns(List.of(new QReportField().withName("id")))
+ ));
+
+ QInstance qInstance = QContext.getQInstance();
+ qInstance.addReport(report);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // the report should run, but with the filter removed, so it should find all (6) person records //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ insertPersonRecords(qInstance);
+ String json = runToString(ReportFormat.JSON, report.getName(), Collections.emptyMap());
+ JSONArray reportJsonArray = new JSONArray(json);
+ assertEquals(6, reportJsonArray.length());
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // re-run now, pretending to be a report that wasn't defined in meta-data, but instead was //
+ // ah-hoc-style, in which case, a missing input is defined as, should throw exception. //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ ReportInput reportInput = new ReportInput();
+ report.setName(null);
+ reportInput.setReportMetaData(report);
+ reportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.JSON).withReportOutputStream(new ByteArrayOutputStream()));
+ assertThatThrownBy(() -> new GenerateReportAction().execute(reportInput))
+ .hasMessageContaining("Missing value for criteria on field: birthDate");
+ }
+
}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStylerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStylerTest.java
new file mode 100644
index 00000000..13b37fa9
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStylerTest.java
@@ -0,0 +1,52 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
+
+
+import java.io.ByteArrayOutputStream;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import org.dhatim.fastexcel.StyleSetter;
+import org.dhatim.fastexcel.Workbook;
+import org.dhatim.fastexcel.Worksheet;
+import org.junit.jupiter.api.Test;
+
+
+/*******************************************************************************
+ ** Unit test for BoldHeaderAndFooterFastExcelStyler
+ *******************************************************************************/
+class BoldHeaderAndFooterFastExcelStylerTest extends BaseTest
+{
+
+ /*******************************************************************************
+ ** ... kinda just here to add test coverage to the class. I suppose, it
+ ** makes sure there's not an NPE inside that method at least...?
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ Workbook workbook = new Workbook(new ByteArrayOutputStream(), "Test", null);
+ Worksheet worksheet = workbook.newWorksheet("Sheet 1");
+ StyleSetter headerStyle = worksheet.range(0, 0, 1, 1).style();
+ new BoldHeaderAndFooterFastExcelStyler().styleHeaderRow(headerStyle);
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorTest.java
new file mode 100644
index 00000000..3434052b
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorTest.java
@@ -0,0 +1,60 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+
+/*******************************************************************************
+ ** Unit test for QCodeExecutor
+ *******************************************************************************/
+class QCodeExecutorTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testConvertJavaObject() throws QCodeException
+ {
+ Object input = new Object();
+ Object converted = ((QCodeExecutor) (codeReference, inputContext, executionLogger) -> null).convertJavaObject(input, null);
+ assertSame(input, converted);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testConvertObjectToJava() throws QCodeException
+ {
+ Object input = new Object();
+ Object converted = ((QCodeExecutor) (codeReference, inputContext, executionLogger) -> null).convertObjectToJava(input);
+ assertSame(input, converted);
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java
new file mode 100644
index 00000000..d5321bcc
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java
@@ -0,0 +1,65 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.instances;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for AbstractMetaDataProducerBasedQQQApplication
+ *******************************************************************************/
+class AbstractMetaDataProducerBasedQQQApplicationTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance qInstance = new TestApplication().defineQInstance();
+ assertEquals(1, qInstance.getTables().size());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static class TestApplication extends AbstractMetaDataProducerBasedQQQApplication
+ {
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getMetaDataPackageName()
+ {
+ return getClass().getPackage().getName() + ".producers";
+ }
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplicationTest.java
new file mode 100644
index 00000000..59288f64
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplicationTest.java
@@ -0,0 +1,74 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.instances;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for AbstractQQQApplication
+ *******************************************************************************/
+class AbstractQQQApplicationTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ TestApplication testApplication = new TestApplication();
+ QInstance qInstance = testApplication.defineQInstance();
+ assertEquals(1, qInstance.getTables().size());
+ assertTrue(qInstance.getTables().containsKey("testTable"));
+
+ assertThatThrownBy(() -> testApplication.defineValidatedQInstance())
+ .hasMessageContaining("validation");
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static class TestApplication extends AbstractQQQApplication
+ {
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QInstance defineQInstance() throws QException
+ {
+ QInstance qInstance = new QInstance();
+ qInstance.addTable(new QTableMetaData().withName("testTable"));
+ return (qInstance);
+ }
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
index e6651f3a..89c69734 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
@@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarChart;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer;
+import com.kingsrook.qqq.backend.core.actions.metadata.AllowAllMetaDataFilter;
import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
@@ -139,6 +140,21 @@ public class QInstanceValidatorTest extends BaseTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMetaDataFilter()
+ {
+ assertValidationFailureReasons((qInstance) -> qInstance.setMetaDataFilter(new QCodeReference(QInstanceValidator.class)),
+ "Instance metaDataFilter CodeReference is not of the expected type");
+
+ assertValidationSuccess((qInstance) -> qInstance.setMetaDataFilter(new QCodeReference(AllowAllMetaDataFilter.class)));
+ assertValidationSuccess((qInstance) -> qInstance.setMetaDataFilter(null));
+ }
+
+
+
/*******************************************************************************
** Test an instance with null backends - should throw.
**
@@ -265,6 +281,38 @@ public class QInstanceValidatorTest extends BaseTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTableFieldInlinePossibleValueSource()
+ {
+ ////////////////////////////////////////////////////
+ // make sure can't have both named and inline PVS //
+ ////////////////////////////////////////////////////
+ assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId")
+ .withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.TABLE).withTableName("person")),
+ "both a possibleValueSourceName and an inlinePossibleValueSource");
+
+ /////////////////////////////////////////////
+ // make require inline PVS to be enum type //
+ /////////////////////////////////////////////
+ assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId")
+ .withPossibleValueSourceName(null)
+ .withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.TABLE)),
+ "must have a type of ENUM");
+
+ ////////////////////////////////////////////////////
+ // make sure validation on the inline PVS happens //
+ ////////////////////////////////////////////////////
+ assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId")
+ .withPossibleValueSourceName(null)
+ .withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.ENUM)),
+ "missing enum values");
+ }
+
+
+
/*******************************************************************************
** Test that if a process specifies a table that doesn't exist, that it fails.
**
@@ -717,8 +765,8 @@ public class QInstanceValidatorTest extends BaseTest
@Test
public void test_validateFieldWithMissingPossibleValueSource()
{
- assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"),
- "Unrecognized possibleValueSourceName");
+ assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"),
+ "unrecognized possibleValueSourceName");
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/TestMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/TestMetaDataProducer.java
new file mode 100644
index 00000000..66e57b9b
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/TestMetaDataProducer.java
@@ -0,0 +1,46 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.instances.producers;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TestMetaDataProducer implements MetaDataProducerInterface
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QTableMetaData produce(QInstance qInstance) throws QException
+ {
+ return new QTableMetaData().withName("fromProducer");
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java
index 3f008fd7..20d9ac84 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java
@@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.utils.TestUtils;
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.assertThatThrownBy;
/*******************************************************************************
@@ -64,4 +65,19 @@ class EmailMessagingProviderTest extends BaseTest
);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMissingInputs()
+ {
+ assertThatThrownBy(() -> new SendMessageAction().execute(new SendMessageInput()))
+ .hasMessageContaining("provider name was not given");
+
+ assertThatThrownBy(() -> new SendMessageAction().execute(new SendMessageInput().withMessagingProviderName("notFound")))
+ .hasMessageContaining("was not found");
+ }
+
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java
index 5d2c3be7..ae137970 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java
@@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java
index 14ce9359..9e66213c 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java
@@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java
index 3e6efae7..3a5523d8 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java
@@ -161,9 +161,19 @@ class RenderSavedReportProcessTest extends BaseTest
*******************************************************************************/
private static InputStream getInputStream(RunProcessOutput runProcessOutput) throws QException
{
- String storageTableName = runProcessOutput.getValueString("storageTableName");
- String storageReference = runProcessOutput.getValueString("storageReference");
- InputStream inputStream = new MemoryStorageAction().getInputStream(new StorageInput(storageTableName).withReference(storageReference));
+ String storageTableName = runProcessOutput.getValueString("storageTableName");
+ String storageReference = runProcessOutput.getValueString("storageReference");
+
+ MemoryStorageAction memoryStorageAction = new MemoryStorageAction();
+ StorageInput storageInput = new StorageInput(storageTableName).withReference(storageReference);
+ InputStream inputStream = memoryStorageAction.getInputStream(storageInput);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // to help add a little bit of class-level code coverage, for the QStorageInterface, call a method //
+ // that has a defualt implementation in there //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ memoryStorageAction.getDownloadURL(storageInput);
+
return inputStream;
}
diff --git a/qqq-bom/pom.xml b/qqq-bom/pom.xml
index 7e3b3ac2..4b6321ce 100644
--- a/qqq-bom/pom.xml
+++ b/qqq-bom/pom.xml
@@ -74,6 +74,11 @@
qqq-middleware-api${revision}
+
+ com.kingsrook.qqq
+ qqq-openapi
+ ${revision}
+ com.kingsrook.qqqqqq-middleware-picocli
diff --git a/qqq-middleware-api/pom.xml b/qqq-middleware-api/pom.xml
index 1d9812ee..5a2d3c6a 100644
--- a/qqq-middleware-api/pom.xml
+++ b/qqq-middleware-api/pom.xml
@@ -48,13 +48,13 @@
qqq-middleware-javalin${revision}
+
+ com.kingsrook.qqq
+ qqq-openapi
+ ${revision}
+
-
- io.javalin
- javalin
- 5.1.4
- com.konghqunirest-java
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java
index 7b42895b..247242b1 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java
@@ -51,22 +51,6 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils;
import com.kingsrook.qqq.api.model.metadata.tables.ApiAssociationMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.Components;
-import com.kingsrook.qqq.api.model.openapi.Contact;
-import com.kingsrook.qqq.api.model.openapi.Content;
-import com.kingsrook.qqq.api.model.openapi.Example;
-import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue;
-import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue;
-import com.kingsrook.qqq.api.model.openapi.Info;
-import com.kingsrook.qqq.api.model.openapi.Method;
-import com.kingsrook.qqq.api.model.openapi.OpenAPI;
-import com.kingsrook.qqq.api.model.openapi.Parameter;
-import com.kingsrook.qqq.api.model.openapi.Path;
-import com.kingsrook.qqq.api.model.openapi.RequestBody;
-import com.kingsrook.qqq.api.model.openapi.Response;
-import com.kingsrook.qqq.api.model.openapi.Schema;
-import com.kingsrook.qqq.api.model.openapi.SecurityScheme;
-import com.kingsrook.qqq.api.model.openapi.Tag;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
@@ -93,6 +77,22 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
+import com.kingsrook.qqq.openapi.model.Components;
+import com.kingsrook.qqq.openapi.model.Contact;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.ExampleWithListValue;
+import com.kingsrook.qqq.openapi.model.ExampleWithSingleValue;
+import com.kingsrook.qqq.openapi.model.Info;
+import com.kingsrook.qqq.openapi.model.Method;
+import com.kingsrook.qqq.openapi.model.OpenAPI;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.Path;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Response;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.SecurityScheme;
+import com.kingsrook.qqq.openapi.model.Tag;
import io.javalin.http.ContentType;
import io.javalin.http.HttpStatus;
import org.apache.commons.lang.BooleanUtils;
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java
index ae7b2757..bc562d4b 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java
@@ -31,14 +31,14 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue;
-import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue;
-import com.kingsrook.qqq.api.model.openapi.HttpMethod;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
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.processes.QProcessMetaData;
+import com.kingsrook.qqq.openapi.model.ExampleWithListValue;
+import com.kingsrook.qqq.openapi.model.ExampleWithSingleValue;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
/*******************************************************************************
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java
index f2429917..41b11e7e 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java
@@ -27,15 +27,15 @@ import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.api.model.actions.HttpApiResponse;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface;
-import com.kingsrook.qqq.api.model.openapi.Content;
-import com.kingsrook.qqq.api.model.openapi.Response;
-import com.kingsrook.qqq.api.model.openapi.Schema;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
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.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Response;
+import com.kingsrook.qqq.openapi.model.Schema;
import org.eclipse.jetty.http.HttpStatus;
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
index 8c541218..e1412297 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
@@ -53,7 +53,6 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContaine
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.HttpMethod;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
@@ -87,6 +86,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.ContentType;
@@ -135,8 +135,17 @@ public class QJavalinApiHandler
///////////////////////////////////////////////
// static endpoints to support rapidoc pages //
///////////////////////////////////////////////
- ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-9.3.4.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT)));
- ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS)));
+ try
+ {
+ ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-9.3.8.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT)));
+ ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS)));
+ }
+ catch(IllegalArgumentException iae)
+ {
+ //////////////////////////////////////////////////////////////
+ // assume a different module already registered these paths //
+ //////////////////////////////////////////////////////////////
+ }
ApiBuilder.get("/apis.json", QJavalinApiHandler::doGetApisJson);
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java
index c9dc8be9..68db04b0 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java
@@ -22,8 +22,8 @@
package com.kingsrook.qqq.api.model.actions;
-import com.kingsrook.qqq.api.model.openapi.OpenAPI;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+import com.kingsrook.qqq.openapi.model.OpenAPI;
/*******************************************************************************
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java
index c472d976..03069285 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java
@@ -32,13 +32,13 @@ import java.util.Set;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.SecurityScheme;
-import com.kingsrook.qqq.api.model.openapi.Server;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.openapi.model.SecurityScheme;
+import com.kingsrook.qqq.openapi.model.Server;
import org.apache.commons.lang.BooleanUtils;
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java
index 78415898..32db55b1 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java
@@ -23,10 +23,10 @@ package com.kingsrook.qqq.api.model.metadata.fields;
import java.util.Map;
-import com.kingsrook.qqq.api.model.openapi.Example;
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.utils.StringUtils;
+import com.kingsrook.qqq.openapi.model.Example;
/*******************************************************************************
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java
index 5da8c160..622d9cc7 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java
@@ -29,7 +29,6 @@ import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.HttpMethod;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@@ -39,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
import org.apache.commons.lang.BooleanUtils;
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java
index 20cf7453..5530aa2c 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java
@@ -31,11 +31,6 @@ import java.util.Objects;
import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.Content;
-import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue;
-import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue;
-import com.kingsrook.qqq.api.model.openapi.Response;
-import com.kingsrook.qqq.api.model.openapi.Schema;
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.fields.QFieldMetaData;
@@ -43,6 +38,11 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.ExampleWithListValue;
+import com.kingsrook.qqq.openapi.model.ExampleWithSingleValue;
+import com.kingsrook.qqq.openapi.model.Response;
+import com.kingsrook.qqq.openapi.model.Schema;
import io.javalin.http.ContentType;
import org.eclipse.jetty.http.HttpStatus;
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java
index 1ee3e70d..2f6eae4e 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java
@@ -25,11 +25,11 @@ package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.api.model.actions.HttpApiResponse;
-import com.kingsrook.qqq.api.model.openapi.Response;
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.utils.collections.MapBuilder;
+import com.kingsrook.qqq.openapi.model.Response;
import org.eclipse.jetty.http.HttpStatus;
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java
index 0fcb94a2..97d405f0 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java
@@ -28,9 +28,6 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import com.kingsrook.qqq.api.model.openapi.Content;
-import com.kingsrook.qqq.api.model.openapi.Response;
-import com.kingsrook.qqq.api.model.openapi.Schema;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryFilterLink;
@@ -42,6 +39,9 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Response;
+import com.kingsrook.qqq.openapi.model.Schema;
import io.javalin.http.ContentType;
import org.apache.commons.lang.NotImplementedException;
import org.eclipse.jetty.http.HttpStatus;
diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java
index 66cb753c..9c198ec0 100644
--- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java
+++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java
@@ -38,7 +38,6 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessObjectOutput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessSummaryListOutput;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
-import com.kingsrook.qqq.api.model.openapi.HttpMethod;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
@@ -86,6 +85,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RenderSavedReportMetaDataProducer;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
/*******************************************************************************
diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java
index c884d47c..d5601688 100644
--- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java
+++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java
@@ -78,9 +78,10 @@ class QJavalinApiHandlerPermissionsTest extends BaseTest
}
qJavalinImplementation = new QJavalinImplementation(qInstance);
- qJavalinImplementation.startJavalinServer(PORT);
+ qJavalinImplementation.clearJavalinRoutes();
EndpointGroup routes = new QJavalinApiHandler(qInstance).getRoutes();
- qJavalinImplementation.getJavalinService().routes(routes);
+ qJavalinImplementation.addJavalinRoutes(routes);
+ qJavalinImplementation.startJavalinServer(PORT);
}
diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
index 20fc10b1..ecfb1073 100644
--- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
+++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
@@ -112,9 +112,10 @@ class QJavalinApiHandlerTest extends BaseTest
.withInitialVersion(TestUtils.V2022_Q4))));
qJavalinImplementation = new QJavalinImplementation(qInstance);
- qJavalinImplementation.startJavalinServer(PORT);
+ qJavalinImplementation.clearJavalinRoutes();
EndpointGroup routes = new QJavalinApiHandler(qInstance).getRoutes();
- qJavalinImplementation.getJavalinService().routes(routes);
+ qJavalinImplementation.addJavalinRoutes(routes);
+ qJavalinImplementation.startJavalinServer(PORT);
}
diff --git a/qqq-middleware-javalin/pom.xml b/qqq-middleware-javalin/pom.xml
index 309acce7..dc94f1ae 100644
--- a/qqq-middleware-javalin/pom.xml
+++ b/qqq-middleware-javalin/pom.xml
@@ -33,7 +33,10 @@
-
+
+
+ 1.9.10
+ 6.3.0
@@ -43,6 +46,12 @@
qqq-backend-core${revision}
+
+ com.kingsrook.qqq
+ qqq-openapi
+ ${revision}
+
+
com.kingsrook.qqqqqq-backend-module-rdbms
@@ -60,7 +69,17 @@
io.javalinjavalin
- 5.6.1
+ ${javalin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36com.konghq
@@ -74,11 +93,6 @@
2.2.220test
-
- org.slf4j
- slf4j-simple
- 1.7.36
-
@@ -105,4 +119,74 @@
+
+ src/main/java
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.10.1
+
+ 11
+ 11
+
+
+ io.javalin.community.openapi
+ openapi-annotation-processor
+ ${javalin.version}
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ com/kingsrook/qqq/middleware/javalin/executors/io/*.class
+ com/kingsrook/qqq/middleware/javalin/tools/**/*.class
+ com/kingsrook/qqq/middleware/javalin/specs/**/*.class
+
+
+
+
+ org.codehaus.mojo
+ appassembler-maven-plugin
+ 1.10
+
+
+
+ com.kingsrook.qqq.middleware.javalin.tools.ValidateAPIVersions
+ ValidateApiVersions
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.4.3
+
+ false
+
+
+ *:*
+
+ META-INF/*
+
+
+
+
+
+
+ ${plugin.shade.phase}
+
+ shade
+
+
+
+
+
+
+
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 cca918ab..1b2094be 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
@@ -63,12 +63,10 @@ import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAc
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
-import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
-import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@@ -106,7 +104,6 @@ import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSo
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
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.MetaDataProducerHelper;
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.AdornmentType;
@@ -123,6 +120,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@@ -178,7 +176,8 @@ public class QJavalinImplementation
private static int DEFAULT_PORT = 8001;
- private static Javalin service;
+ private static Javalin service;
+ private static List endpointGroups;
private static long startTime = 0;
@@ -241,8 +240,18 @@ public class QJavalinImplementation
{
// todo port from arg
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
- service = Javalin.create().start(port);
- service.routes(getRoutes());
+
+ service = Javalin.create(config ->
+ {
+ config.router.apiBuilder(getRoutes());
+
+ for(EndpointGroup endpointGroup : CollectionUtils.nonNullList(endpointGroups))
+ {
+ config.router.apiBuilder(endpointGroup);
+ }
+ }
+ ).start(port);
+
service.before(QJavalinImplementation::hotSwapQInstance);
service.before((Context context) -> context.header("Content-Type", "application/json"));
service.after(QJavalinImplementation::clearQContext);
@@ -292,7 +301,7 @@ public class QJavalinImplementation
////////////////////////////////////////////////////////////////////////////////
// clear the cache of classes in this class, so that new classes can be found //
////////////////////////////////////////////////////////////////////////////////
- MetaDataProducerHelper.clearTopLevelClassCache();
+ ClassPathUtils.clearTopLevelClassCache();
/////////////////////////////////////////////////
// try to get a new instance from the supplier //
@@ -1014,8 +1023,7 @@ public class QJavalinImplementation
QRecord record = getOutput.getRecord();
if(record == null)
{
- throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
- + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
+ throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
@@ -1078,8 +1086,7 @@ public class QJavalinImplementation
///////////////////////////////////////////////////////
if(getOutput.getRecord() == null)
{
- throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
- + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
+ throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
String mimeType = null;
@@ -1727,7 +1734,7 @@ public class QJavalinImplementation
}
catch(QUserFacingException e)
{
- handleException(HttpStatus.Code.BAD_REQUEST, context, e);
+ QJavalinUtils.handleException(HttpStatus.Code.BAD_REQUEST, context, e);
return null;
}
return reportFormat;
@@ -1816,7 +1823,7 @@ public class QJavalinImplementation
if(CollectionUtils.nullSafeHasContents(valuesParamList))
{
String valuesParam = valuesParamList.get(0);
- values = JsonUtils.toObject(valuesParam, new TypeReference<>(){});
+ values = JsonUtils.toObject(valuesParam, new TypeReference<>() {});
}
}
@@ -1867,67 +1874,7 @@ public class QJavalinImplementation
*******************************************************************************/
public static void handleException(Context context, Exception e)
{
- handleException(null, context, e);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public static void handleException(HttpStatus.Code statusCode, Context context, Exception e)
- {
- QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
- if(userFacingException != null)
- {
- if(userFacingException instanceof QNotFoundException)
- {
- statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404
- respondWithError(context, statusCode, userFacingException.getMessage());
- }
- else if(userFacingException instanceof QBadRequestException)
- {
- statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400
- respondWithError(context, statusCode, userFacingException.getMessage());
- }
- else
- {
- LOG.info("User-facing exception", e);
- statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500
- respondWithError(context, statusCode, userFacingException.getMessage());
- }
- }
- else
- {
- if(e instanceof QAuthenticationException)
- {
- respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401
- return;
- }
-
- if(e instanceof QPermissionDeniedException)
- {
- respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403
- return;
- }
-
- ////////////////////////////////
- // default exception handling //
- ////////////////////////////////
- LOG.warn("Exception in javalin request", e);
- respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500
- }
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage)
- {
- context.status(statusCode.getCode());
- context.result(JsonUtils.toJson(Map.of("error", errorMessage)));
+ QJavalinUtils.handleException(null, context, e);
}
@@ -1946,7 +1893,7 @@ public class QJavalinImplementation
** Getter for javalinMetaData
**
*******************************************************************************/
- public QJavalinMetaData getJavalinMetaData()
+ public static QJavalinMetaData getJavalinMetaData()
{
return javalinMetaData;
}
@@ -1975,7 +1922,7 @@ public class QJavalinImplementation
/*******************************************************************************
- ** Getter for qInstanceHotSwapSupplier
+ ** Getter for qInstance
*******************************************************************************/
public static QInstance getQInstance()
{
@@ -1984,6 +1931,16 @@ public class QJavalinImplementation
+ /*******************************************************************************
+ ** Setter for qInstance
+ *******************************************************************************/
+ public static void setQInstance(QInstance qInstance)
+ {
+ QJavalinImplementation.qInstance = qInstance;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -2011,4 +1968,30 @@ public class QJavalinImplementation
{
return (startTime);
}
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public void addJavalinRoutes(EndpointGroup routes)
+ {
+ if(endpointGroups == null)
+ {
+ endpointGroups = new ArrayList<>();
+ }
+ endpointGroups.add(routes);
+ }
+
+
+
+ /***************************************************************************
+ ** if restarting this class, and you want to re-run addJavalinRoutes, but
+ ** not create duplicates, well, you might want to call this method!
+ ***************************************************************************/
+ public void clearJavalinRoutes()
+ {
+ endpointGroups = null;
+ }
+
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
index 48dfef38..bb00bfab 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
@@ -40,6 +40,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
@@ -235,13 +236,13 @@ public class QJavalinProcessHandler
if(inputField.getIsRequired() && !setValue)
{
- QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]");
+ QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]");
return;
}
}
catch(Exception e)
{
- QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")");
+ QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")");
return;
}
}
@@ -432,7 +433,18 @@ public class QJavalinProcessHandler
QJavalinAccessLogger.logEndSuccess();
}
- context.result(JsonUtils.toJson(resultForCaller));
+ ///////////////////////////////////////////////////////////////////////////////////
+ // Note: originally we did not have this serializationInclusion:ALWAYS here - //
+ // which meant that null and empty values from backend would not go to frontend, //
+ // which made things like clearing out a value in a field not happen. //
+ // So, this is added to get that beneficial effect. Unclear if there are any //
+ // negative side-effects - but be aware. //
+ // One could imagine that we'd need this to be configurable in the future? //
+ ///////////////////////////////////////////////////////////////////////////////////
+ context.result(JsonUtils.toJson(resultForCaller, mapper ->
+ {
+ mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
+ }));
}
@@ -453,11 +465,19 @@ public class QJavalinProcessHandler
resultForCaller.put("values", runProcessOutput.getValues());
runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // todo - delete after all frontends look for processMetaDataAdjustment instead of updatedFrontendStepList //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
List updatedFrontendStepList = runProcessOutput.getUpdatedFrontendStepList();
if(updatedFrontendStepList != null)
{
resultForCaller.put("updatedFrontendStepList", updatedFrontendStepList);
}
+
+ if(runProcessOutput.getProcessMetaDataAdjustment() != null)
+ {
+ resultForCaller.put("processMetaDataAdjustment", runProcessOutput.getProcessMetaDataAdjustment());
+ }
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java
index daa3fe8b..f335601d 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java
@@ -22,11 +22,21 @@
package com.kingsrook.qqq.backend.javalin;
+import java.util.Map;
import java.util.Objects;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
+import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
+import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
+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 io.javalin.http.Context;
+import org.eclipse.jetty.http.HttpStatus;
/*******************************************************************************
@@ -34,6 +44,10 @@ import io.javalin.http.Context;
*******************************************************************************/
public class QJavalinUtils
{
+ private static final QLogger LOG = QLogger.getLogger(QJavalinUtils.class);
+
+
+
/*******************************************************************************
** Returns Integer if context has a valid int query parameter by the given name,
@@ -178,4 +192,64 @@ public class QJavalinUtils
return value;
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void handleException(HttpStatus.Code statusCode, Context context, Exception e)
+ {
+ QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
+ if(userFacingException != null)
+ {
+ if(userFacingException instanceof QNotFoundException)
+ {
+ statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404
+ respondWithError(context, statusCode, userFacingException.getMessage());
+ }
+ else if(userFacingException instanceof QBadRequestException)
+ {
+ statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400
+ respondWithError(context, statusCode, userFacingException.getMessage());
+ }
+ else
+ {
+ LOG.info("User-facing exception", e);
+ statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500
+ respondWithError(context, statusCode, userFacingException.getMessage());
+ }
+ }
+ else
+ {
+ if(e instanceof QAuthenticationException)
+ {
+ respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401
+ return;
+ }
+
+ if(e instanceof QPermissionDeniedException)
+ {
+ respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403
+ return;
+ }
+
+ ////////////////////////////////
+ // default exception handling //
+ ////////////////////////////////
+ LOG.warn("Exception in javalin request", e);
+ respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage)
+ {
+ context.status(statusCode.getCode());
+ context.result(JsonUtils.toJson(Map.of("error", errorMessage)));
+ }
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java
new file mode 100644
index 00000000..d18a6a03
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java
@@ -0,0 +1,529 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin;
+
+
+import java.util.List;
+import java.util.function.Consumer;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
+import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
+import io.javalin.Javalin;
+import io.javalin.http.Context;
+import org.apache.commons.lang.BooleanUtils;
+
+
+/*******************************************************************************
+ ** Second-generation qqq javalin server.
+ **
+ ** An evolution over the original QJavalinImplementation, which both managed
+ ** the javalin instance itself, but also provided all of the endpoint handlers...
+ ** This class instead just configures & starts the server.
+ **
+ ** Makes several setters available, to let application-developer choose what
+ ** standard qqq endpoints are served (e.g., frontend-material-dashboard, the
+ ** legacy-unversioned middleware, newer versioned-middleware, and additional qqq
+ ** modules or application-defined services (both provided as instances of
+ ** QJavalinRouteProviderInterface).
+ **
+ ** System property `qqq.javalin.hotSwapInstance` (defaults to false), causes the
+ ** QInstance to be re-loaded every X millis, to avoid some server restarts while
+ ** doing dev.
+ *******************************************************************************/
+public class QApplicationJavalinServer
+{
+ private static final QLogger LOG = QLogger.getLogger(QApplicationJavalinServer.class);
+
+ private final AbstractQQQApplication application;
+
+ private Integer port = 8000;
+ private boolean serveFrontendMaterialDashboard = true;
+ private boolean serveLegacyUnversionedMiddlewareAPI = true;
+ private List middlewareVersionList = List.of(new MiddlewareVersionV1());
+ private List additionalRouteProviders = null;
+ private Consumer javalinConfigurationCustomizer = null;
+
+ private long lastQInstanceHotSwapMillis;
+ private long millisBetweenHotSwaps = 2500;
+ private Consumer hotSwapCustomizer = null;
+
+ private Javalin service;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QApplicationJavalinServer(AbstractQQQApplication application)
+ {
+ this.application = application;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public void start() throws QException
+ {
+ QInstance qInstance = application.defineValidatedQInstance();
+
+ service = Javalin.create(config ->
+ {
+ if(serveFrontendMaterialDashboard)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // If you have any assets to add to the web server (e.g., logos, icons) place them at //
+ // src/main/resources/material-dashboard-overlay (or a directory of your choice //
+ // under src/main/resources) and use this line of code to tell javalin about it. //
+ // Make sure to add your app-specific directory to the javalin config before the core //
+ // material-dashboard directory, so in case the same file exists in both (e.g., //
+ // favicon.png), the app-specific one will be used. //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ config.staticFiles.add("/material-dashboard-overlay");
+
+ /////////////////////////////////////////////////////////////////////
+ // tell javalin where to find material-dashboard static web assets //
+ /////////////////////////////////////////////////////////////////////
+ config.staticFiles.add("/material-dashboard");
+
+ ////////////////////////////////////////////////////////////
+ // set the index page for the SPA from material dashboard //
+ ////////////////////////////////////////////////////////////
+ config.spaRoot.addFile("/", "material-dashboard/index.html");
+ }
+
+ ///////////////////////////////////////////
+ // add qqq routes to the javalin service //
+ ///////////////////////////////////////////
+ if(serveLegacyUnversionedMiddlewareAPI)
+ {
+ try
+ {
+ QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance);
+ config.router.apiBuilder(qJavalinImplementation.getRoutes());
+ }
+ catch(QInstanceValidationException e)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // we should be pretty comfortable that this won't happen, because we've pre-validated the instance above... //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ throw new RuntimeException(e);
+ }
+ }
+
+ /////////////////////////////////////
+ // versioned qqq middleware routes //
+ /////////////////////////////////////
+ if(CollectionUtils.nullSafeHasContents(middlewareVersionList))
+ {
+ config.router.apiBuilder(new QMiddlewareApiSpecHandler(middlewareVersionList).defineJavalinEndpointGroup());
+ for(AbstractMiddlewareVersion version : middlewareVersionList)
+ {
+ version.setQInstance(qInstance);
+ config.router.apiBuilder(version.getJavalinEndpointGroup(qInstance));
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // additional route providers (e.g., application-apis, other middlewares) //
+ ////////////////////////////////////////////////////////////////////////////
+ for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
+ {
+ routeProvider.setQInstance(qInstance);
+ config.router.apiBuilder(routeProvider.getJavalinEndpointGroup());
+ }
+ });
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // per system property, set the server to hot-swap the q instance before all routes //
+ //////////////////////////////////////////////////////////////////////////////////////
+ String hotSwapPropertyValue = System.getProperty("qqq.javalin.hotSwapInstance", "false");
+ if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(hotSwapPropertyValue)))
+ {
+ LOG.info("Server will hotSwap QInstance before requests every [" + millisBetweenHotSwaps + "] millis.");
+ service.before(context -> hotSwapQInstance());
+ }
+
+ service.before((Context context) -> context.header("Content-Type", "application/json"));
+ service.after(QJavalinImplementation::clearQContext);
+
+ ////////////////////////////////////////////////
+ // allow a configuration-customizer to be run //
+ ////////////////////////////////////////////////
+ if(javalinConfigurationCustomizer != null)
+ {
+ javalinConfigurationCustomizer.accept(service);
+ }
+
+ service.start(port);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public void stop()
+ {
+ if(this.service == null)
+ {
+ LOG.info("Stop called, but there is no javalin service, so noop.");
+ return;
+ }
+
+ this.service.stop();
+ }
+
+
+
+ /*******************************************************************************
+ ** If there's a qInstanceHotSwapSupplier, and its been a little while, replace
+ ** the qInstance with a new one from the supplier. Meant to be used while doing
+ ** development.
+ *******************************************************************************/
+ public void hotSwapQInstance()
+ {
+ long now = System.currentTimeMillis();
+ if(now - lastQInstanceHotSwapMillis < millisBetweenHotSwaps)
+ {
+ return;
+ }
+
+ lastQInstanceHotSwapMillis = now;
+
+ try
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // clear the cache of classes in this class, so that new classes can be found if a meta-data-producer is being used //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ClassPathUtils.clearTopLevelClassCache();
+
+ ///////////////////////////////////////////////////////////////
+ // try to get a new, validated instance from the application //
+ ///////////////////////////////////////////////////////////////
+ QInstance newQInstance = application.defineValidatedQInstance();
+ if(newQInstance == null)
+ {
+ LOG.warn("Got a null qInstance from the application.defineQInstance(). Not hot-swapping.");
+ return;
+ }
+
+ ////////////////////////////////////////
+ // allow a hot-swap customizer to run //
+ ////////////////////////////////////////
+ if(hotSwapCustomizer != null)
+ {
+ hotSwapCustomizer.accept(newQInstance);
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+ // pass the new qInstance into all of the objects serving qqq routes //
+ ///////////////////////////////////////////////////////////////////////
+ if(serveLegacyUnversionedMiddlewareAPI)
+ {
+ QJavalinImplementation.setQInstance(newQInstance);
+ }
+
+ if(CollectionUtils.nullSafeHasContents(middlewareVersionList))
+ {
+ for(AbstractMiddlewareVersion spec : CollectionUtils.nonNullList(middlewareVersionList))
+ {
+ spec.setQInstance(newQInstance);
+ }
+ }
+
+ for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
+ {
+ routeProvider.setQInstance(newQInstance);
+ }
+
+ LOG.info("Swapped qInstance");
+ }
+ catch(QInstanceValidationException e)
+ {
+ LOG.error("Validation Error while hot-swapping QInstance", e);
+ }
+ catch(Exception e)
+ {
+ LOG.error("Error hot-swapping QInstance", e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for port
+ *******************************************************************************/
+ public Integer getPort()
+ {
+ return (this.port);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for port
+ *******************************************************************************/
+ public void setPort(Integer port)
+ {
+ this.port = port;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for port
+ *******************************************************************************/
+ public QApplicationJavalinServer withPort(Integer port)
+ {
+ this.port = port;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void setMillisBetweenHotSwaps(long millisBetweenHotSwaps)
+ {
+ this.millisBetweenHotSwaps = millisBetweenHotSwaps;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for serveFrontendMaterialDashboard
+ *******************************************************************************/
+ public boolean getServeFrontendMaterialDashboard()
+ {
+ return (this.serveFrontendMaterialDashboard);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for serveFrontendMaterialDashboard
+ *******************************************************************************/
+ public void setServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard)
+ {
+ this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for serveFrontendMaterialDashboard
+ *******************************************************************************/
+ public QApplicationJavalinServer withServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard)
+ {
+ this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for serveLegacyUnversionedMiddlewareAPI
+ *******************************************************************************/
+ public boolean getServeLegacyUnversionedMiddlewareAPI()
+ {
+ return (this.serveLegacyUnversionedMiddlewareAPI);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for serveLegacyUnversionedMiddlewareAPI
+ *******************************************************************************/
+ public void setServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI)
+ {
+ this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for serveLegacyUnversionedMiddlewareAPI
+ *******************************************************************************/
+ public QApplicationJavalinServer withServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI)
+ {
+ this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for middlewareVersionList
+ *******************************************************************************/
+ public List getMiddlewareVersionList()
+ {
+ return (this.middlewareVersionList);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for middlewareVersionList
+ *******************************************************************************/
+ public void setMiddlewareVersionList(List middlewareVersionList)
+ {
+ this.middlewareVersionList = middlewareVersionList;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for middlewareVersionList
+ *******************************************************************************/
+ public QApplicationJavalinServer withMiddlewareVersionList(List middlewareVersionList)
+ {
+ this.middlewareVersionList = middlewareVersionList;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for additionalRouteProviders
+ *******************************************************************************/
+ public List getAdditionalRouteProviders()
+ {
+ return (this.additionalRouteProviders);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for additionalRouteProviders
+ *******************************************************************************/
+ public void setAdditionalRouteProviders(List additionalRouteProviders)
+ {
+ this.additionalRouteProviders = additionalRouteProviders;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for additionalRouteProviders
+ *******************************************************************************/
+ public QApplicationJavalinServer withAdditionalRouteProviders(List additionalRouteProviders)
+ {
+ this.additionalRouteProviders = additionalRouteProviders;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for MILLIS_BETWEEN_HOT_SWAPS
+ *******************************************************************************/
+ public long getMillisBetweenHotSwaps()
+ {
+ return (millisBetweenHotSwaps);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for MILLIS_BETWEEN_HOT_SWAPS
+ *******************************************************************************/
+ public QApplicationJavalinServer withMillisBetweenHotSwaps(long millisBetweenHotSwaps)
+ {
+ this.millisBetweenHotSwaps = millisBetweenHotSwaps;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for hotSwapCustomizer
+ *******************************************************************************/
+ public Consumer getHotSwapCustomizer()
+ {
+ return (this.hotSwapCustomizer);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for hotSwapCustomizer
+ *******************************************************************************/
+ public void setHotSwapCustomizer(Consumer hotSwapCustomizer)
+ {
+ this.hotSwapCustomizer = hotSwapCustomizer;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for hotSwapCustomizer
+ *******************************************************************************/
+ public QApplicationJavalinServer withHotSwapCustomizer(Consumer hotSwapCustomizer)
+ {
+ this.hotSwapCustomizer = hotSwapCustomizer;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for javalinConfigurationCustomizer
+ *******************************************************************************/
+ public Consumer getJavalinConfigurationCustomizer()
+ {
+ return (this.javalinConfigurationCustomizer);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for javalinConfigurationCustomizer
+ *******************************************************************************/
+ public void setJavalinConfigurationCustomizer(Consumer javalinConfigurationCustomizer)
+ {
+ this.javalinConfigurationCustomizer = javalinConfigurationCustomizer;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for javalinConfigurationCustomizer
+ *******************************************************************************/
+ public QApplicationJavalinServer withJavalinConfigurationCustomizer(Consumer javalinConfigurationCustomizer)
+ {
+ this.javalinConfigurationCustomizer = javalinConfigurationCustomizer;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java
new file mode 100644
index 00000000..dabe521c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java
@@ -0,0 +1,47 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import io.javalin.apibuilder.EndpointGroup;
+
+
+/*******************************************************************************
+ ** Interface for classes that can provide a list of endpoints to a javalin
+ ** server.
+ *******************************************************************************/
+public interface QJavalinRouteProviderInterface
+{
+
+ /***************************************************************************
+ ** For initial setup when server boots, set the qInstance - but also,
+ ** e.g., for development, to do a hot-swap.
+ ***************************************************************************/
+ void setQInstance(QInstance qInstance);
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ EndpointGroup getJavalinEndpointGroup();
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java
new file mode 100644
index 00000000..7988435c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java
@@ -0,0 +1,269 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin;
+
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+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.YamlUtils;
+import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
+import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
+import com.kingsrook.qqq.openapi.model.OpenAPI;
+import io.javalin.apibuilder.ApiBuilder;
+import io.javalin.apibuilder.EndpointGroup;
+import io.javalin.http.ContentType;
+import io.javalin.http.Context;
+import org.apache.commons.io.IOUtils;
+
+
+/*******************************************************************************
+ ** javalin-handler that serves both rapidoc static html/css/js files, and
+ ** dynamically generated openapi json/yaml, for a given list of qqq middleware
+ ** versions
+ *******************************************************************************/
+public class QMiddlewareApiSpecHandler
+{
+ private final List middlewareVersionList;
+ private final String basePath;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QMiddlewareApiSpecHandler(List middlewareVersionList)
+ {
+ this(middlewareVersionList, "qqq");
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QMiddlewareApiSpecHandler(List middlewareVersionList, String basePath)
+ {
+ this.middlewareVersionList = middlewareVersionList;
+ this.basePath = basePath.replaceFirst("^/+", "").replaceFirst("/+$", "");;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public EndpointGroup defineJavalinEndpointGroup()
+ {
+ return (() ->
+ {
+ ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> serveResource(context, "rapidoc/rapidoc-9.3.8.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT)));
+ ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS)));
+ ApiBuilder.get("/images/qqq-api-logo.png", (context) -> serveResource(context, "images/qqq-on-crown-trans-160x80.png", MapBuilder.of("Content-Type", ContentType.IMAGE_PNG.getMimeType())));
+
+ //////////////////////////////////////////////
+ // default page is the current version spec //
+ //////////////////////////////////////////////
+ ApiBuilder.get("/" + basePath + "/", context -> doSpecHtml(context));
+ ApiBuilder.get("/" + basePath + "/versions.json", context -> doVersions(context));
+
+ ////////////////////////////////////////////
+ // default page for a version is its spec //
+ ////////////////////////////////////////////
+ for(AbstractMiddlewareVersion middlewareSpec : middlewareVersionList)
+ {
+ String version = middlewareSpec.getVersion();
+ String versionPath = "/" + basePath + "/" + version;
+ ApiBuilder.get(versionPath + "/", context -> doSpecHtml(context, version));
+
+ ///////////////////////////////////////////
+ // add known paths for specs & docs page //
+ ///////////////////////////////////////////
+ ApiBuilder.get(versionPath + "/openapi.yaml", context -> doSpecYaml(context, version));
+ ApiBuilder.get(versionPath + "/openapi.json", context -> doSpecJson(context, version));
+ ApiBuilder.get(versionPath + "/openapi.html", context -> doSpecHtml(context, version));
+ }
+ });
+ }
+
+
+
+ /*******************************************************************************
+ ** list the versions in this api
+ *******************************************************************************/
+ private void doVersions(Context context)
+ {
+ Map rs = new HashMap<>();
+
+ List supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList();
+ String currentVersion = supportedVersions.get(supportedVersions.size() - 1);
+
+ rs.put("supportedVersions", supportedVersions);
+ rs.put("currentVersion", currentVersion);
+
+ context.contentType(ContentType.APPLICATION_JSON);
+ context.result(JsonUtils.toJson(rs));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void serveResource(Context context, String resourcePath, Map headers)
+ {
+ InputStream resourceAsStream = QJavalinImplementation.class.getClassLoader().getResourceAsStream(resourcePath);
+ for(Map.Entry entry : CollectionUtils.nonNullMap(headers).entrySet())
+ {
+ context.header(entry.getKey(), entry.getValue());
+ }
+ context.result(resourceAsStream);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void doSpecYaml(Context context, String version)
+ {
+ try
+ {
+ OpenAPI openAPI = new MiddlewareVersionV1().generateOpenAPIModel(basePath);
+ context.contentType(ContentType.APPLICATION_YAML);
+ context.result(YamlUtils.toYaml(openAPI));
+ }
+ catch(Exception e)
+ {
+ QJavalinImplementation.handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void doSpecJson(Context context, String version)
+ {
+ try
+ {
+ OpenAPI openAPI = new MiddlewareVersionV1().generateOpenAPIModel(basePath);
+ context.contentType(ContentType.APPLICATION_JSON);
+ context.result(JsonUtils.toJson(openAPI));
+ }
+ catch(Exception e)
+ {
+ QJavalinImplementation.handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void doSpecHtml(Context context)
+ {
+ String version = null;
+
+ try
+ {
+ version = context.pathParam("version");
+ }
+ catch(Exception e)
+ {
+ ////////////////
+ // leave null //
+ ////////////////
+ }
+
+ if(!StringUtils.hasContent(version))
+ {
+ List supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList();
+ version = supportedVersions.get(supportedVersions.size() - 1);
+ }
+
+ doSpecHtml(context, version);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void doSpecHtml(Context context, String version)
+ {
+ try
+ {
+ //////////////////////////////////
+ // read html from resource file //
+ //////////////////////////////////
+ InputStream resourceAsStream = QMiddlewareApiSpecHandler.class.getClassLoader().getResourceAsStream("rapidoc/rapidoc-container.html");
+ String html = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8);
+
+ /////////////////////////////////
+ // do replacements in the html //
+ /////////////////////////////////
+ html = html.replace("{spec-url}", "/" + basePath + "/" + version + "/openapi.json");
+ html = html.replace("{version}", version);
+ html = html.replace("{primaryColor}", "#444444");
+ html = html.replace("{navLogoImg}", "");
+
+ Optional middlewareSpec = middlewareVersionList.stream().filter(msi -> msi.getVersion().equals(version)).findFirst();
+ if(middlewareSpec.isEmpty())
+ {
+ throw (new QUserFacingException("Unrecognized version: " + version));
+ }
+
+ OpenAPI openAPI = middlewareSpec.get().generateOpenAPIModel(basePath);
+ html = html.replace("{title}", openAPI.getInfo().getTitle() + " - " + version);
+
+ StringBuilder otherVersionOptions = new StringBuilder();
+ for(AbstractMiddlewareVersion otherVersionSpec : middlewareVersionList)
+ {
+ otherVersionOptions.append("");
+ }
+
+ html = html.replace("{otherVersionOptions}", otherVersionOptions.toString());
+
+ context.contentType(ContentType.HTML);
+ context.result(html);
+ }
+ catch(Exception e)
+ {
+ QJavalinImplementation.handleException(context, e);
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java
new file mode 100644
index 00000000..c83a98d5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java
@@ -0,0 +1,41 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public abstract class AbstractMiddlewareExecutor
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public abstract void execute(INPUT input, OUTPUT output) throws QException;
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java
new file mode 100644
index 00000000..a630feb7
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java
@@ -0,0 +1,46 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuthenticationMetaDataExecutor extends AbstractMiddlewareExecutor
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(AuthenticationMetaDataInput input, AuthenticationMetaDataOutputInterface output) throws QException
+ {
+ output.setAuthenticationMetaData(QContext.getQInstance().getAuthentication());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java
new file mode 100644
index 00000000..7a379fd5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java
@@ -0,0 +1,224 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import java.util.HashMap;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+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.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ExecutorSessionUtils
+{
+ private static final QLogger LOG = QLogger.getLogger(ExecutorSessionUtils.class);
+
+ public static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
+ public static final String SESSION_ID_COOKIE_NAME = "sessionId";
+ public static final String API_KEY_NAME = "apiKey";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QSession setupSession(Context context, QInstance qInstance) throws QModuleDispatchException, QAuthenticationException
+ {
+ QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
+
+ try
+ {
+ Map authenticationContext = new HashMap<>();
+
+ String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
+ String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
+ String authorizationHeaderValue = context.header("Authorization");
+ String apiKeyHeaderValue = context.header("x-api-key");
+
+ if(StringUtils.hasContent(sessionIdCookieValue))
+ {
+ ///////////////////////////////////////////////////////
+ // sessionId - maybe used by table-based auth module //
+ ///////////////////////////////////////////////////////
+ authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue);
+ }
+ else if(StringUtils.hasContent(sessionUuidCookieValue))
+ {
+ ///////////////////////////////////////////////////////////////////////////
+ // session UUID - known to be used by auth0 module (in aug. 2023 update) //
+ ///////////////////////////////////////////////////////////////////////////
+ authenticationContext.put(Auth0AuthenticationModule.SESSION_UUID_KEY, sessionUuidCookieValue);
+ }
+ else if(apiKeyHeaderValue != null)
+ {
+ /////////////////////////////////////////////////////////////////
+ // next, look for an api key header: //
+ // this will be used to look up auth0 values via an auth table //
+ /////////////////////////////////////////////////////////////////
+ authenticationContext.put(API_KEY_NAME, apiKeyHeaderValue);
+ }
+ else if(authorizationHeaderValue != null)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // second, look for the authorization header: //
+ // either with a "Basic " prefix (for a username:password pair) //
+ // or with a "Bearer " prefix (for a token that can be handled the same as a sessionId cookie) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ processAuthorizationValue(authenticationContext, authorizationHeaderValue);
+ }
+ else
+ {
+ try
+ {
+ String authorizationFormValue = context.formParam("Authorization");
+ if(StringUtils.hasContent(authorizationFormValue))
+ {
+ processAuthorizationValue(authenticationContext, authorizationFormValue);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.info("Exception looking for Authorization formParam", e);
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // put the qInstance into context - but no session yet (since, the whole point of this call is to setup the session!) //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.init(qInstance, null);
+ QSession session = authenticationModule.createSession(qInstance, authenticationContext);
+ QContext.init(qInstance, session, null, null);
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // todo - this must be moved ... not exactly sure where, but into some spec. //
+ ///////////////////////////////////////////////////////////////////////////////
+ // String tableVariant = QJavalinUtils.getFormParamOrQueryParam(context, "tableVariant");
+ // if(StringUtils.hasContent(tableVariant))
+ // {
+ // JSONObject variant = new JSONObject(tableVariant);
+ // QContext.getQSession().setBackendVariants(MapBuilder.of(variant.getString("type"), variant.getInt("id")));
+ // }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ // if we got a session id cookie in, then send it back with updated cookie age //
+ /////////////////////////////////////////////////////////////////////////////////
+ if(authenticationModule.usesSessionIdCookie())
+ {
+ context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE);
+ }
+
+ setUserTimezoneOffsetMinutesInSession(context, session);
+ setUserTimezoneInSession(context, session);
+
+ return (session);
+ }
+ catch(QAuthenticationException qae)
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // if exception caught, clear out the cookie so the frontend will reauthorize //
+ ////////////////////////////////////////////////////////////////////////////////
+ if(authenticationModule.usesSessionIdCookie())
+ {
+ context.removeCookie(SESSION_ID_COOKIE_NAME);
+ }
+
+ throw (qae);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void processAuthorizationValue(Map authenticationContext, String authorizationHeaderValue)
+ {
+ String basicPrefix = "Basic ";
+ String bearerPrefix = "Bearer ";
+ if(authorizationHeaderValue.startsWith(basicPrefix))
+ {
+ authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, "");
+ authenticationContext.put(Auth0AuthenticationModule.BASIC_AUTH_KEY, authorizationHeaderValue);
+ }
+ else if(authorizationHeaderValue.startsWith(bearerPrefix))
+ {
+ authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, "");
+ authenticationContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, authorizationHeaderValue);
+ }
+ else
+ {
+ LOG.debug("Authorization value did not have Basic or Bearer prefix. [" + authorizationHeaderValue + "]");
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void setUserTimezoneOffsetMinutesInSession(Context context, QSession session)
+ {
+ String userTimezoneOffsetMinutes = context.header("X-QQQ-UserTimezoneOffsetMinutes");
+ if(StringUtils.hasContent(userTimezoneOffsetMinutes))
+ {
+ try
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // even though we're putting it in the session as a string, go through parse int, to make sure it's a valid int. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ session.setValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES, String.valueOf(Integer.parseInt(userTimezoneOffsetMinutes)));
+ }
+ catch(Exception e)
+ {
+ LOG.debug("Received non-integer value for X-QQQ-UserTimezoneOffsetMinutes header: " + userTimezoneOffsetMinutes);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void setUserTimezoneInSession(Context context, QSession session)
+ {
+ String userTimezone = context.header("X-QQQ-UserTimezone");
+ if(StringUtils.hasContent(userTimezone))
+ {
+ session.setValue(QSession.VALUE_KEY_USER_TIMEZONE, userTimezone);
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java
new file mode 100644
index 00000000..64beb3cd
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java
@@ -0,0 +1,75 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ManageSessionExecutor extends AbstractMiddlewareExecutor
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(ManageSessionInput input, ManageSessionOutputInterface output) throws QException
+ {
+ QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication());
+
+ Map authContext = new HashMap<>();
+ authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, input.getAccessToken());
+ authContext.put(Auth0AuthenticationModule.DO_STORE_USER_SESSION_KEY, "true");
+
+ /////////////////////////////////
+ // (try to) create the session //
+ /////////////////////////////////
+ QSession session = authenticationModule.createSession(QContext.getQInstance(), authContext);
+
+ //////////////////
+ // build output //
+ //////////////////
+ output.setUuid(session.getUuid());
+
+ if(session.getValuesForFrontend() != null)
+ {
+ LinkedHashMap valuesForFrontend = new LinkedHashMap<>(session.getValuesForFrontend());
+ output.setValues(valuesForFrontend);
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java
new file mode 100644
index 00000000..b16d8496
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java
@@ -0,0 +1,58 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MetaDataExecutor extends AbstractMiddlewareExecutor
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(MetaDataInput input, MetaDataOutputInterface output) throws QException
+ {
+ com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput actionInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput();
+
+ actionInput.setMiddlewareName(input.getMiddlewareName());
+ actionInput.setMiddlewareVersion(input.getMiddlewareVersion());
+ actionInput.setFrontendName(input.getFrontendName());
+ actionInput.setFrontendVersion(input.getFrontendVersion());
+ actionInput.setApplicationName(input.getApplicationName());
+ actionInput.setApplicationVersion(input.getApplicationVersion());
+
+ MetaDataAction metaDataAction = new MetaDataAction();
+ MetaDataOutput metaDataOutput = metaDataAction.execute(actionInput);
+ output.setMetaDataOutput(metaDataOutput);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java
new file mode 100644
index 00000000..adaededc
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java
@@ -0,0 +1,186 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
+import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
+import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
+import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+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.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.executors.utils.ProcessExecutorUtils;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessInitOrStepExecutor extends AbstractMiddlewareExecutor
+{
+ private static final QLogger LOG = QLogger.getLogger(ProcessInitOrStepExecutor.class);
+
+
+
+ /***************************************************************************
+ ** Note: implementation of the output interface here, it wants to know what
+ ** type it's going to be first, so, be polite and always call .setType before
+ ** any other setters.
+ ***************************************************************************/
+ @Override
+ public void execute(ProcessInitOrStepInput input, ProcessInitOrStepOrStatusOutputInterface output) throws QException
+ {
+ Exception returningException = null;
+
+ String processName = input.getProcessName();
+ String startAfterStep = input.getStartAfterStep();
+ String processUUID = input.getProcessUUID();
+
+ if(processUUID == null)
+ {
+ processUUID = UUID.randomUUID().toString();
+ }
+
+ LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
+ : "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
+
+ try
+ {
+ RunProcessInput runProcessInput = new RunProcessInput();
+ QContext.pushAction(runProcessInput);
+
+ runProcessInput.setProcessName(processName);
+ runProcessInput.setFrontendStepBehavior(input.getFrontendStepBehavior());
+ runProcessInput.setProcessUUID(processUUID);
+ runProcessInput.setStartAfterStep(startAfterStep);
+ runProcessInput.setValues(Objects.requireNonNullElseGet(input.getValues(), HashMap::new));
+
+ if(input.getRecordsFilter() != null)
+ {
+ runProcessInput.setCallback(new QProcessCallback()
+ {
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QQueryFilter getQueryFilter()
+ {
+ return (input.getRecordsFilter());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map getFieldValues(List fields)
+ {
+ return (Collections.emptyMap());
+ }
+ });
+ }
+
+ String reportName = ValueUtils.getValueAsString(runProcessInput.getValue("reportName"));
+ QJavalinAccessLogger.logStart(startAfterStep == null ? "processInit" : "processStep", logPair("processName", processName), logPair("processUUID", processUUID),
+ StringUtils.hasContent(startAfterStep) ? logPair("startAfterStep", startAfterStep) : null,
+ StringUtils.hasContent(reportName) ? logPair("reportName", reportName) : null);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // important to do this check AFTER the runProcessInput is populated with values from context - //
+ // e.g., in case things like a reportName are set in here //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ PermissionsHelper.checkProcessPermissionThrowing(runProcessInput, processName);
+
+ ////////////////////////////////////////
+ // run the process as an async action //
+ ////////////////////////////////////////
+ RunProcessOutput runProcessOutput = new AsyncJobManager().startJob(processName, input.getStepTimeoutMillis(), TimeUnit.MILLISECONDS, (callback) ->
+ {
+ runProcessInput.setAsyncJobCallback(callback);
+ return (new RunProcessAction().execute(runProcessInput));
+ });
+
+ LOG.debug("Process result error? " + runProcessOutput.getException());
+ for(QFieldMetaData outputField : QContext.getQInstance().getProcess(runProcessInput.getProcessName()).getOutputFields())
+ {
+ LOG.debug("Process result output value: " + outputField.getName() + ": " + runProcessOutput.getValues().get(outputField.getName()));
+ }
+
+ ProcessExecutorUtils.serializeRunProcessResultForCaller(output, processName, runProcessOutput);
+ QJavalinAccessLogger.logProcessSummary(processName, processUUID, runProcessOutput);
+ }
+ catch(JobGoingAsyncException jgae)
+ {
+ output.setType(ProcessInitOrStepOrStatusOutputInterface.Type.JOB_STARTED);
+ output.setJobUUID(jgae.getJobUUID());
+ }
+ catch(QPermissionDeniedException | QAuthenticationException e)
+ {
+ throw (e);
+ }
+ catch(Exception e)
+ {
+ //////////////////////////////////////////////////////////////////////////////
+ // our other actions in here would do: handleException(context, e); //
+ // which would return a 500 to the client. //
+ // but - other process-step actions, they always return a 200, just with an //
+ // optional error message - so - keep all of the processes consistent. //
+ //////////////////////////////////////////////////////////////////////////////
+ returningException = e;
+ ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, e);
+ }
+
+ output.setProcessUUID(processUUID);
+
+ if(returningException != null)
+ {
+ QJavalinAccessLogger.logEndFail(returningException);
+ }
+ else
+ {
+ QJavalinAccessLogger.logEndSuccess();
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java
new file mode 100644
index 00000000..2b1d4e93
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java
@@ -0,0 +1,65 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
+import com.kingsrook.qqq.backend.core.context.QContext;
+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.ProcessMetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessMetaDataExecutor extends AbstractMiddlewareExecutor
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(ProcessMetaDataInput input, ProcessMetaDataOutputInterface output) throws QException
+ {
+ com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput processMetaDataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput();
+
+ String processName = input.getProcessName();
+ QProcessMetaData process = QContext.getQInstance().getProcess(processName);
+ if(process == null)
+ {
+ throw (new QNotFoundException("Process [" + processName + "] was not found."));
+ }
+ PermissionsHelper.checkProcessPermissionThrowing(processMetaDataInput, processName);
+
+ processMetaDataInput.setProcessName(processName);
+ ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction();
+ ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput);
+
+ output.setProcessMetaData(processMetaDataOutput.getProcess());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java
new file mode 100644
index 00000000..c0396a83
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java
@@ -0,0 +1,121 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
+import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
+import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessStatusInput;
+import com.kingsrook.qqq.middleware.javalin.executors.utils.ProcessExecutorUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessStatusExecutor extends AbstractMiddlewareExecutor
+{
+ private static final QLogger LOG = QLogger.getLogger(ProcessStatusExecutor.class);
+
+
+
+ /***************************************************************************
+ ** Note: implementation of the output interface here, it wants to know what
+ ** type it's going to be first, so, be polite and always call .setType before
+ ** any other setters.
+ ***************************************************************************/
+ @Override
+ public void execute(ProcessStatusInput input, ProcessInitOrStepOrStatusOutputInterface output) throws QException
+ {
+ try
+ {
+ String processName = input.getProcessName();
+ String processUUID = input.getProcessUUID();
+ String jobUUID = input.getJobUUID();
+
+ LOG.debug("Request for status of process " + processUUID + ", job " + jobUUID);
+ Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
+ if(optionalJobStatus.isEmpty())
+ {
+ ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, new RuntimeException("Could not find status of process step job"));
+ }
+ else
+ {
+ AsyncJobStatus jobStatus = optionalJobStatus.get();
+
+ // resultForCaller.put("jobStatus", jobStatus);
+ LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID);
+
+ if(jobStatus.getState().equals(AsyncJobState.COMPLETE))
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // if the job is complete, get the process result from state provider, and return it //
+ // this output should look like it did if the job finished synchronously!! //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ Optional processState = RunProcessAction.getState(processUUID);
+ if(processState.isPresent())
+ {
+ RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
+ ProcessExecutorUtils.serializeRunProcessResultForCaller(output, processName, runProcessOutput);
+ QJavalinAccessLogger.logProcessSummary(processName, processUUID, runProcessOutput);
+ }
+ else
+ {
+ ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, new RuntimeException("Could not find results for process " + processUUID));
+ }
+ }
+ else if(jobStatus.getState().equals(AsyncJobState.ERROR))
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(jobStatus.getCaughtException() != null)
+ {
+ ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, jobStatus.getCaughtException());
+ }
+ }
+ else
+ {
+ output.setType(ProcessInitOrStepOrStatusOutputInterface.Type.RUNNING);
+ output.setMessage(jobStatus.getMessage());
+ output.setCurrent(jobStatus.getCurrent());
+ output.setTotal(jobStatus.getTotal());
+ }
+ }
+
+ output.setProcessUUID(processUUID);
+ }
+ catch(Exception e)
+ {
+ ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, e);
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java
new file mode 100644
index 00000000..e939781e
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java
@@ -0,0 +1,30 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AbstractMiddlewareInput
+{
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java
new file mode 100644
index 00000000..9bd32404
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java
@@ -0,0 +1,30 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface AbstractMiddlewareOutputInterface
+{
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java
new file mode 100644
index 00000000..1d97e7bd
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuthenticationMetaDataInput extends AbstractMiddlewareInput
+{
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java
new file mode 100644
index 00000000..ff6d7456
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java
@@ -0,0 +1,37 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface AuthenticationMetaDataOutputInterface extends AbstractMiddlewareOutputInterface
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void setAuthenticationMetaData(QAuthenticationMetaData qAuthenticationMetaData);
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java
new file mode 100644
index 00000000..577f38a0
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ ** generic middleware input that has no fields.
+ *******************************************************************************/
+public class EmptyMiddlewareInput extends AbstractMiddlewareInput
+{
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java
new file mode 100644
index 00000000..a5ddb568
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface EmptyMiddlewareOutputInterface extends AbstractMiddlewareOutputInterface
+{
+
+}
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Content.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java
similarity index 74%
rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Content.java
rename to qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java
index c62795ed..4fbc0124 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Content.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2023. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,44 +19,44 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.api.model.openapi;
+package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
-public class Content
+public class ManageSessionInput extends AbstractMiddlewareInput
{
- private Schema schema;
+ private String accessToken;
/*******************************************************************************
- ** Getter for schema
+ ** Getter for accessToken
*******************************************************************************/
- public Schema getSchema()
+ public String getAccessToken()
{
- return (this.schema);
+ return (this.accessToken);
}
/*******************************************************************************
- ** Setter for schema
+ ** Setter for accessToken
*******************************************************************************/
- public void setSchema(Schema schema)
+ public void setAccessToken(String accessToken)
{
- this.schema = schema;
+ this.accessToken = accessToken;
}
/*******************************************************************************
- ** Fluent setter for schema
+ ** Fluent setter for accessToken
*******************************************************************************/
- public Content withSchema(Schema schema)
+ public ManageSessionInput withAccessToken(String accessToken)
{
- this.schema = schema;
+ this.accessToken = accessToken;
return (this);
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java
new file mode 100644
index 00000000..676661a5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java
@@ -0,0 +1,45 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import java.io.Serializable;
+import java.util.Map;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface ManageSessionOutputInterface extends AbstractMiddlewareOutputInterface
+{
+ /***************************************************************************
+ ** Setter for Uuid
+ ***************************************************************************/
+ void setUuid(String uuid);
+
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ void setValues(Map values);
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java
new file mode 100644
index 00000000..2ec771bd
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java
@@ -0,0 +1,225 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MetaDataInput extends AbstractMiddlewareInput
+{
+ private String frontendName;
+ private String frontendVersion;
+
+ private String middlewareName;
+ private String middlewareVersion;
+
+ private String applicationName;
+ private String applicationVersion;
+
+
+
+ /*******************************************************************************
+ ** Getter for frontendName
+ *******************************************************************************/
+ public String getFrontendName()
+ {
+ return (this.frontendName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for frontendName
+ *******************************************************************************/
+ public void setFrontendName(String frontendName)
+ {
+ this.frontendName = frontendName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendName
+ *******************************************************************************/
+ public MetaDataInput withFrontendName(String frontendName)
+ {
+ this.frontendName = frontendName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for frontendVersion
+ *******************************************************************************/
+ public String getFrontendVersion()
+ {
+ return (this.frontendVersion);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for frontendVersion
+ *******************************************************************************/
+ public void setFrontendVersion(String frontendVersion)
+ {
+ this.frontendVersion = frontendVersion;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendVersion
+ *******************************************************************************/
+ public MetaDataInput withFrontendVersion(String frontendVersion)
+ {
+ this.frontendVersion = frontendVersion;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for middlewareName
+ *******************************************************************************/
+ public String getMiddlewareName()
+ {
+ return (this.middlewareName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for middlewareName
+ *******************************************************************************/
+ public void setMiddlewareName(String middlewareName)
+ {
+ this.middlewareName = middlewareName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for middlewareName
+ *******************************************************************************/
+ public MetaDataInput withMiddlewareName(String middlewareName)
+ {
+ this.middlewareName = middlewareName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for middlewareVersion
+ *******************************************************************************/
+ public String getMiddlewareVersion()
+ {
+ return (this.middlewareVersion);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for middlewareVersion
+ *******************************************************************************/
+ public void setMiddlewareVersion(String middlewareVersion)
+ {
+ this.middlewareVersion = middlewareVersion;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for middlewareVersion
+ *******************************************************************************/
+ public MetaDataInput withMiddlewareVersion(String middlewareVersion)
+ {
+ this.middlewareVersion = middlewareVersion;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for applicationName
+ *******************************************************************************/
+ public String getApplicationName()
+ {
+ return (this.applicationName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for applicationName
+ *******************************************************************************/
+ public void setApplicationName(String applicationName)
+ {
+ this.applicationName = applicationName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for applicationName
+ *******************************************************************************/
+ public MetaDataInput withApplicationName(String applicationName)
+ {
+ this.applicationName = applicationName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for applicationVersion
+ *******************************************************************************/
+ public String getApplicationVersion()
+ {
+ return (this.applicationVersion);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for applicationVersion
+ *******************************************************************************/
+ public void setApplicationVersion(String applicationVersion)
+ {
+ this.applicationVersion = applicationVersion;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for applicationVersion
+ *******************************************************************************/
+ public MetaDataInput withApplicationVersion(String applicationVersion)
+ {
+ this.applicationVersion = applicationVersion;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java
new file mode 100644
index 00000000..748805e1
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java
@@ -0,0 +1,39 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface MetaDataOutputInterface extends AbstractMiddlewareOutputInterface
+{
+
+ /*******************************************************************************
+ ** Setter for metaDataOutput
+ *******************************************************************************/
+ void setMetaDataOutput(MetaDataOutput metaDataOutput);
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java
new file mode 100644
index 00000000..c138b0bc
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java
@@ -0,0 +1,295 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessInitOrStepInput extends AbstractMiddlewareInput
+{
+ private String processName;
+ private Integer stepTimeoutMillis = 3000;
+ private QQueryFilter recordsFilter;
+
+ private Map values = new LinkedHashMap<>();
+
+ /////////////////////////////////////
+ // used only for 'step' (not init) //
+ /////////////////////////////////////
+ private String processUUID;
+ private String startAfterStep;
+
+ private RunProcessInput.FrontendStepBehavior frontendStepBehavior = RunProcessInput.FrontendStepBehavior.BREAK;
+
+ // todo - file??
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public enum RecordsParam
+ {
+ FILTER_JSON("filterJSON"),
+ RECORD_IDS("recordIds");
+
+
+ private final String value;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ RecordsParam(String value)
+ {
+ this.value = value;
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processName
+ *******************************************************************************/
+ public String getProcessName()
+ {
+ return (this.processName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processName
+ *******************************************************************************/
+ public void setProcessName(String processName)
+ {
+ this.processName = processName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processName
+ *******************************************************************************/
+ public ProcessInitOrStepInput withProcessName(String processName)
+ {
+ this.processName = processName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for stepTimeoutMillis
+ *******************************************************************************/
+ public Integer getStepTimeoutMillis()
+ {
+ return (this.stepTimeoutMillis);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for stepTimeoutMillis
+ *******************************************************************************/
+ public void setStepTimeoutMillis(Integer stepTimeoutMillis)
+ {
+ this.stepTimeoutMillis = stepTimeoutMillis;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for stepTimeoutMillis
+ *******************************************************************************/
+ public ProcessInitOrStepInput withStepTimeoutMillis(Integer stepTimeoutMillis)
+ {
+ this.stepTimeoutMillis = stepTimeoutMillis;
+ return (this);
+ }
+
+
+
+
+ /*******************************************************************************
+ ** Getter for recordsFilter
+ *******************************************************************************/
+ public QQueryFilter getRecordsFilter()
+ {
+ return (this.recordsFilter);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for recordsFilter
+ *******************************************************************************/
+ public void setRecordsFilter(QQueryFilter recordsFilter)
+ {
+ this.recordsFilter = recordsFilter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for recordsFilter
+ *******************************************************************************/
+ public ProcessInitOrStepInput withRecordsFilter(QQueryFilter recordsFilter)
+ {
+ this.recordsFilter = recordsFilter;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ *******************************************************************************/
+ public Map getValues()
+ {
+ return (this.values);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ public void setValues(Map values)
+ {
+ this.values = values;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for values
+ *******************************************************************************/
+ public ProcessInitOrStepInput withValues(Map values)
+ {
+ this.values = values;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for frontendStepBehavior
+ *******************************************************************************/
+ public RunProcessInput.FrontendStepBehavior getFrontendStepBehavior()
+ {
+ return (this.frontendStepBehavior);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for frontendStepBehavior
+ *******************************************************************************/
+ public void setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
+ {
+ this.frontendStepBehavior = frontendStepBehavior;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendStepBehavior
+ *******************************************************************************/
+ public ProcessInitOrStepInput withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
+ {
+ this.frontendStepBehavior = frontendStepBehavior;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processUUID
+ *******************************************************************************/
+ public String getProcessUUID()
+ {
+ return (this.processUUID);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processUUID
+ *******************************************************************************/
+ public void setProcessUUID(String processUUID)
+ {
+ this.processUUID = processUUID;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processUUID
+ *******************************************************************************/
+ public ProcessInitOrStepInput withProcessUUID(String processUUID)
+ {
+ this.processUUID = processUUID;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for startAfterStep
+ *******************************************************************************/
+ public String getStartAfterStep()
+ {
+ return (this.startAfterStep);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for startAfterStep
+ *******************************************************************************/
+ public void setStartAfterStep(String startAfterStep)
+ {
+ this.startAfterStep = startAfterStep;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for startAfterStep
+ *******************************************************************************/
+ public ProcessInitOrStepInput withStartAfterStep(String startAfterStep)
+ {
+ this.startAfterStep = startAfterStep;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java
new file mode 100644
index 00000000..f7d0d4a5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java
@@ -0,0 +1,100 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface ProcessInitOrStepOrStatusOutputInterface extends AbstractMiddlewareOutputInterface
+{
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ enum Type
+ {
+ COMPLETE, JOB_STARTED, RUNNING, ERROR;
+ }
+
+
+ /*******************************************************************************
+ ** Setter for type
+ *******************************************************************************/
+ void setType(Type type);
+
+ /*******************************************************************************
+ ** Setter for processUUID
+ *******************************************************************************/
+ void setProcessUUID(String processUUID);
+
+ /*******************************************************************************
+ ** Setter for nextStep
+ *******************************************************************************/
+ void setNextStep(String nextStep);
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ void setValues(Map values);
+
+ /*******************************************************************************
+ ** Setter for jobUUID
+ *******************************************************************************/
+ void setJobUUID(String jobUUID);
+
+ /*******************************************************************************
+ ** Setter for message
+ *******************************************************************************/
+ void setMessage(String message);
+
+ /*******************************************************************************
+ ** Setter for current
+ *******************************************************************************/
+ void setCurrent(Integer current);
+
+ /*******************************************************************************
+ ** Setter for total
+ *******************************************************************************/
+ void setTotal(Integer total);
+
+ /*******************************************************************************
+ ** Setter for error
+ *******************************************************************************/
+ void setError(String error);
+
+ /*******************************************************************************
+ ** Setter for userFacingError
+ *******************************************************************************/
+ void setUserFacingError(String userFacingError);
+
+ /*******************************************************************************
+ ** Setter for processMetaDataAdjustment
+ *******************************************************************************/
+ void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment);
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java
new file mode 100644
index 00000000..0d92c427
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java
@@ -0,0 +1,66 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessMetaDataInput extends AbstractMiddlewareInput
+{
+ private String processName;
+
+
+
+ /*******************************************************************************
+ ** Getter for processName
+ **
+ *******************************************************************************/
+ public String getProcessName()
+ {
+ return processName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processName
+ **
+ *******************************************************************************/
+ public void setProcessName(String processName)
+ {
+ this.processName = processName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processName
+ **
+ *******************************************************************************/
+ public ProcessMetaDataInput withProcessName(String processName)
+ {
+ this.processName = processName;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java
new file mode 100644
index 00000000..eb92f147
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java
@@ -0,0 +1,37 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface ProcessMetaDataOutputInterface extends AbstractMiddlewareOutputInterface
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void setProcessMetaData(QFrontendProcessMetaData processMetaData);
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java
new file mode 100644
index 00000000..2406d5a3
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java
@@ -0,0 +1,127 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessStatusInput extends AbstractMiddlewareInput
+{
+ private String processName;
+ private String processUUID;
+ private String jobUUID;
+
+
+
+ /*******************************************************************************
+ ** Getter for processName
+ *******************************************************************************/
+ public String getProcessName()
+ {
+ return (this.processName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processName
+ *******************************************************************************/
+ public void setProcessName(String processName)
+ {
+ this.processName = processName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processName
+ *******************************************************************************/
+ public ProcessStatusInput withProcessName(String processName)
+ {
+ this.processName = processName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processUUID
+ *******************************************************************************/
+ public String getProcessUUID()
+ {
+ return (this.processUUID);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processUUID
+ *******************************************************************************/
+ public void setProcessUUID(String processUUID)
+ {
+ this.processUUID = processUUID;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processUUID
+ *******************************************************************************/
+ public ProcessStatusInput withProcessUUID(String processUUID)
+ {
+ this.processUUID = processUUID;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for jobUUID
+ *******************************************************************************/
+ public String getJobUUID()
+ {
+ return (this.jobUUID);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for jobUUID
+ *******************************************************************************/
+ public void setJobUUID(String jobUUID)
+ {
+ this.jobUUID = jobUUID;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for jobUUID
+ *******************************************************************************/
+ public ProcessStatusInput withJobUUID(String jobUUID)
+ {
+ this.jobUUID = jobUUID;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java
new file mode 100644
index 00000000..449e7671
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface ProcessStatusOutputInterface extends AbstractMiddlewareOutputInterface
+{
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java
new file mode 100644
index 00000000..4060f149
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java
@@ -0,0 +1,132 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QueryMiddlewareInput extends AbstractMiddlewareInput
+{
+ private String table;
+ private QQueryFilter filter;
+ private List queryJoins;
+
+
+
+ /*******************************************************************************
+ ** Getter for table
+ *******************************************************************************/
+ public String getTable()
+ {
+ return (this.table);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for table
+ *******************************************************************************/
+ public void setTable(String table)
+ {
+ this.table = table;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for table
+ *******************************************************************************/
+ public QueryMiddlewareInput withTable(String table)
+ {
+ this.table = table;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for filter
+ *******************************************************************************/
+ public QQueryFilter getFilter()
+ {
+ return (this.filter);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for filter
+ *******************************************************************************/
+ public void setFilter(QQueryFilter filter)
+ {
+ this.filter = filter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for filter
+ *******************************************************************************/
+ public QueryMiddlewareInput withFilter(QQueryFilter filter)
+ {
+ this.filter = filter;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for queryJoins
+ *******************************************************************************/
+ public List getQueryJoins()
+ {
+ return (this.queryJoins);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for queryJoins
+ *******************************************************************************/
+ public void setQueryJoins(List queryJoins)
+ {
+ this.queryJoins = queryJoins;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for queryJoins
+ *******************************************************************************/
+ public QueryMiddlewareInput withQueryJoins(List queryJoins)
+ {
+ this.queryJoins = queryJoins;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java
new file mode 100644
index 00000000..d342e723
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/*******************************************************************************
+ ** These classes are input/output wrappers for middleware executors.
+ **
+ ** Some "empty" implementations are provided, for executors that (more likely)
+ ** take no inputs, or (less likely?) return no outputs.
+ **
+ *******************************************************************************/
+package com.kingsrook.qqq.middleware.javalin.executors.io;
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java
new file mode 100644
index 00000000..50c38d64
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java
@@ -0,0 +1,40 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/*******************************************************************************
+ ** This package contains (hopefully generally) api-version-agnostic classes
+ ** that implement the actual QQQ Middleware. That is to say, subclasses of
+ ** `AbstractMiddlewareExecutor`, which use classes from the `.io` subpackage
+ ** for I/O, to run code in a QQQ server.
+ **
+ ** As new versions of the middleware evolve, the idea is that the spec classes
+ ** for new versions will be responsible for appropriately marshalling data
+ ** in and out of the executors, via the I/O classes, with "feature flags", etc
+ ** added to those input classes as needed (say if v N+1 adds a new feature,
+ ** then a request for v N may omit the feature-flag that turns that feature on).
+ **
+ ** As functionality continues to evolve, the time may come when it's appropriate
+ ** to fork an Executor. Hypothetically, if version 5 of the QueryExecutor
+ ** bears very little resemblance to versions 1 through 4 (due to additional
+ ** pizzazz?) spawn a new QueryWithPizzazzExecutor. Of course, naming here
+ ** will be the hardest part (e.g., avoid NewNewQueryExecutorFinal2B...)
+ *******************************************************************************/
+package com.kingsrook.qqq.middleware.javalin.executors;
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java
new file mode 100644
index 00000000..9afff3b8
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java
@@ -0,0 +1,118 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.utils;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessExecutorUtils
+{
+ private static final QLogger LOG = QLogger.getLogger(ProcessExecutorUtils.class);
+
+
+
+ /*******************************************************************************
+ ** Whether a step finished synchronously or asynchronously, return its data
+ ** to the caller the same way.
+ *******************************************************************************/
+ public static void serializeRunProcessResultForCaller(ProcessInitOrStepOrStatusOutputInterface processInitOrStepOutput, String processName, RunProcessOutput runProcessOutput)
+ {
+ processInitOrStepOutput.setType(ProcessInitOrStepOrStatusOutputInterface.Type.COMPLETE);
+
+ if(runProcessOutput.getException().isPresent())
+ {
+ ////////////////////////////////////////////////////////////////
+ // per code coverage, this path may never actually get hit... //
+ ////////////////////////////////////////////////////////////////
+ serializeRunProcessExceptionForCaller(processInitOrStepOutput, runProcessOutput.getException().get());
+ }
+
+ processInitOrStepOutput.setValues(runProcessOutput.getValues());
+ // processInitOrStepOutput.setValues(getValuesForCaller(processName, runProcessOutput));
+
+ runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> processInitOrStepOutput.setNextStep(nextStep));
+
+ if(runProcessOutput.getProcessMetaDataAdjustment() != null)
+ {
+ processInitOrStepOutput.setProcessMetaDataAdjustment(runProcessOutput.getProcessMetaDataAdjustment());
+ }
+ }
+
+ // /***************************************************************************
+ // ** maybe good idea here, but... to only return fields that the frontend steps
+ // ** say they care about. yeah.
+ // ***************************************************************************/
+ // private static Map getValuesForCaller(String processName, RunProcessOutput runProcessOutput)
+ // {
+ // QProcessMetaData process = QContext.getQInstance().getProcess(processName);
+ // Map frontendFields = new LinkedHashMap<>();
+ // for(QStepMetaData step : process.getAllSteps().values())
+ // {
+ // if(step instanceof QFrontendStepMetaData frontendStepMetaData)
+ // {
+ // frontendFields.addAll(frontendStepMetaData.getAllFields());
+ // }
+ // else if(step instanceof QStateMachineStep stateMachineStep)
+ // {
+ // for(QStepMetaData subStep : stateMachineStep.getSubSteps())
+ // {
+ // // recur, etc
+ // }
+ // }
+ // }
+ //
+ // // then, only return ones in the map, eh
+ // }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void serializeRunProcessExceptionForCaller(ProcessInitOrStepOrStatusOutputInterface processInitOrStepOutput, Exception exception)
+ {
+ processInitOrStepOutput.setType(ProcessInitOrStepOrStatusOutputInterface.Type.ERROR);
+
+ QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class);
+
+ if(userFacingException != null)
+ {
+ LOG.info("User-facing exception in process", userFacingException);
+ processInitOrStepOutput.setError(userFacingException.getMessage());
+ processInitOrStepOutput.setUserFacingError(userFacingException.getMessage());
+ }
+ else
+ {
+ Throwable rootException = ExceptionUtils.getRootException(exception);
+ LOG.warn("Uncaught Exception in process", exception);
+ processInitOrStepOutput.setError("Error message: " + rootException.getMessage());
+ }
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java
new file mode 100644
index 00000000..9b0de98c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java
@@ -0,0 +1,347 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder;
+
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIHasAdditionalProperties;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+
+
+/*******************************************************************************
+ ** This class facilitates generating OpenAPI Schema objects based on reflectively
+ ** reading classes and annotations
+ *******************************************************************************/
+public class SchemaBuilder
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public Schema classToSchema(Class> c)
+ {
+ return classToSchema(c, c);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static class SchemaFromBuilder extends Schema
+ {
+ private Class> originalClass;
+
+
+
+ /*******************************************************************************
+ ** Getter for originalClass
+ **
+ *******************************************************************************/
+ @JsonIgnore
+ public Class> getOriginalClass()
+ {
+ return originalClass;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for originalClass
+ **
+ *******************************************************************************/
+ public void setOriginalClass(Class> originalClass)
+ {
+ this.originalClass = originalClass;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for originalClass
+ **
+ *******************************************************************************/
+ public SchemaFromBuilder withOriginalClass(Class> originalClass)
+ {
+ this.originalClass = originalClass;
+ return (this);
+ }
+
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private Schema classToSchema(Class> c, AnnotatedElement element)
+ {
+ SchemaFromBuilder schema = new SchemaFromBuilder();
+ schema.setOriginalClass(c);
+
+ if(c.isEnum())
+ {
+ schema.withType(Type.STRING);
+ schema.withEnumValues(Arrays.stream(c.getEnumConstants()).map(e -> String.valueOf(e)).collect(Collectors.toList()));
+ }
+ else if(c.equals(String.class))
+ {
+ schema.withType(Type.STRING);
+ }
+ else if(c.equals(Integer.class) || c.equals(BigDecimal.class))
+ {
+ schema.withType(Type.NUMBER);
+ }
+ else if(c.equals(Boolean.class))
+ {
+ schema.withType(Type.BOOLEAN);
+ }
+ else if(c.equals(List.class))
+ {
+ schema.withType(Type.ARRAY);
+ // Class> itemType = field.getType().getTypeParameters()[0].getBounds().getClass();
+
+ OpenAPIListItems openAPIListItemsAnnotation = element.getAnnotation(OpenAPIListItems.class);
+ if(openAPIListItemsAnnotation == null)
+ {
+ // todo - can this be allowed, to make a generic list? maybe.
+ // throw (new QRuntimeException("A List field [" + field.getName() + "] was missing its @OpenAPIItems annotation"));
+ }
+ else
+ {
+ if(openAPIListItemsAnnotation.useRef())
+ {
+ schema.withItems(new Schema().withRef("#/components/schemas/" + openAPIListItemsAnnotation.value().getSimpleName()));
+ }
+ else
+ {
+ Class> itemType = openAPIListItemsAnnotation.value();
+ schema.withItems(classToSchema(itemType));
+ }
+ }
+ }
+ else if(c.equals(Map.class))
+ {
+ schema.withType(Type.OBJECT);
+
+ OpenAPIMapKnownEntries openAPIMapKnownEntriesAnnotation = element.getAnnotation(OpenAPIMapKnownEntries.class);
+ if(openAPIMapKnownEntriesAnnotation != null)
+ {
+ schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName());
+ // if(openAPIMapKnownEntriesAnnotation.additionalProperties())
+ // {
+ // schema.withAdditionalProperties(true);
+ // }
+ }
+
+ OpenAPIMapValueType openAPIMapValueTypeAnnotation = element.getAnnotation(OpenAPIMapValueType.class);
+ if(openAPIMapValueTypeAnnotation != null)
+ {
+ if(openAPIMapValueTypeAnnotation.useRef())
+ {
+ schema.withAdditionalProperties(new Schema().withRef("#/components/schemas/" + openAPIMapValueTypeAnnotation.value().getSimpleName()));
+ }
+ else
+ {
+ schema.withAdditionalProperties(classToSchema(openAPIMapValueTypeAnnotation.value()));
+ }
+ }
+ }
+ else
+ {
+ OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class);
+ if(openAPIOneOfAnnotation != null)
+ {
+ String description = "[" + element + "]";
+ List oneOfList = processOneOfAnnotation(openAPIOneOfAnnotation, c, description);
+ schema.withOneOf(oneOfList);
+ }
+ else
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // else, if not a one-of then assume the schema is an object, and build out its properties //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ schema.withType(Type.OBJECT);
+
+ Map properties = new TreeMap<>();
+ schema.withProperties(properties);
+
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // if we're told to includeProperties (e.g., from ancestor classes), then go find those //
+ //////////////////////////////////////////////////////////////////////////////////////////
+ OpenAPIIncludeProperties openAPIIncludePropertiesAnnotation = c.getAnnotation(OpenAPIIncludeProperties.class);
+ if(openAPIIncludePropertiesAnnotation != null)
+ {
+ Set> ancestorClasses = Arrays.stream(openAPIIncludePropertiesAnnotation.ancestorClasses()).collect(Collectors.toSet());
+ Class> superClass = c.getSuperclass();
+ do
+ {
+ if(ancestorClasses.contains(superClass))
+ {
+ addDeclaredFieldsToProperties(superClass, properties);
+ addDeclaredMethodsToProperties(superClass, properties);
+ }
+ superClass = superClass.getSuperclass();
+ }
+ while(superClass != null);
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+ // make all declared-fields and getters in the class into properties //
+ ///////////////////////////////////////////////////////////////////////
+ addDeclaredFieldsToProperties(c, properties);
+ addDeclaredMethodsToProperties(c, properties);
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // now (after schema may have been replaced, e.g., in a recursive call), add more details to it //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ OpenAPIDescription openAPIDescriptionAnnotation = element.getAnnotation(OpenAPIDescription.class);
+ if(openAPIDescriptionAnnotation != null)
+ {
+ schema.setDescription(openAPIDescriptionAnnotation.value());
+ }
+
+ if(element.isAnnotationPresent(OpenAPIHasAdditionalProperties.class))
+ {
+ schema.withAdditionalProperties(true);
+ }
+
+ return (schema);
+ }
+
+
+
+ /***************************************************************************
+ ** Getter methods with an annotation
+ ***************************************************************************/
+ private void addDeclaredMethodsToProperties(Class> c, Map properties)
+ {
+ for(Method method : c.getDeclaredMethods())
+ {
+ OpenAPIDescription methodDescription = method.getAnnotation(OpenAPIDescription.class);
+ OpenAPIExclude openAPIExclude = method.getAnnotation(OpenAPIExclude.class);
+ if(method.getName().startsWith("get") && method.getParameterCount() == 0 && methodDescription != null && openAPIExclude == null)
+ {
+ String name = StringUtils.lcFirst(method.getName().substring(3));
+ properties.put(name, getMemberSchema(method));
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void addDeclaredFieldsToProperties(Class> c, Map properties)
+ {
+ for(Field declaredField : c.getDeclaredFields())
+ {
+ OpenAPIExclude openAPIExclude = declaredField.getAnnotation(OpenAPIExclude.class);
+ if(openAPIExclude == null)
+ {
+ properties.put(declaredField.getName(), getMemberSchema(declaredField));
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private Schema getMemberSchema(AccessibleObject member)
+ {
+ Class> type;
+ if(member instanceof Field field)
+ {
+ type = field.getType();
+ }
+ else if(member instanceof Method method)
+ {
+ type = method.getReturnType();
+ }
+ else
+ {
+ throw (new IllegalArgumentException("Unsupported AccessibleObject: " + member));
+ }
+
+ return (classToSchema(type, member));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private List processOneOfAnnotation(OpenAPIOneOf openAPIOneOfAnnotation, Class> type, String description)
+ {
+ List oneOfList = new ArrayList<>();
+
+ if(openAPIOneOfAnnotation.mode().equals(OpenAPIOneOf.Mode.PERMITTED_SUBCLASSES))
+ {
+ Class>[] permittedSubclasses = type.getPermittedSubclasses();
+ for(Class> permittedSubclass : permittedSubclasses)
+ {
+ oneOfList.add(classToSchema(permittedSubclass));
+ }
+ }
+ else if(openAPIOneOfAnnotation.mode().equals(OpenAPIOneOf.Mode.SPECIFIED_LIST))
+ {
+ for(Class> oneOfClass : openAPIOneOfAnnotation.options())
+ {
+ oneOfList.add(classToSchema(oneOfClass));
+ }
+ }
+
+ if(oneOfList.isEmpty())
+ {
+ throw (new QRuntimeException("Could not find any options to use for an @OpenAPIOneOf annotation on " + description));
+ }
+ return oneOfList;
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchema.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchema.java
new file mode 100644
index 00000000..792032ad
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchema.java
@@ -0,0 +1,46 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder;
+
+
+import com.kingsrook.qqq.openapi.model.Schema;
+
+
+/*******************************************************************************
+ ** Mark a class as eligible for running through the SchemaBuilder.
+ **
+ ** Actually not really necessary, as schemaBuilder can run on any class - but
+ ** does provide a method that a class might use to customize how it gets
+ ** schemafied.
+ *******************************************************************************/
+public interface ToSchema
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ default Schema toSchema()
+ {
+ return new SchemaBuilder().classToSchema(getClass());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIDescription.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIDescription.java
new file mode 100644
index 00000000..563b5cfe
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIDescription.java
@@ -0,0 +1,42 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@Target({ ElementType.FIELD, ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIDescription
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ String value();
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIEnumSubSet.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIEnumSubSet.java
new file mode 100644
index 00000000..0c38baa5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIEnumSubSet.java
@@ -0,0 +1,55 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.EnumSet;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIEnumSubSet
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Class extends EnumSubSet>> value();
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ interface EnumSubSet>
+ {
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ EnumSet getSubSet();
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIExclude.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIExclude.java
new file mode 100644
index 00000000..de6d6039
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIExclude.java
@@ -0,0 +1,38 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIExclude
+{
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java
new file mode 100644
index 00000000..497dc090
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java
@@ -0,0 +1,38 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIHasAdditionalProperties
+{
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIIncludeProperties.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIIncludeProperties.java
new file mode 100644
index 00000000..aeb380de
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIIncludeProperties.java
@@ -0,0 +1,42 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@Target({ ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIIncludeProperties
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Class>[] ancestorClasses() default { };
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIListItems.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIListItems.java
new file mode 100644
index 00000000..fc04792e
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIListItems.java
@@ -0,0 +1,47 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIListItems
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Class> value();
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean useRef() default false;
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapKnownEntries.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapKnownEntries.java
new file mode 100644
index 00000000..ffbc354a
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapKnownEntries.java
@@ -0,0 +1,47 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIMapKnownEntries
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Class> value();
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean useRef() default false;
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapValueType.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapValueType.java
new file mode 100644
index 00000000..e82603ce
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapValueType.java
@@ -0,0 +1,47 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIMapValueType
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Class> value();
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ boolean useRef() default false;
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIOneOf.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIOneOf.java
new file mode 100644
index 00000000..b7e8861c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIOneOf.java
@@ -0,0 +1,57 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OpenAPIOneOf
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Mode mode() default Mode.PERMITTED_SUBCLASSES;
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ Class>[] options() default { };
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ enum Mode
+ {
+ PERMITTED_SUBCLASSES,
+ SPECIFIED_LIST
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java
new file mode 100644
index 00000000..e27c5e43
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java
@@ -0,0 +1,546 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs;
+
+
+import java.io.Serializable;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.AbstractMiddlewareExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.ExecutorSessionUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareOutputInterface;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Method;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Response;
+import com.kingsrook.qqq.openapi.model.Schema;
+import io.javalin.apibuilder.ApiBuilder;
+import io.javalin.http.ContentType;
+import io.javalin.http.Context;
+import io.javalin.http.Handler;
+import org.apache.commons.lang.NotImplementedException;
+import org.json.JSONObject;
+
+
+/*******************************************************************************
+ ** Base class for individual endpoint specs.
+ ** e.g., one path, that has one "spec" (a "Method" in openapi structure),
+ ** with one implementation (executor + input & output)
+ *******************************************************************************/
+public abstract class AbstractEndpointSpec<
+ INPUT extends AbstractMiddlewareInput,
+ OUTPUT extends AbstractMiddlewareOutputInterface,
+ EXECUTOR extends AbstractMiddlewareExecutor>
+{
+ private static final QLogger LOG = QLogger.getLogger(AbstractEndpointSpec.class);
+
+ protected QInstance qInstance;
+
+ private List memoizedRequestParameters = null;
+ private RequestBody memoizedRequestBody = null;
+
+
+
+ /***************************************************************************
+ ** build the endpoint's input object from a javalin context
+ ***************************************************************************/
+ public abstract INPUT buildInput(Context context) throws Exception;
+
+
+ /***************************************************************************
+ ** build the endpoint's http response (written to the javalin context) from
+ ** an execution output object
+ ***************************************************************************/
+ public abstract void handleOutput(Context context, OUTPUT output) throws Exception;
+
+
+
+ /***************************************************************************
+ ** Construct a new instance of the executor class, based on type-argument
+ ***************************************************************************/
+ @SuppressWarnings("unchecked")
+ public EXECUTOR newExecutor()
+ {
+ Object object = newObjectFromTypeArgument(2);
+ return (EXECUTOR) object;
+ }
+
+
+
+ /***************************************************************************
+ ** Construct a new instance of the output class, based on type-argument
+ ***************************************************************************/
+ @SuppressWarnings("unchecked")
+ public OUTPUT newOutput()
+ {
+ Object object = newObjectFromTypeArgument(1);
+ return (OUTPUT) object;
+ }
+
+
+
+ /***************************************************************************
+ ** credit: https://www.baeldung.com/java-generic-type-find-class-runtime
+ ***************************************************************************/
+ private Object newObjectFromTypeArgument(int argumentIndex)
+ {
+ try
+ {
+ Type superClass = getClass().getGenericSuperclass();
+ Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[argumentIndex];
+ String className = actualTypeArgument.getTypeName().replaceAll("<.*", "");
+ Class> aClass = Class.forName(className);
+ return (aClass.getConstructor().newInstance());
+ }
+ catch(Exception e)
+ {
+ throw (new QRuntimeException("Failed to reflectively create new object from type argument", e));
+ }
+ }
+
+
+
+ /***************************************************************************
+ ** define a javalin route for the spec
+ ***************************************************************************/
+ public void defineRoute(String versionBasePath)
+ {
+ CompleteOperation completeOperation = defineCompleteOperation();
+
+ String fullPath = "/qqq/" + versionBasePath + completeOperation.getPath();
+ fullPath = fullPath.replaceAll("/+", "/");
+
+ final Handler handler = context -> serveRequest(context);
+
+ switch(completeOperation.getHttpMethod())
+ {
+ case GET -> ApiBuilder.get(fullPath, handler);
+ case POST -> ApiBuilder.post(fullPath, handler);
+ case PUT -> ApiBuilder.put(fullPath, handler);
+ case PATCH -> ApiBuilder.patch(fullPath, handler);
+ case DELETE -> ApiBuilder.delete(fullPath, handler);
+ default -> throw new IllegalStateException("Unexpected value: " + completeOperation.getHttpMethod());
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public OUTPUT serveRequest(Context context) throws Exception
+ {
+ try
+ {
+ if(isSecured())
+ {
+ ExecutorSessionUtils.setupSession(context, qInstance);
+ }
+ else
+ {
+ QContext.setQInstance(qInstance);
+ }
+
+ INPUT input = buildInput(context);
+ EXECUTOR executor = newExecutor();
+ OUTPUT output = newOutput();
+ executor.execute(input, output);
+ handleOutput(context, output);
+ return (output);
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ return (null);
+ }
+ finally
+ {
+ QContext.clear();
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected void handleException(Context context, Exception e)
+ {
+ QJavalinUtils.handleException(null, context, e);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public Method defineMethod()
+ {
+ BasicOperation basicOperation = defineBasicOperation();
+
+ Method method = new Method()
+ .withTag(basicOperation.getTag().getText())
+ .withSummary(basicOperation.getShortSummary())
+ .withDescription(basicOperation.getLongDescription())
+ .withParameters(defineRequestParameters())
+ .withRequestBody(defineRequestBody())
+ .withResponses(defineResponses());
+
+ customizeMethod(method);
+
+ return (method);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected void customizeMethod(Method method)
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ throw new NotImplementedException(getClass().getSimpleName() + " did not implement defineBasicOperation or defineMethod");
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public CompleteOperation defineCompleteOperation()
+ {
+ CompleteOperation completeOperation = new CompleteOperation(defineBasicOperation());
+ completeOperation.setMethod(defineMethod());
+ return completeOperation;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public boolean isSecured()
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ return (null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public List defineAdditionalBasicResponses()
+ {
+ return (Collections.emptyList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public Map defineResponses()
+ {
+ BasicResponse standardSuccessResponse = defineBasicSuccessResponse();
+ List basicResponseList = defineAdditionalBasicResponses();
+
+ List allBasicResponses = new ArrayList<>();
+ if(standardSuccessResponse != null)
+ {
+ allBasicResponses.add(standardSuccessResponse);
+ }
+
+ if(basicResponseList != null)
+ {
+ allBasicResponses.addAll(basicResponseList);
+ }
+
+ Map rs = new HashMap<>();
+ for(BasicResponse basicResponse : allBasicResponses)
+ {
+ Response responseObject = rs.computeIfAbsent(basicResponse.status().getCode(), (k) -> new Response());
+ responseObject.withDescription(basicResponse.description());
+ Map content = responseObject.getContent();
+ if(content == null)
+ {
+ content = new HashMap<>();
+ responseObject.setContent(content);
+ }
+
+ content.put(basicResponse.contentType(), new Content()
+ .withSchema(new Schema().withRefToSchema(basicResponse.schemaRefName()))
+ .withExamples(basicResponse.examples())
+ );
+ }
+
+ return rs;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public List defineRequestParameters()
+ {
+ return Collections.emptyList();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public RequestBody defineRequestBody()
+ {
+ return null;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public Map defineComponentSchemas()
+ {
+ return Collections.emptyMap();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected String getRequestParam(Context context, String name)
+ {
+ for(Parameter parameter : CollectionUtils.nonNullList(getMemoizedRequestParameters()))
+ {
+ if(parameter.getName().equals(name))
+ {
+ String value = switch(parameter.getIn())
+ {
+ case "path" -> context.pathParam(parameter.getName());
+ case "query" -> context.queryParam(parameter.getName());
+ default -> throw new IllegalStateException("Unexpected 'in' value for parameter [" + parameter.getName() + "]: " + parameter.getIn());
+ };
+
+ // todo - validate value vs. required?
+ // todo - validate value vs. schema?
+
+ return (value);
+ }
+ }
+
+ RequestBody requestBody = getMemoizedRequestBody();
+ if(requestBody != null)
+ {
+ String requestContentType = context.contentType();
+ if(requestContentType != null)
+ {
+ requestContentType = requestContentType.toLowerCase().replaceAll(" *;.*", "");
+ }
+
+ Content contentSpec = requestBody.getContent().get(requestContentType);
+ if(contentSpec != null && "object".equals(contentSpec.getSchema().getType()))
+ {
+ if(contentSpec.getSchema().getProperties() != null && contentSpec.getSchema().getProperties().containsKey(name))
+ {
+ String value = null;
+ if(ContentType.MULTIPART_FORM_DATA.getMimeType().equals(requestContentType))
+ {
+ value = context.formParam(name);
+ }
+ else if(ContentType.APPLICATION_JSON.getMimeType().equals(requestContentType))
+ {
+ /////////////////////////////////////////////////////////////////////////////
+ // avoid re-parsing the JSON object if getting multiple attributes from it //
+ // by stashing it in a (request) attribute. //
+ /////////////////////////////////////////////////////////////////////////////
+ Object jsonBodyAttribute = context.attribute("jsonBody");
+ JSONObject jsonObject = null;
+
+ if(jsonBodyAttribute instanceof JSONObject jo)
+ {
+ jsonObject = jo;
+ }
+
+ if(jsonObject == null)
+ {
+ jsonObject = new JSONObject(context.body());
+ context.attribute("jsonBody", jsonObject);
+ }
+
+ if(jsonObject.has(name))
+ {
+ value = jsonObject.getString(name);
+ }
+ }
+ else
+ {
+ LOG.warn("Unhandled content type: " + requestContentType);
+ }
+
+ return (value);
+ }
+ }
+ }
+
+ return (null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected Map getRequestParamMap(Context context, String name)
+ {
+ String requestParam = getRequestParam(context, name);
+ if(requestParam == null)
+ {
+ return (null);
+ }
+
+ JSONObject jsonObject = new JSONObject(requestParam);
+ Map map = new LinkedHashMap<>();
+ for(String key : jsonObject.keySet())
+ {
+ Object value = jsonObject.get(key);
+ if(value instanceof Serializable s)
+ {
+ map.put(key, s);
+ }
+ else
+ {
+ throw (new QRuntimeException("Non-serializable value in param map under key [" + name + "][" + key + "]: " + value.getClass().getSimpleName()));
+ }
+ }
+ return (map);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected Integer getRequestParamInteger(Context context, String name)
+ {
+ String requestParam = getRequestParam(context, name);
+ return ValueUtils.getValueAsInteger(requestParam);
+ }
+
+
+
+ /***************************************************************************
+ ** For initial setup when server boots, set the qInstance - but also,
+ ** e.g., for development, to do a hot-swap.
+ ***************************************************************************/
+ public void setQInstance(QInstance qInstance)
+ {
+ this.qInstance = qInstance;
+
+ //////////////////////////////////////////////////////////////////
+ // if we did a hot swap, we should clear these memoizations too //
+ //////////////////////////////////////////////////////////////////
+ memoizedRequestParameters = null;
+ memoizedRequestBody = null;
+ }
+
+
+
+ /***************************************************************************
+ ** An original implementation here was prone to race-condition-based errors:
+ *
+ ** if(memoizedRequestParameters == null)
+ ** {
+ ** memoizedRequestParameters = CollectionUtils.nonNullList(defineRequestParameters());
+ ** }
+ ** return (memoizedRequestParameters);
+ **
+ ** where between the defineX call and the return, if another thread cleared the
+ ** memoizedX field, then a null would be returned, which isn't supposed to happen.
+ ** Thus, this implementation which looks a bit more convoluted, but should
+ ** be safe(r).
+ ***************************************************************************/
+ private List getMemoizedRequestParameters()
+ {
+ List rs = memoizedRequestParameters;
+ if(rs == null)
+ {
+ rs = CollectionUtils.nonNullList(defineRequestParameters());
+ memoizedRequestParameters = rs;
+ }
+
+ return (rs);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private RequestBody getMemoizedRequestBody()
+ {
+ RequestBody rs = memoizedRequestBody;
+ if(rs == null)
+ {
+ rs = defineRequestBody();
+ memoizedRequestBody = rs;
+ }
+
+ return (rs);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java
new file mode 100644
index 00000000..d922ab33
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java
@@ -0,0 +1,361 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs;
+
+
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.openapi.model.Components;
+import com.kingsrook.qqq.openapi.model.Contact;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.Info;
+import com.kingsrook.qqq.openapi.model.Method;
+import com.kingsrook.qqq.openapi.model.OpenAPI;
+import com.kingsrook.qqq.openapi.model.Path;
+import com.kingsrook.qqq.openapi.model.Response;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.SecurityScheme;
+import com.kingsrook.qqq.openapi.model.SecuritySchemeType;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.apibuilder.EndpointGroup;
+
+
+/*******************************************************************************
+ ** Baseclass that combines multiple specs together into a single "version" of
+ ** the full qqq middleware.
+ *******************************************************************************/
+public abstract class AbstractMiddlewareVersion
+{
+ public static final QLogger LOG = QLogger.getLogger(AbstractMiddlewareVersion.class);
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public abstract String getVersion();
+
+ /***************************************************************************
+ ** hey - don't re-construct the endpoint-spec objects inside this method...
+ ***************************************************************************/
+ public abstract List> getEndpointSpecs();
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public EndpointGroup getJavalinEndpointGroup(QInstance qInstance)
+ {
+ return (() ->
+ {
+ for(AbstractEndpointSpec, ?, ?> spec : CollectionUtils.nonNullList(getEndpointSpecs()))
+ {
+ spec.defineRoute("/" + getVersion() + "/");
+ }
+ });
+ }
+
+
+
+ /***************************************************************************
+ ** For initial setup when server boots, set the qInstance - but also,
+ ** e.g., for development, to do a hot-swap.
+ ***************************************************************************/
+ public void setQInstance(QInstance qInstance)
+ {
+ getEndpointSpecs().forEach(spec -> spec.setQInstance(qInstance));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public OpenAPI generateOpenAPIModel(String basePath) throws QException
+ {
+ List> list = getEndpointSpecs();
+
+ Map paths = new LinkedHashMap<>();
+ Map componentExamples = new LinkedHashMap<>();
+
+ Set> componentClasses = new HashSet<>();
+ Map componentSchemas = new TreeMap<>();
+ buildComponentSchemasFromComponentsPackage(componentSchemas, componentClasses);
+
+ String sessionUuidCookieSchemeName = "sessionUuidCookie";
+ SecurityScheme sessionUuidCookieScheme = new SecurityScheme()
+ .withType(SecuritySchemeType.API_KEY)
+ .withName("sessionUUID")
+ .withIn("cookie");
+
+ for(AbstractEndpointSpec, ?, ?> spec : list)
+ {
+ CompleteOperation completeOperation = spec.defineCompleteOperation();
+ String fullPath = ("/" + basePath + "/" + getVersion() + "/" + completeOperation.getPath()).replaceAll("/+", "/");
+ Path path = paths.computeIfAbsent(fullPath, (k) -> new Path());
+ Method method = completeOperation.getMethod();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if this spec is supposed to be secured, but no security has been applied, then add our default security //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(spec.isSecured() && method.getSecurity() == null)
+ {
+ //////////////////////////////////////////////////////////////////////////////
+ // the N/A here refers to the lack of a 'scope' for this kind of permission //
+ //////////////////////////////////////////////////////////////////////////////
+ method.withSecurity(List.of(Map.of(sessionUuidCookieSchemeName, List.of("N/A"))));
+ }
+
+ convertMethodSchemasToRefs(method, componentClasses);
+
+ switch(completeOperation.getHttpMethod())
+ {
+ case GET ->
+ {
+ warnIfPathMethodAlreadyUsed(path.getGet(), completeOperation, spec);
+ path.withGet(method);
+ }
+ case POST ->
+ {
+ warnIfPathMethodAlreadyUsed(path.getPost(), completeOperation, spec);
+ path.withPost(method);
+ }
+ case PUT ->
+ {
+ warnIfPathMethodAlreadyUsed(path.getPut(), completeOperation, spec);
+ path.withPut(method);
+ }
+ case PATCH ->
+ {
+ warnIfPathMethodAlreadyUsed(path.getPatch(), completeOperation, spec);
+ path.withPatch(method);
+ }
+ case DELETE ->
+ {
+ warnIfPathMethodAlreadyUsed(path.getDelete(), completeOperation, spec);
+ path.withDelete(method);
+ }
+ default -> throw new IllegalStateException("Unexpected value: " + completeOperation.getHttpMethod());
+ }
+
+ for(Map.Entry entry : CollectionUtils.nonNullMap(spec.defineComponentSchemas()).entrySet())
+ {
+ if(componentSchemas.containsKey(entry.getKey()))
+ {
+ LOG.warn("More than one endpoint spec defined a componentSchema named: " + entry.getKey() + ". The last one encountered (from " + spec.getClass().getSimpleName() + ") will be used.");
+ }
+
+ componentSchemas.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ OpenAPI openAPI = new OpenAPI();
+ openAPI.withInfo(new Info()
+ .withVersion(getVersion())
+ .withTitle("QQQ Middleware API")
+ .withDescription(getDescription())
+ .withContact(new Contact().withEmail("contact@kingsrook.com"))
+ );
+
+ openAPI.withPaths(paths);
+
+ openAPI.withComponents(new Components()
+ .withSchemas(componentSchemas)
+ .withExamples(componentExamples)
+ .withSecuritySchemes(Map.of(sessionUuidCookieSchemeName, sessionUuidCookieScheme))
+ );
+
+ return openAPI;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void buildComponentSchemasFromComponentsPackage(Map componentSchemas, Set> componentClasses) throws QException
+ {
+ try
+ {
+ ////////////////////////////////////////////////////
+ // find all classes in the components sub-package //
+ ////////////////////////////////////////////////////
+ String packageName = getClass().getPackageName();
+ List> classesInPackage = ClassPathUtils.getClassesInPackage(packageName);
+ for(Class> c : classesInPackage)
+ {
+ if(c.getPackageName().matches(".*\\bcomponents\\b.*"))
+ {
+ componentClasses.add(c);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ // now that we know that full set, make any references to others schemas in those objects be via Ref //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ for(Class> c : componentClasses)
+ {
+ Object o = null;
+ try
+ {
+ o = c.getConstructor().newInstance();
+ }
+ catch(Exception nsme)
+ {
+ ///////////////////////////////////////
+ // fine, assume we can't do toSchema //
+ ///////////////////////////////////////
+ }
+
+ Schema schema = null;
+ if(o instanceof ToSchema toSchema)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // just in case a custom implementation of toSchema is provided (e.g., to go around a wrapped object or some-such) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ schema = toSchema.toSchema();
+ }
+ else
+ {
+ schema = new SchemaBuilder().classToSchema(c);
+ }
+
+ convertSchemaToRefs(schema, componentClasses);
+
+ componentSchemas.put(c.getSimpleName(), schema);
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error building component schemas from components package", e));
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void convertMethodSchemasToRefs(Method method, Set> componentClasses)
+ {
+ for(Response response : method.getResponses().values())
+ {
+ for(Content content : response.getContent().values())
+ {
+ Schema schema = content.getSchema();
+ convertSchemaToRefs(schema, componentClasses);
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void convertSchemaToRefs(Schema schema, Set> componentClasses)
+ {
+ if(schema.getItems() instanceof SchemaBuilder.SchemaFromBuilder itemSchemaFromBuilder && componentClasses.contains(itemSchemaFromBuilder.getOriginalClass()))
+ {
+ schema.getItems().withRefToSchema(itemSchemaFromBuilder.getOriginalClass().getSimpleName());
+ schema.getItems().setProperties(null);
+ schema.getItems().setType((Type) null);
+ }
+ else if(schema.getItems() != null)
+ {
+ convertSchemaToRefs(schema.getItems(), componentClasses);
+ }
+
+ if(schema.getProperties() != null)
+ {
+ for(Schema propertySchema : schema.getProperties().values())
+ {
+ if(propertySchema instanceof SchemaBuilder.SchemaFromBuilder propertySchemaFromBuilder && componentClasses.contains(propertySchemaFromBuilder.getOriginalClass()))
+ {
+ propertySchema.withRefToSchema(propertySchemaFromBuilder.getOriginalClass().getSimpleName());
+ propertySchema.setProperties(null);
+ propertySchema.setType((Type) null);
+ }
+ else
+ {
+ convertSchemaToRefs(propertySchema, componentClasses);
+ }
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static String getDescription()
+ {
+ return """
+ ## Intro
+ This is the definition of the standard API implemented by QQQ Middleware.
+
+ Developers of QQQ Frontends (e.g., javascript libraries, or native applications) use this API to access
+ a QQQ Backend server.
+
+ As such, this API itself is not concerned with any of the application-level details of any particular
+ application built using QQQ. Instead, this API is all about the generic endpoints used for any application
+ built on QQQ. For example, many endpoints work with a `${table}` path parameter - whose possible values
+ are defined by the application - but which are not known to this API.
+
+ ## Flow
+ The typical flow of a user (as implemented in a frontend that utilizes this API) looks like:
+ 1. Frontend calls `.../metaData/authentication`, to know what type of authentication provider is used by the backend, and display an appropriate UI to the user for authenticating.
+ 2. User authenticates in frontend, as required for the authentication provider.
+ 3. Frontend calls `.../manageSession`, providing authentication details (e.g., an accessToken or other credentials) to the backend.
+ 4. The response from the `manageSession` call (assuming success), sets the `sessionUUID` Cookie, which should be included in all subsequent requests for authentication.
+ 5. After the user is authenticated, the frontend calls `.../metaData`, to discover the apps, tables, processes, etc, that the application is made up of (and that the authenticated user has permission to access).
+ 6. As the user interacts with apps, tables, process, etc, the frontend utilizes the appropriate endpoints as required.
+ """;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public void warnIfPathMethodAlreadyUsed(Method existing, CompleteOperation completeOperation, AbstractEndpointSpec, ?, ?> spec)
+ {
+ if(existing != null)
+ {
+ LOG.warn("More than one endpoint spec for version " + getVersion() + " defined a " + completeOperation.getHttpMethod() + " at path: " + completeOperation.getPath() + ". The last one encountered (from " + spec.getClass().getSimpleName() + ") will be used.");
+ }
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java
new file mode 100644
index 00000000..35b745c4
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java
@@ -0,0 +1,194 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs;
+
+
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+
+
+/*******************************************************************************
+ ** Basic definition of an operation (e.g., an endpoint exposed in the API).
+ *******************************************************************************/
+public class BasicOperation
+{
+ private String path;
+ private HttpMethod httpMethod;
+ private TagsInterface tag;
+ private String shortSummary;
+ private String longDescription;
+
+
+
+ /*******************************************************************************
+ ** Getter for path
+ *******************************************************************************/
+ public String getPath()
+ {
+ return (this.path);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for path
+ *******************************************************************************/
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for path
+ *******************************************************************************/
+ public BasicOperation withPath(String path)
+ {
+ this.path = path;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for httpMethod
+ *******************************************************************************/
+ public HttpMethod getHttpMethod()
+ {
+ return (this.httpMethod);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for httpMethod
+ *******************************************************************************/
+ public void setHttpMethod(HttpMethod httpMethod)
+ {
+ this.httpMethod = httpMethod;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for httpMethod
+ *******************************************************************************/
+ public BasicOperation withHttpMethod(HttpMethod httpMethod)
+ {
+ this.httpMethod = httpMethod;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tag
+ *******************************************************************************/
+ public TagsInterface getTag()
+ {
+ return (this.tag);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tag
+ *******************************************************************************/
+ public void setTag(TagsInterface tag)
+ {
+ this.tag = tag;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tag
+ *******************************************************************************/
+ public BasicOperation withTag(TagsInterface tag)
+ {
+ this.tag = tag;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for shortSummary
+ *******************************************************************************/
+ public String getShortSummary()
+ {
+ return (this.shortSummary);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for shortSummary
+ *******************************************************************************/
+ public void setShortSummary(String shortSummary)
+ {
+ this.shortSummary = shortSummary;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for shortSummary
+ *******************************************************************************/
+ public BasicOperation withShortSummary(String shortSummary)
+ {
+ this.shortSummary = shortSummary;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for longDescription
+ *******************************************************************************/
+ public String getLongDescription()
+ {
+ return (this.longDescription);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for longDescription
+ *******************************************************************************/
+ public void setLongDescription(String longDescription)
+ {
+ this.longDescription = longDescription;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for longDescription
+ *******************************************************************************/
+ public BasicOperation withLongDescription(String longDescription)
+ {
+ this.longDescription = longDescription;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java
new file mode 100644
index 00000000..7447ab02
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java
@@ -0,0 +1,79 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs;
+
+
+import java.util.Map;
+import com.kingsrook.qqq.openapi.model.Example;
+import io.javalin.http.ContentType;
+import io.javalin.http.HttpStatus;
+
+
+/***************************************************************************
+ ** Basic version of a response from a spec/endpoint.
+ ***************************************************************************/
+public record BasicResponse(String contentType, HttpStatus status, String description, String schemaRefName, Map examples)
+{
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public BasicResponse(String description, String schemaRefName)
+ {
+ this(ContentType.APPLICATION_JSON.getMimeType(), HttpStatus.OK, description, schemaRefName, null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public BasicResponse(String description, String schemaRefName, Map examples)
+ {
+ this(ContentType.APPLICATION_JSON.getMimeType(), HttpStatus.OK, description, schemaRefName, examples);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public BasicResponse(HttpStatus status, String description, String schemaRefName)
+ {
+ this(ContentType.APPLICATION_JSON.getMimeType(), status, description, schemaRefName, null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public BasicResponse(HttpStatus status, String description, String schemaRefName, Map examples)
+ {
+ this(ContentType.APPLICATION_JSON.getMimeType(), status, description, schemaRefName, examples);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java
new file mode 100644
index 00000000..02c11853
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java
@@ -0,0 +1,80 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs;
+
+
+import com.kingsrook.qqq.openapi.model.Method;
+
+
+/*******************************************************************************
+ ** Extension of a BasicOperation that adds the full openAPI Method object.
+ *******************************************************************************/
+public class CompleteOperation extends BasicOperation
+{
+ private Method method;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public CompleteOperation(BasicOperation basicOperation)
+ {
+ setPath(basicOperation.getPath());
+ setHttpMethod(basicOperation.getHttpMethod());
+ setTag(basicOperation.getTag());
+ setLongDescription(basicOperation.getLongDescription());
+ setShortSummary(basicOperation.getShortSummary());
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for method
+ *******************************************************************************/
+ public Method getMethod()
+ {
+ return (this.method);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for method
+ *******************************************************************************/
+ public void setMethod(Method method)
+ {
+ this.method = method;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for method
+ *******************************************************************************/
+ public CompleteOperation withMethod(Method method)
+ {
+ this.method = method;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java
new file mode 100644
index 00000000..23d2b4a4
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java
@@ -0,0 +1,34 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface TagsInterface
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ String getText();
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java
new file mode 100644
index 00000000..5dce2f06
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java
@@ -0,0 +1,137 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.AuthenticationMetaDataExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.AuthenticationMetaDataResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.Schema;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuthenticationMetaDataSpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/metaData/authentication")
+ .withHttpMethod(HttpMethod.GET)
+ .withTag(TagsV1.AUTHENTICATION)
+ .withShortSummary("Get authentication metaData")
+ .withLongDescription("""
+ For a frontend to determine which authentication provider or mechanism to use, it should begin its lifecycle
+ by requesting this metaData object, and inspecting the `type` property in the response.
+
+ Note that this endpoint is not secured, as its purpose is to be called as part of the workflow that results
+ in a user being authenticated."""
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public boolean isSecured()
+ {
+ return (false);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public AuthenticationMetaDataInput buildInput(Context context) throws Exception
+ {
+ AuthenticationMetaDataInput input = new AuthenticationMetaDataInput();
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(AuthenticationMetaDataResponseV1.class.getSimpleName(), new AuthenticationMetaDataResponseV1().toSchema());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = new LinkedHashMap<>();
+ examples.put("For FULLY_ANONYMOUS type", new Example()
+ .withValue(new AuthenticationMetaDataResponseV1()
+ .withType(QAuthenticationType.FULLY_ANONYMOUS.name())
+ .withName("anonymous")));
+
+ examples.put("For AUTH_0 type", new Example()
+ .withValue(new AuthenticationMetaDataResponseV1()
+ .withType(QAuthenticationType.AUTH_0.name())
+ .withName("auth0")
+ .withValues(new AuthenticationMetaDataResponseV1.Auth0Values()
+ .withClientId("abcdefg1234567")
+ .withBaseUrl("https://myapp.auth0.com/")
+ .withAudience("myapp.mydomain.com"))));
+
+ return new BasicResponse("Successful Response", AuthenticationMetaDataResponseV1.class.getSimpleName(), examples);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, AuthenticationMetaDataResponseV1 output) throws Exception
+ {
+ context.result(JsonUtils.toJson(output));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java
new file mode 100644
index 00000000..c15ac971
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java
@@ -0,0 +1,213 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
+import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import com.kingsrook.qqq.middleware.javalin.executors.ManageSessionExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.BasicErrorResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ManageSessionResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.ContentType;
+import io.javalin.http.Context;
+import io.javalin.http.HttpStatus;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ManageSessionSpecV1 extends AbstractEndpointSpec
+{
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/manageSession")
+ .withHttpMethod(HttpMethod.POST)
+ .withTag(TagsV1.AUTHENTICATION)
+ .withShortSummary("Create a session")
+ .withLongDescription("""
+ After a frontend authenticates the user as per the requirements of the authentication provider specified by the
+ `type` field in the `metaData/authentication` response, data from that authentication provider should be posted
+ to this endpoint, to create a session within the QQQ application.
+
+ The response object will include a session identifier (`uuid`) to authenticate the user in subsequent API calls.""");
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public boolean isSecured()
+ {
+ return (false);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ManageSessionResponseV1 serveRequest(Context context) throws Exception
+ {
+ ManageSessionResponseV1 result = super.serveRequest(context);
+ if(result != null)
+ {
+ String sessionUuid = result.getUuid();
+ context.cookie(QJavalinImplementation.SESSION_UUID_COOKIE_NAME, sessionUuid, QJavalinImplementation.SESSION_COOKIE_AGE);
+ }
+ return (result);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public RequestBody defineRequestBody()
+ {
+ return new RequestBody()
+ .withRequired(true)
+ .withContent(MapBuilder.of(ContentType.JSON, new Content()
+ .withSchema(new Schema()
+ .withDescription("Data required to create the session. Specific needs may vary based on the AuthenticationModule type in the QQQ Backend.")
+ .withType(Type.OBJECT)
+ .withProperty("accessToken", new Schema()
+ .withType(Type.STRING)
+ .withDescription("An access token from a downstream authentication provider (e.g., Auth0), to use as the basis for authentication and authorization.")
+ )
+ )
+ ));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ManageSessionInput buildInput(Context context) throws Exception
+ {
+ ManageSessionInput manageSessionInput = new ManageSessionInput();
+ manageSessionInput.setAccessToken(getRequestParam(context, "accessToken"));
+ return (manageSessionInput);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = new LinkedHashMap<>();
+
+ examples.put("With no custom values", new Example().withValue(new ManageSessionResponseV1()
+ .withUuid(ProcessSpecUtilsV1.EXAMPLE_PROCESS_UUID)
+ ));
+
+ examples.put("With custom values", new Example().withValue(new ManageSessionResponseV1()
+ .withUuid(ProcessSpecUtilsV1.EXAMPLE_JOB_UUID)
+ .withValues(MapBuilder.of(LinkedHashMap::new)
+ .with("region", "US")
+ .with("userCategoryId", 47)
+ .build()
+ )
+ ));
+
+ return new BasicResponse("Successful response - session has been created",
+ ManageSessionResponseV1.class.getSimpleName(),
+ examples);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(
+ ManageSessionResponseV1.class.getSimpleName(), new ManageSessionResponseV1().toSchema(),
+ BasicErrorResponseV1.class.getSimpleName(), new BasicErrorResponseV1().toSchema()
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineAdditionalBasicResponses()
+ {
+ Map examples = new LinkedHashMap<>();
+ examples.put("Invalid token", new Example().withValue(new BasicErrorResponseV1().withError("Unable to decode access token.")));
+
+ return List.of(
+ new BasicResponse(HttpStatus.UNAUTHORIZED,
+ "Authentication error - session was not created",
+ BasicErrorResponseV1.class.getSimpleName(),
+ examples
+ )
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, ManageSessionResponseV1 output) throws Exception
+ {
+ context.result(JsonUtils.toJson(output));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java
new file mode 100644
index 00000000..cfb4c22e
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java
@@ -0,0 +1,282 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
+import com.kingsrook.qqq.backend.core.context.CapturedContext;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+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.authentication.QAuthenticationMetaData;
+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.QAppMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
+import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.MetaDataExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.MetaDataResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MetaDataSpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/metaData")
+ .withHttpMethod(HttpMethod.GET)
+ .withTag(TagsV1.GENERAL)
+ .withShortSummary("Get instance metaData")
+ .withLongDescription("""
+ Load the overall metadata, as is relevant to a frontend, for the entire application, with permissions applied, as per the
+ authenticated user.
+
+ This includes:
+ - Apps (both as a map of name to AppMetaData (`apps`), but also as a tree (`appTree`), for presenting
+ hierarchical navigation),
+ - Tables (but without all details, e.g., fields),
+ - Processes (also without full details, e.g., screens),
+ - Reports
+ - Widgets
+ - Branding
+ - Help Contents
+ - Environment values
+ """
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+
+ new Parameter()
+ .withName("frontendName")
+ .withDescription("""
+ Name of the frontend requesting the meta-data.
+ Generally a QQQ frontend library, unless a custom application frontend has been built.""")
+ .withIn(In.QUERY)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("qqq-frontend-material-dashboard"),
+
+ new Parameter()
+ .withName("frontendVersion")
+ .withDescription("Version of the frontend requesting the meta-data.")
+ .withIn(In.QUERY)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("0.23.0"),
+
+ new Parameter()
+ .withName("applicationName")
+ .withDescription("""
+ Name of the application requesting the meta-data. e.g., an instance of a specific frontend
+ (i.e., an application might be deployed with 2 different qqq-frontend-material-dashboard frontends,
+ in which case this attribute allows differentiation between them).""")
+ .withIn(In.QUERY)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("my-admin-web-app"),
+
+ new Parameter()
+ .withName("applicationVersion")
+ .withDescription("Version of the application requesting the meta-data.")
+ .withIn(In.QUERY)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("20241021")
+
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public MetaDataInput buildInput(Context context) throws Exception
+ {
+ MetaDataInput input = new MetaDataInput();
+
+ input.setMiddlewareName("qqq-middleware-javalin");
+ input.setMiddlewareVersion("v1");
+
+ input.setFrontendName(getRequestParam(context, "frontendName"));
+ input.setFrontendVersion(getRequestParam(context, "frontendVersion"));
+
+ input.setApplicationName(getRequestParam(context, "applicationName"));
+ input.setApplicationVersion(getRequestParam(context, "applicationVersion"));
+
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(MetaDataResponseV1.class.getSimpleName(), new MetaDataResponseV1().toSchema());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = new HashMap<>();
+
+ QInstance exampleInstance = new QInstance();
+
+ exampleInstance.setAuthentication(new QAuthenticationMetaData().withName("anonymous").withType(QAuthenticationType.FULLY_ANONYMOUS));
+
+ QBackendMetaData exampleBackend = new QBackendMetaData()
+ .withName("example")
+ .withBackendType(MemoryBackendModule.class);
+ exampleInstance.addBackend(exampleBackend);
+
+ //////////////////////////////////////
+ // create stable sorting of entries //
+ //////////////////////////////////////
+ TreeSet capabilities = new TreeSet<>(Comparator.comparing((Capability c) -> c.name()));
+ capabilities.addAll(Capability.allReadCapabilities());
+ capabilities.addAll(Capability.allWriteCapabilities());
+
+ QTableMetaData exampleTable = new QTableMetaData()
+ .withName("person")
+ .withLabel("Person")
+ .withBackendName("example")
+ .withPrimaryKeyField("id")
+ .withIsHidden(false)
+ .withIcon(new QIcon().withName("person_outline"))
+ .withEnabledCapabilities(capabilities)
+ .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER));
+ exampleInstance.addTable(exampleTable);
+
+ QProcessMetaData exampleProcess = new QProcessMetaData()
+ .withName("samplePersonProcess")
+ .withLabel("Sample Person Process")
+ .withTableName("person")
+ .withIsHidden(false)
+ .withIcon(new QIcon().withName("person_add"))
+ .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
+ .withStep(new QFrontendStepMetaData().withName("example"));
+ exampleInstance.addProcess(exampleProcess);
+
+ QAppMetaData childApp = new QAppMetaData()
+ .withName("childApp")
+ .withLabel("Child App")
+ .withIcon(new QIcon().withName("child_friendly"))
+ .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
+ .withChild(exampleProcess);
+ exampleInstance.addApp(childApp);
+
+ QAppMetaData exampleApp = new QAppMetaData()
+ .withName("homeApp")
+ .withLabel("Home App")
+ .withIcon(new QIcon().withName("home"))
+ .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
+ .withChild(childApp)
+ .withChild(exampleTable);
+ exampleInstance.addApp(exampleApp);
+
+ QContext.withTemporaryContext(new CapturedContext(exampleInstance, new QSystemUserSession()), () ->
+ {
+ try
+ {
+ MetaDataAction metaDataAction = new MetaDataAction();
+ MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput());
+ examples.put("Example", new Example()
+ .withValue(new MetaDataResponseV1()
+ .withMetaDataOutput(output)
+ )
+ );
+ }
+ catch(Exception e)
+ {
+ examples.put("Example", new Example().withValue("Error building example: " + e.getMessage())
+ );
+ }
+ });
+
+ return new BasicResponse("""
+ Overall metadata for the application.""",
+ MetaDataResponseV1.class.getSimpleName(),
+ examples
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, MetaDataResponseV1 output) throws Exception
+ {
+ context.result(JsonUtils.toJson(output));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java
new file mode 100644
index 00000000..ec907d4f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java
@@ -0,0 +1,70 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MiddlewareVersionV1 extends AbstractMiddlewareVersion
+{
+ private static List> list = new ArrayList<>();
+
+ static
+ {
+ list.add(new AuthenticationMetaDataSpecV1());
+ list.add(new ManageSessionSpecV1());
+
+ list.add(new MetaDataSpecV1());
+
+ list.add(new ProcessMetaDataSpecV1());
+ list.add(new ProcessInitSpecV1());
+ list.add(new ProcessStepSpecV1());
+ list.add(new ProcessStatusSpecV1());
+ }
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getVersion()
+ {
+ return "v1";
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public List> getEndpointSpecs()
+ {
+ return (list);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java
new file mode 100644
index 00000000..62377711
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java
@@ -0,0 +1,268 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+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.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.ProcessInitOrStepExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.ContentType;
+import io.javalin.http.Context;
+import org.apache.commons.lang.NotImplementedException;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessInitSpecV1 extends AbstractEndpointSpec
+{
+ private static final QLogger LOG = QLogger.getLogger(ProcessInitSpecV1.class);
+
+ public static int DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/processes/{processName}/init")
+ .withHttpMethod(HttpMethod.POST)
+ .withTag(TagsV1.PROCESSES)
+ .withShortSummary("Initialize a process")
+ .withLongDescription("""
+ For a user to start running a process, this endpoint should be called, to start the process
+ and run its first step(s) (any backend steps before the first frontend step).
+
+ Additional process-specific values should posted in a form param named `values`, as JSON object
+ with keys defined by the process in question.
+
+ For a process which needs to operate on a set of records that a user selected, see
+ `recordsParam`, and `recordIds` or `filterJSON`.
+
+ The response will include a `processUUID`, to be included in all subsequent requests relevant
+ to the process.
+
+ Note that this request, if it takes longer than a given threshold* to complete, will return a
+ a `jobUUID`, which should be sent to the `/processes/{processName}/{processUUID}/status/{jobUUID}`
+ endpoint, to poll for a status update.
+
+ *This threshold has a default value of 3,000 ms., but can be set per-request via the form
+ parameter `stepTimeoutMillis`.
+ """);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName("processName")
+ .withDescription("Name of the process to initialize")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("samplePersonProcess")
+ .withIn(In.PATH)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public RequestBody defineRequestBody()
+ {
+ return new RequestBody()
+ .withContent(
+ ContentType.MULTIPART_FORM_DATA.getMimeType(), new Content()
+ .withSchema(new Schema()
+ .withType(Type.OBJECT)
+ .withProperty("values", new Schema()
+ .withType(Type.OBJECT)
+ .withDescription("Process-specific field names and values."))
+
+ .withProperty("recordsParam", new Schema()
+ .withDescription("Specifies which other query-param will contain the indicator of initial records to pass in to the process.")
+ .withType(Type.STRING)
+ .withExample("recordIds", new Example().withValue("recordIds"))
+ .withExample("filterJSON", new Example().withValue("recordIds")))
+
+ .withProperty("recordIds", new Schema()
+ .withDescription("Comma-separated list of ids from the table this process is based on, to use as input records for the process. Needs `recordsParam=recordIds` value to be given as well.")
+ .withType(Type.STRING)
+ .withExample("one id", new Example().withValue("1701"))
+ .withExample("multiple ids", new Example().withValue("42,47")))
+
+ .withProperty("filterJSON", new Schema()
+ .withDescription("JSON encoded QQueryFilter object, to execute against the table this process is based on, to find input records for the process. Needs `recordsParam=filterJSON` value to be given as well.")
+ .withType(Type.STRING)
+ .withExample("empty filter (all records)", new Example().withValue("{}"))
+ .withExample("filter by a condition", new Example().withValue(
+ JsonUtils.toJson(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, 10))))
+ ))
+
+ .withProperty("stepTimeoutMillis", new Schema()
+ .withDescription("Optionally change the time that the server will wait for the job before letting it go asynchronous. Default value is 3000.")
+ .withType(Type.INTEGER)
+ .withExample("shorter timeout", new Example().withValue("500"))
+ .withExample("longer timeout", new Example().withValue("60000")))
+
+ .withProperty("file", new Schema()
+ .withType(Type.STRING)
+ .withFormat("binary")
+ .withDescription("A file upload, for processes which expect to be initialized with an uploaded file.")
+ )
+ )
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ProcessInitOrStepInput buildInput(Context context) throws Exception
+ {
+ ProcessInitOrStepInput processInitOrStepInput = new ProcessInitOrStepInput();
+ processInitOrStepInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ processInitOrStepInput.setProcessName(getRequestParam(context, "processName"));
+ processInitOrStepInput.setStepTimeoutMillis(Objects.requireNonNullElse(getRequestParamInteger(context, "stepTimeoutMillis"), DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS));
+ processInitOrStepInput.setValues(getRequestParamMap(context, "values"));
+
+ String recordsParam = getRequestParam(context, "recordsParam");
+ String recordIds = getRequestParam(context, "recordIds");
+ String filterJSON = getRequestParam(context, "filterJSON");
+ QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(recordsParam, recordIds, filterJSON, processInitOrStepInput);
+ processInitOrStepInput.setRecordsFilter(initialRecordsFilter);
+
+ // todo - uploaded files
+ // todo - archive uploaded files?
+
+ return (processInitOrStepInput);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static QQueryFilter buildProcessInitRecordsFilter(String recordsParam, String recordIds, String filterJSON, ProcessInitOrStepInput processInitOrStepInput) throws IOException
+ {
+ QProcessMetaData process = QContext.getQInstance().getProcess(processInitOrStepInput.getProcessName());
+ QTableMetaData table = QContext.getQInstance().getTable(process.getTableName());
+
+ if(table == null)
+ {
+ LOG.info("No table found in process - so not building an init records filter.");
+ return (null);
+ }
+ String primaryKeyField = table.getPrimaryKeyField();
+
+ if(StringUtils.hasContent(recordsParam))
+ {
+ return switch(recordsParam)
+ {
+ case "recordIds" ->
+ {
+ Serializable[] idStrings = recordIds.split(",");
+ yield (new QQueryFilter().withCriteria(new QFilterCriteria()
+ .withFieldName(primaryKeyField)
+ .withOperator(QCriteriaOperator.IN)
+ .withValues(Arrays.stream(idStrings).toList())));
+ }
+ case "filterJSON" -> (JsonUtils.toObject(filterJSON, QQueryFilter.class));
+ case "filterId" -> throw (new NotImplementedException("Saved filters are not yet implemented."));
+ default -> throw (new IllegalArgumentException("Unrecognized value [" + recordsParam + "] for query parameter: recordsParam"));
+ };
+ }
+
+ return (null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ return new BasicResponse("""
+ State of the initialization of the job, with different fields set, based on the
+ status of the task.""",
+
+ ProcessSpecUtilsV1.getResponseSchemaRefName(),
+ ProcessSpecUtilsV1.buildResponseExample()
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output) throws Exception
+ {
+ ProcessSpecUtilsV1.handleOutput(context, output);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java
new file mode 100644
index 00000000..d311dd8c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java
@@ -0,0 +1,140 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.ProcessMetaDataExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessMetaDataResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessMetaDataSpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/metaData/process/{processName}")
+ .withHttpMethod(HttpMethod.GET)
+ .withTag(TagsV1.PROCESSES)
+ .withShortSummary("Get process metaData")
+ .withLongDescription("""
+ Load the full metadata for a single process, including all screens (aka, frontend steps), which a frontend
+ needs to display to users."""
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName("processName")
+ .withDescription("Name of the process to load.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("samplePersonProcess")
+ .withIn(In.PATH)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ProcessMetaDataInput buildInput(Context context) throws Exception
+ {
+ ProcessMetaDataInput input = new ProcessMetaDataInput();
+ input.setProcessName(getRequestParam(context, "processName"));
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(ProcessMetaDataResponseV1.class.getSimpleName(), new ProcessMetaDataResponseV1().toSchema());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = new LinkedHashMap<>();
+ examples.put("TODO", new Example()
+ .withValue(new ProcessMetaDataResponseV1())); // todo do
+
+ return new BasicResponse("""
+ The full process metadata""",
+ ProcessMetaDataResponseV1.class.getSimpleName(),
+ examples
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, ProcessMetaDataResponseV1 output) throws Exception
+ {
+ context.result(JsonUtils.toJson(output.getProcessMetaData()));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java
new file mode 100644
index 00000000..d7625157
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java
@@ -0,0 +1,164 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.middleware.javalin.executors.ProcessStatusExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessStatusInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessStatusSpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/processes/{processName}/{processUUID}/status/{jobUUID}")
+ .withHttpMethod(HttpMethod.GET)
+ .withTag(TagsV1.PROCESSES)
+ .withShortSummary("Get job status")
+ .withLongDescription("""
+ Get the status of a running job for a process.
+
+ Response is the same format as for an init or step call that completed synchronously.
+ """
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName("processName")
+ .withDescription("Name of the process that is being ran")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("samplePersonProcess")
+ .withIn(In.PATH),
+
+ new Parameter()
+ .withName("processUUID")
+ .withDescription("Unique identifier for this run of the process - as was returned by the `init` call.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING).withFormat("uuid"))
+ .withExample(ProcessSpecUtilsV1.EXAMPLE_PROCESS_UUID)
+ .withIn(In.PATH),
+
+ new Parameter()
+ .withName("jobUUID")
+ .withDescription("Unique identifier for the asynchronous job being executed, as returned by an `init` or `step` call that went asynch.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING).withFormat("uuid"))
+ .withExample(ProcessSpecUtilsV1.EXAMPLE_JOB_UUID)
+ .withIn(In.PATH)
+ );
+ }
+
+
+
+ /***************************************************************************
+ ** These aren't in the components sub-package, so they don't get auto-found.
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(
+ ProcessSpecUtilsV1.getResponseSchemaRefName(), new ProcessInitOrStepOrStatusResponseV1().toSchema(),
+ "ProcessStepComplete", new ProcessInitOrStepOrStatusResponseV1.ProcessStepComplete().toSchema(),
+ "ProcessStepJobStarted", new ProcessInitOrStepOrStatusResponseV1.ProcessStepJobStarted().toSchema(),
+ "ProcessStepRunning", new ProcessInitOrStepOrStatusResponseV1.ProcessStepRunning().toSchema(),
+ "ProcessStepError", new ProcessInitOrStepOrStatusResponseV1.ProcessStepError().toSchema()
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ProcessStatusInput buildInput(Context context) throws Exception
+ {
+ ProcessStatusInput input = new ProcessStatusInput();
+ input.setProcessName(getRequestParam(context, "processName"));
+ input.setProcessUUID(getRequestParam(context, "processUUID"));
+ input.setJobUUID(getRequestParam(context, "jobUUID"));
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ return new BasicResponse("""
+ State of the backend's running of the specified job, with different fields set,
+ based on the status of the job.""",
+ // new ProcessInitOrStepOrStatusResponseV1().toSchema(),
+
+ ProcessSpecUtilsV1.getResponseSchemaRefName(),
+ ProcessSpecUtilsV1.buildResponseExample()
+ );
+ }
+
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output) throws Exception
+ {
+ ProcessSpecUtilsV1.handleOutput(context, output);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java
new file mode 100644
index 00000000..156bf7ce
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java
@@ -0,0 +1,201 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.List;
+import java.util.Objects;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.middleware.javalin.executors.ProcessInitOrStepExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.ContentType;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessStepSpecV1 extends AbstractEndpointSpec
+{
+ public static int DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/processes/{processName}/{processUUID}/step/{stepName}")
+ .withHttpMethod(HttpMethod.POST)
+ .withTag(TagsV1.PROCESSES)
+ .withShortSummary("Run a step in a process")
+ .withLongDescription("""
+ To run the next step in a process, this endpoint should be called, with the `processName`
+ and existing `processUUID`, as well as the step that was just completed in the frontend,
+ given as `stepName`.
+
+ Additional process-specific values should posted in a form param named `values`, as JSON object
+ with keys defined by the process in question.
+
+ Note that this request, if it takes longer than a given threshold* to complete, will return a
+ a `jobUUID`, which should be sent to the `/processes/{processName}/{processUUID}/status/{jobUUID}`
+ endpoint, to poll for a status update.
+
+ *This threshold has a default value of 3,000 ms., but can be set per-request via the form
+ parameter `stepTimeoutMillis`.
+ """);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+
+ new Parameter()
+ .withName("processName")
+ .withDescription("Name of the process to perform the step in.")
+ .withRequired(true)
+ .withExample("samplePersonProcess")
+ .withSchema(new Schema().withType(Type.STRING))
+ .withIn(In.PATH),
+
+ new Parameter()
+ .withName("processUUID")
+ .withDescription("Unique identifier for this run of the process - as was returned by the `init` call.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample(ProcessSpecUtilsV1.EXAMPLE_PROCESS_UUID)
+ .withIn(In.PATH),
+
+ new Parameter()
+ .withName("stepName")
+ .withDescription("Name of the frontend step that the user has just completed.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("inputForm")
+ .withIn(In.PATH)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public RequestBody defineRequestBody()
+ {
+ return new RequestBody()
+ .withContent(ContentType.MULTIPART_FORM_DATA.getMimeType(), new Content()
+ .withSchema(new Schema()
+ .withType(Type.OBJECT)
+ .withProperty("values", new Schema()
+ .withType(Type.OBJECT)
+ .withDescription("Process-specific field names and values."))
+
+ .withProperty("stepTimeoutMillis", new Schema()
+ .withDescription("Optionally change the time that the server will wait for the job before letting it go asynchronous. Default value is 3000.")
+ .withType(Type.INTEGER)
+ .withExample("shorter timeout", new Example().withValue("500"))
+ .withExample("longer timeout", new Example().withValue("60000")))
+
+ .withProperty("file", new Schema()
+ .withType(Type.STRING)
+ .withFormat("binary")
+ .withDescription("A file upload, for process steps which expect an uploaded file."))
+ )
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ProcessInitOrStepInput buildInput(Context context) throws Exception
+ {
+ ProcessInitOrStepInput processInitOrStepInput = new ProcessInitOrStepInput();
+ processInitOrStepInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+
+ processInitOrStepInput.setProcessName(getRequestParam(context, "processName"));
+ processInitOrStepInput.setProcessUUID(getRequestParam(context, "processUUID"));
+ processInitOrStepInput.setStartAfterStep(getRequestParam(context, "stepName"));
+ processInitOrStepInput.setStepTimeoutMillis(Objects.requireNonNullElse(getRequestParamInteger(context, "stepTimeoutMillis"), DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS));
+ processInitOrStepInput.setValues(getRequestParamMap(context, "values"));
+
+ // todo - uploaded files
+ // todo - archive uploaded files?
+
+ return (processInitOrStepInput);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ return new BasicResponse("""
+ State of the backend's running of the next step(s) of the job, with different fields set,
+ based on the status of the job.""",
+
+ ProcessSpecUtilsV1.getResponseSchemaRefName(),
+ ProcessSpecUtilsV1.buildResponseExample()
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output) throws Exception
+ {
+ ProcessSpecUtilsV1.handleOutput(context, output);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java
new file mode 100644
index 00000000..22db2725
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java
@@ -0,0 +1,340 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuthenticationMetaDataResponseV1 implements AuthenticationMetaDataOutputInterface, ToSchema
+{
+ @OpenAPIDescription("""
+ Specifier for the type of authentication module being used.
+
+ Frontends should use this value to determine how to prompt the user for authentication credentials.
+ In addition, depending on this value, additional properties will be included in this object, as
+ may be needed to complete the authorization workflow with the provider (e.g., a baseUrl, clientId,
+ and audience for an OAuth type workflow).""")
+ private String type;
+
+ @OpenAPIDescription("""
+ Unique name for the authentication metaData object within the QInstance.
+ """)
+ private String name;
+
+ @OpenAPIDescription("""
+ Additional values, as determined by the type of authentication provider.
+ """)
+ @OpenAPIOneOf()
+ private Values values;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public sealed interface Values permits EmptyValues, Auth0Values
+ {
+
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("No additional values are used for some authentication providers.")
+ public static final class EmptyValues implements Values
+ {
+
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Additional values used by the Auth0 type authentication provider.")
+ public static final class Auth0Values implements Values
+ {
+ @OpenAPIDescription("ClientId for auth0")
+ private String clientId;
+
+ @OpenAPIDescription("BaseUrl for auth0")
+ private String baseUrl;
+
+ @OpenAPIDescription("Audience for auth0")
+ private String audience;
+
+
+
+ /*******************************************************************************
+ ** Getter for clientId
+ **
+ *******************************************************************************/
+ public String getClientId()
+ {
+ return clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientId
+ **
+ *******************************************************************************/
+ public void setClientId(String clientId)
+ {
+ this.clientId = clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for clientId
+ **
+ *******************************************************************************/
+ public Auth0Values withClientId(String clientId)
+ {
+ this.clientId = clientId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for baseUrl
+ **
+ *******************************************************************************/
+ public String getBaseUrl()
+ {
+ return baseUrl;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for baseUrl
+ **
+ *******************************************************************************/
+ public void setBaseUrl(String baseUrl)
+ {
+ this.baseUrl = baseUrl;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for baseUrl
+ **
+ *******************************************************************************/
+ public Auth0Values withBaseUrl(String baseUrl)
+ {
+ this.baseUrl = baseUrl;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for audience
+ **
+ *******************************************************************************/
+ public String getAudience()
+ {
+ return audience;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for audience
+ **
+ *******************************************************************************/
+ public void setAudience(String audience)
+ {
+ this.audience = audience;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for audience
+ **
+ *******************************************************************************/
+ public Auth0Values withAudience(String audience)
+ {
+ this.audience = audience;
+ return (this);
+ }
+
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setAuthenticationMetaData(QAuthenticationMetaData qAuthenticationMetaData)
+ {
+ setType(qAuthenticationMetaData.getType().name());
+ setName(qAuthenticationMetaData.getName());
+
+ if(qAuthenticationMetaData instanceof Auth0AuthenticationMetaData auth0MetaData)
+ {
+ // values = new LinkedHashMap<>();
+ // values.put("clientId", auth0MetaData.getClientId());
+ // values.put("baseUrl", auth0MetaData.getBaseUrl());
+ // values.put("audience", auth0MetaData.getAudience());
+ Auth0Values auth0Values = new Auth0Values();
+ values = auth0Values;
+ auth0Values.setClientId(auth0MetaData.getClientId());
+ auth0Values.setBaseUrl(auth0MetaData.getBaseUrl());
+ auth0Values.setAudience(auth0MetaData.getAudience());
+ }
+
+ /*
+ JSONObject jsonObject = new JSONObject(JsonUtils.toJson(qAuthenticationMetaData));
+ for(String key : jsonObject.keySet())
+ {
+ if("name".equals(key) || "type".equals(key))
+ {
+ continue;
+ }
+
+ if(values == null)
+ {
+ values = new LinkedHashMap<>();
+ }
+
+ Object value = jsonObject.get(key);
+ if(value instanceof Serializable s)
+ {
+ values.put(key, s);
+ }
+ }
+ */
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for type
+ *******************************************************************************/
+ public String getType()
+ {
+ return (this.type);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for type
+ *******************************************************************************/
+ public void setType(String type)
+ {
+ this.type = type;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for type
+ *******************************************************************************/
+ public AuthenticationMetaDataResponseV1 withType(String type)
+ {
+ this.type = type;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for name
+ *******************************************************************************/
+ public String getName()
+ {
+ return (this.name);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for name
+ *******************************************************************************/
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ *******************************************************************************/
+ public AuthenticationMetaDataResponseV1 withName(String name)
+ {
+ this.name = name;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ *******************************************************************************/
+ public Values getValues()
+ {
+ return (this.values);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ public void setValues(Values values)
+ {
+ this.values = values;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for values
+ *******************************************************************************/
+ public AuthenticationMetaDataResponseV1 withValues(Values values)
+ {
+ this.values = values;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java
new file mode 100644
index 00000000..0062e105
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java
@@ -0,0 +1,69 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class BasicErrorResponseV1 implements ToSchema
+{
+ @OpenAPIDescription("Description of the error")
+ private String error;
+
+
+
+ /*******************************************************************************
+ ** Getter for error
+ *******************************************************************************/
+ public String getError()
+ {
+ return (this.error);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for error
+ *******************************************************************************/
+ public void setError(String error)
+ {
+ this.error = error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for error
+ *******************************************************************************/
+ public BasicErrorResponseV1 withError(String error)
+ {
+ this.error = error;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java
new file mode 100644
index 00000000..90d6810c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java
@@ -0,0 +1,107 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIHasAdditionalProperties;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ManageSessionResponseV1 implements ManageSessionOutputInterface, ToSchema
+{
+ @OpenAPIDescription("Unique identifier of the session. Required to be returned on subsequent requests in the sessionUUID Cookie, to prove authentication.")
+ private String uuid;
+
+ @OpenAPIDescription("Optional object with application-defined values.")
+ @OpenAPIHasAdditionalProperties()
+ private Map values;
+
+
+
+ /*******************************************************************************
+ ** Getter for uuid
+ *******************************************************************************/
+ public String getUuid()
+ {
+ return (this.uuid);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for uuid
+ *******************************************************************************/
+ public void setUuid(String uuid)
+ {
+ this.uuid = uuid;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for uuid
+ *******************************************************************************/
+ public ManageSessionResponseV1 withUuid(String uuid)
+ {
+ this.uuid = uuid;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ *******************************************************************************/
+ public Map getValues()
+ {
+ return (this.values);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ public void setValues(Map values)
+ {
+ this.values = values;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for values
+ *******************************************************************************/
+ public ManageSessionResponseV1 withValues(Map values)
+ {
+ this.values = values;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java
new file mode 100644
index 00000000..7b7f51c1
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java
@@ -0,0 +1,177 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppMetaData;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppTreeNode;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaDataLight;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableMetaDataLight;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MetaDataResponseV1 implements MetaDataOutputInterface, ToSchema
+{
+ @OpenAPIDescription("Map of all apps within the QQQ Instance (that the user has permission to see that they exist).")
+ @OpenAPIMapValueType(value = AppMetaData.class, useRef = true)
+ private Map apps;
+
+ @OpenAPIDescription("Tree of apps within the QQQ Instance, sorted and organized hierarchically, for presentation to a user.")
+ @OpenAPIListItems(value = AppTreeNode.class, useRef = true)
+ private List appTree;
+
+ @OpenAPIDescription("Map of all tables within the QQQ Instance (that the user has permission to see that they exist).")
+ @OpenAPIMapValueType(value = TableMetaDataLight.class, useRef = true)
+ private Map tables;
+
+ @OpenAPIDescription("Map of all processes within the QQQ Instance (that the user has permission to see that they exist).")
+ @OpenAPIMapValueType(value = ProcessMetaDataLight.class, useRef = true)
+ private Map processes;
+
+ @OpenAPIDescription("Map of all widgets within the QQQ Instance (that the user has permission to see that they exist).")
+ @OpenAPIMapValueType(value = ProcessMetaDataLight.class, useRef = true)
+ private Map widgets;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setMetaDataOutput(MetaDataOutput metaDataOutput)
+ {
+ apps = new HashMap<>();
+ for(QFrontendAppMetaData app : CollectionUtils.nonNullMap(metaDataOutput.getApps()).values())
+ {
+ apps.put(app.getName(), new AppMetaData(app));
+ }
+
+ appTree = new ArrayList<>();
+ for(com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode app : CollectionUtils.nonNullList(metaDataOutput.getAppTree()))
+ {
+ appTree.add(new AppTreeNode(app));
+ }
+
+ tables = new HashMap<>();
+ for(QFrontendTableMetaData table : CollectionUtils.nonNullMap(metaDataOutput.getTables()).values())
+ {
+ tables.put(table.getName(), new TableMetaDataLight(table));
+ }
+
+ processes = new HashMap<>();
+ for(QFrontendProcessMetaData process : CollectionUtils.nonNullMap(metaDataOutput.getProcesses()).values())
+ {
+ processes.put(process.getName(), new ProcessMetaDataLight(process));
+ }
+
+ widgets = new HashMap<>();
+ for(QFrontendWidgetMetaData widget : CollectionUtils.nonNullMap(metaDataOutput.getWidgets()).values())
+ {
+ widgets.put(widget.getName(), new WidgetMetaData(widget));
+ }
+
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for MetaDataOutput
+ **
+ *******************************************************************************/
+ public MetaDataResponseV1 withMetaDataOutput(MetaDataOutput metaDataOutput)
+ {
+ setMetaDataOutput(metaDataOutput);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for apps
+ **
+ *******************************************************************************/
+ public Map getApps()
+ {
+ return apps;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for appTree
+ **
+ *******************************************************************************/
+ public List getAppTree()
+ {
+ return appTree;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tables
+ **
+ *******************************************************************************/
+ public Map getTables()
+ {
+ return tables;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processes
+ **
+ *******************************************************************************/
+ public Map getProcesses()
+ {
+ return processes;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for widgets
+ **
+ *******************************************************************************/
+ public Map getWidgets()
+ {
+ return widgets;
+ }
+}
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java
new file mode 100644
index 00000000..5723c3c2
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java
@@ -0,0 +1,444 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.FieldMetaData;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.FrontendStep;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaDataAdjustment;
+import com.kingsrook.qqq.openapi.model.Schema;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessInitOrStepOrStatusResponseV1 implements ProcessInitOrStepOrStatusOutputInterface, ToSchema
+{
+ private TypedResponse typedResponse;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIOneOf()
+ public static sealed class TypedResponse implements ToSchema permits ProcessStepComplete, ProcessStepJobStarted, ProcessStepRunning, ProcessStepError
+ {
+ @OpenAPIDescription("What kind of response has been received. Determines what additional fields will be set.")
+ private String type;
+
+ @OpenAPIDescription("Unique identifier for a running instance the process.")
+ private String processUUID;
+
+
+
+ /*******************************************************************************
+ ** Getter for type
+ **
+ *******************************************************************************/
+ public String getType()
+ {
+ return type;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processUUID
+ **
+ *******************************************************************************/
+ public String getProcessUUID()
+ {
+ return processUUID;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
+ @OpenAPIDescription("Data returned after the job is complete (whether it was synchronous, or asynchronous)")
+ public static final class ProcessStepComplete extends TypedResponse
+ {
+ @OpenAPIDescription("Name of the next process step that needs to run (a frontend step). If there are no more steps in the process, this field will not be included. ")
+ private String nextStep;
+
+ @OpenAPIDescription("Current values for fields used by the process.Keys are Strings, values can be any type, as determined by the application & process.")
+ private Map values;
+
+ @OpenAPIDescription("Changes to be made to the process's metaData.")
+ private ProcessMetaDataAdjustment processMetaDataAdjustment;
+
+
+
+ /*******************************************************************************
+ ** Getter for nextStep
+ **
+ *******************************************************************************/
+ public String getNextStep()
+ {
+ return nextStep;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ **
+ *******************************************************************************/
+ public Map getValues()
+ {
+ return values;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processMetaDataAdjustment
+ **
+ *******************************************************************************/
+ public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
+ {
+ return processMetaDataAdjustment;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
+ @OpenAPIDescription("In case the backend needs more time, this is a UUID of the background job that has been started.")
+ public static final class ProcessStepJobStarted extends TypedResponse
+ {
+ @OpenAPIDescription("Unique identifier for a running step of the process. Must be passed into `status` check calls.")
+ private String jobUUID;
+
+
+
+ /*******************************************************************************
+ ** Getter for jobUUID
+ **
+ *******************************************************************************/
+ public String getJobUUID()
+ {
+ return jobUUID;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
+ @OpenAPIDescription("Response to a status check for a backgrounded job.")
+ public static final class ProcessStepRunning extends TypedResponse
+ {
+ @OpenAPIDescription("Status message regarding the running process step.")
+ private String message;
+
+ @OpenAPIDescription("Optional indicator of progress (e.g., `current` of `total`, as in (`1 of 10`).")
+ private Integer current;
+
+ @OpenAPIDescription("Optional indicator of progress (e.g., `current` of `total`, as in (`1 of 10`).")
+ private Integer total;
+
+
+
+ /*******************************************************************************
+ ** Getter for message
+ **
+ *******************************************************************************/
+ public String getMessage()
+ {
+ return message;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for current
+ **
+ *******************************************************************************/
+ public Integer getCurrent()
+ {
+ return current;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for total
+ **
+ *******************************************************************************/
+ public Integer getTotal()
+ {
+ return total;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
+ @OpenAPIDescription("In case an error is thrown in the backend job.")
+ public static final class ProcessStepError extends TypedResponse
+ {
+ @OpenAPIDescription("Exception message, in case the process step threw an error.")
+ private String error;
+
+ @OpenAPIDescription("Optional user-facing exception message, in case the process step threw a user-facing error.")
+ private String userFacingError;
+
+
+
+ /*******************************************************************************
+ ** Getter for error
+ **
+ *******************************************************************************/
+ public String getError()
+ {
+ return error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userFacingError
+ **
+ *******************************************************************************/
+ public String getUserFacingError()
+ {
+ return userFacingError;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setType(Type type)
+ {
+ this.typedResponse = switch(type)
+ {
+ case COMPLETE -> new ProcessStepComplete();
+ case JOB_STARTED -> new ProcessStepJobStarted();
+ case RUNNING -> new ProcessStepRunning();
+ case ERROR -> new ProcessStepError();
+ };
+
+ this.typedResponse.type = type.toString();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setProcessUUID(String processUUID)
+ {
+ this.typedResponse.processUUID = processUUID;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setNextStep(String nextStep)
+ {
+ if(this.typedResponse instanceof ProcessStepComplete complete)
+ {
+ complete.nextStep = nextStep;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setValues(Map values)
+ {
+ if(this.typedResponse instanceof ProcessStepComplete complete)
+ {
+ complete.values = values;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setProcessMetaDataAdjustment(com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment processMetaDataAdjustment)
+ {
+ if(this.typedResponse instanceof ProcessStepComplete complete)
+ {
+ if(processMetaDataAdjustment == null)
+ {
+ complete.processMetaDataAdjustment = null;
+ }
+ else
+ {
+ complete.processMetaDataAdjustment = new ProcessMetaDataAdjustment();
+
+ Map updatedFields = processMetaDataAdjustment.getUpdatedFields().entrySet()
+ .stream().collect(Collectors.toMap(e -> e.getKey(), f -> new FieldMetaData(f.getValue())));
+ complete.processMetaDataAdjustment.setUpdatedFields(updatedFields);
+
+ List updatedFrontendSteps = processMetaDataAdjustment.getUpdatedFrontendStepList()
+ .stream().map(f -> new FrontendStep(f)).toList();
+ complete.processMetaDataAdjustment.setUpdatedFrontendStepList(updatedFrontendSteps);
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setJobUUID(String jobUUID)
+ {
+ if(this.typedResponse instanceof ProcessStepJobStarted jobStarted)
+ {
+ jobStarted.jobUUID = jobUUID;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setMessage(String message)
+ {
+ if(this.typedResponse instanceof ProcessStepRunning running)
+ {
+ running.message = message;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setCurrent(Integer current)
+ {
+ if(this.typedResponse instanceof ProcessStepRunning running)
+ {
+ running.current = current;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setTotal(Integer total)
+ {
+ if(this.typedResponse instanceof ProcessStepRunning running)
+ {
+ running.total = total;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setError(String errorString)
+ {
+ if(this.typedResponse instanceof ProcessStepError error)
+ {
+ error.error = errorString;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setUserFacingError(String userFacingError)
+ {
+ if(this.typedResponse instanceof ProcessStepError error)
+ {
+ error.userFacingError = userFacingError;
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Schema toSchema()
+ {
+ return new SchemaBuilder().classToSchema(TypedResponse.class);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for typedResponse
+ **
+ *******************************************************************************/
+ public TypedResponse getTypedResponse()
+ {
+ return typedResponse;
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java
new file mode 100644
index 00000000..2a4d45b5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java
@@ -0,0 +1,72 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
+import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaData;
+import com.kingsrook.qqq.openapi.model.Schema;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessMetaDataResponseV1 implements ProcessMetaDataOutputInterface, ToSchema
+{
+ private ProcessMetaData processMetaData;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setProcessMetaData(QFrontendProcessMetaData frontendProcessMetaData)
+ {
+ this.processMetaData = new ProcessMetaData(frontendProcessMetaData);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Schema toSchema()
+ {
+ return new SchemaBuilder().classToSchema(ProcessMetaData.class);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processMetaData
+ **
+ *******************************************************************************/
+ public ProcessMetaData getProcessMetaData()
+ {
+ return processMetaData;
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/WidgetMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/WidgetMetaData.java
new file mode 100644
index 00000000..d23cf2a8
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/WidgetMetaData.java
@@ -0,0 +1,92 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class WidgetMetaData
+{
+ @OpenAPIExclude()
+ private QFrontendWidgetMetaData wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public WidgetMetaData(QFrontendWidgetMetaData wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public WidgetMetaData()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Unique name for this widget within the QQQ Instance")
+ public String getName()
+ {
+ return (this.wrapped.getName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("User-facing name for this widget")
+ public String getLabel()
+ {
+ return (this.wrapped.getLabel());
+ }
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("The type of this widget.")
+ // todo enum of the NAMES of the widget types?? or, can we just f'ing change to return the enum.name's?
+ public String getType()
+ {
+ return this.wrapped.getType();
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java
new file mode 100644
index 00000000..a31855d7
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java
@@ -0,0 +1,161 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class AppMetaData implements ToSchema
+{
+ @OpenAPIExclude()
+ private QFrontendAppMetaData wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public AppMetaData(QFrontendAppMetaData wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public AppMetaData()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Unique name for this app within the QQQ Instance")
+ public String getName()
+ {
+ return (this.wrapped.getName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("User-facing name for this app")
+ public String getLabel()
+ {
+ return (this.wrapped.getLabel());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Icon to display for the app.")
+ @OpenAPIMapKnownEntries(value = Icon.class, useRef = true)
+ public Icon getIcon()
+ {
+ return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("List of widgets names that are part of this app. These strings should be keys to the widgets map in the QQQ Instance.")
+ @OpenAPIListItems(value = String.class)
+ public List getWidgets()
+ {
+ return (this.wrapped.getWidgets());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("List of other apps, tables, process, and reports, which are contained within this app.")
+ @OpenAPIListItems(value = AppTreeNode.class, useRef = true)
+ public List getChildren()
+ {
+ return (CollectionUtils.nonNullList(this.wrapped.getChildren()).stream().map(a -> new AppTreeNode(a)).toList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Map of other apps, tables, process, and reports, which are contained within this app. Same contents as the children list, just structured as a map.")
+ @OpenAPIMapValueType(value = AppTreeNode.class, useRef = true)
+ public Map getChildMap()
+ {
+ return (CollectionUtils.nonNullMap(this.wrapped.getChildMap()).entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> new AppTreeNode(e.getValue()))));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("List of sections - sub-divisions of the app, to further organize its children.")
+ @OpenAPIListItems(value = AppSection.class, useRef = true) // todo local type
+ public List getSections()
+ {
+ return (CollectionUtils.nonNullList(this.wrapped.getSections()).stream().map(s -> new AppSection(s)).toList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Additional meta-data describing the app, which may not be known to the QQQ backend core module.")
+ public Map getSupplementalAppMetaData()
+ {
+ return (new LinkedHashMap<>(CollectionUtils.nonNullMap(this.wrapped.getSupplementalAppMetaData())));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java
new file mode 100644
index 00000000..236cf0df
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java
@@ -0,0 +1,133 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see