diff --git a/pom.xml b/pom.xml
index f4d71592..8f5d6e69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,7 +53,7 @@
com.kingsrook.qqq
qqq-backend-core
- 0.1.0-20220708.195335-5
+ 0.1.0-20220708.203555-6
com.kingsrook.qqq
diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
index 97998401..ec05e05c 100644
--- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -29,24 +29,15 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.CountAction;
import com.kingsrook.qqq.backend.core.actions.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.InsertAction;
import com.kingsrook.qqq.backend.core.actions.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.ProcessMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.QueryAction;
-import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.UpdateAction;
-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.async.JobGoingAsyncException;
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
-import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
@@ -64,9 +55,6 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMeta
import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataResult;
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest;
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult;
-import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
-import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest;
-import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult;
import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
@@ -75,15 +63,11 @@ import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult;
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateRequest;
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateResult;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface;
-import com.kingsrook.qqq.backend.core.state.StateType;
-import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
-import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -114,12 +98,11 @@ public class QJavalinImplementation
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
- private static QInstance qInstance;
+ protected static QInstance qInstance;
private static int DEFAULT_PORT = 8001;
- private static int ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
-
+ private static Javalin service;
/*******************************************************************************
@@ -165,11 +148,20 @@ public class QJavalinImplementation
{
// todo port from arg
// todo base path from arg?
- Javalin service = Javalin.create().start(port);
+ service = Javalin.create().start(port);
service.routes(getRoutes());
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ void stopJavalinServer()
+ {
+ service.stop();
+ }
+
+
/*******************************************************************************
**
@@ -181,16 +173,6 @@ public class QJavalinImplementation
- /*******************************************************************************
- **
- *******************************************************************************/
- public static void setAsyncStepTimeoutMillis(int asyncStepTimeoutMillis)
- {
- QJavalinImplementation.ASYNC_STEP_TIMEOUT_MILLIS = asyncStepTimeoutMillis;
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
@@ -230,20 +212,7 @@ public class QJavalinImplementation
});
});
});
- path("/processes", () ->
- {
- path("/:processName", () ->
- {
- get("/init", QJavalinImplementation::processInit);
- post("/init", QJavalinImplementation::processInit);
-
- path("/:processUUID", () ->
- {
- post("/step/:step", QJavalinImplementation::processStep);
- get("/status/:jobUUID", QJavalinImplementation::processStatus);
- });
- });
- });
+ path("", QJavalinProcessHandler.getRoutes());
});
}
@@ -252,7 +221,7 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
- private static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException
+ static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
@@ -465,6 +434,7 @@ public class QJavalinImplementation
}
+
/*******************************************************************************
*
* Filter parameter is a serialized QQueryFilter object, that is to say:
@@ -579,7 +549,7 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
- private static void handleException(Context context, Exception e)
+ public static void handleException(Context context, Exception e)
{
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
if(userFacingException != null)
@@ -611,7 +581,7 @@ public class QJavalinImplementation
** Returns null if no param (or empty value).
** Throws QValueException for malformed numbers.
*******************************************************************************/
- private static Integer integerQueryParam(Context context, String name) throws QValueException
+ public static Integer integerQueryParam(Context context, String name) throws QValueException
{
String value = context.queryParam(name);
if(StringUtils.hasContent(value))
@@ -639,223 +609,4 @@ public class QJavalinImplementation
return (null);
}
-
-
- /*******************************************************************************
- ** Init a process (named in path param :process)
- **
- *******************************************************************************/
- private static void processInit(Context context) throws QException
- {
- doProcessInitOrStep(context, null, null);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep) throws QModuleDispatchException
- {
- if(processUUID == null)
- {
- processUUID = UUID.randomUUID().toString();
- }
-
- RunProcessRequest runProcessRequest = new RunProcessRequest(qInstance);
- setupSession(context, runProcessRequest);
- runProcessRequest.setProcessName(context.pathParam("processName"));
- runProcessRequest.setCallback(new QJavalinProcessCallback());
- runProcessRequest.setBackendOnly(true);
- runProcessRequest.setProcessUUID(processUUID);
- runProcessRequest.setStartAfterStep(startAfterStep);
- populateRunProcessRequestWithValuesFromContext(context, runProcessRequest);
-
- LOG.info(startAfterStep == null ? "Initiating process [" + runProcessRequest.getProcessName() + "] [" + processUUID + "]"
- : "Resuming process [" + runProcessRequest.getProcessName() + "] [" + processUUID + "] after step [" + startAfterStep + "]");
-
- Map resultForCaller = new HashMap<>();
- resultForCaller.put("processUUID", processUUID);
-
- try
- {
- ////////////////////////////////////////
- // run the process as an async action //
- ////////////////////////////////////////
- Integer timeout = getTimeoutMillis(context);
- RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) ->
- {
- runProcessRequest.setAsyncJobCallback(callback);
- return (new RunProcessAction().execute(runProcessRequest));
- });
-
- LOG.info("Process result error? " + runProcessResult.getException());
- for(QFieldMetaData outputField : qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields())
- {
- LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName()));
- }
- serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
- }
- catch(JobGoingAsyncException jgae)
- {
- resultForCaller.put("jobUUID", jgae.getJobUUID());
- }
- 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. //
- //////////////////////////////////////////////////////////////////////////////
- serializeRunProcessExceptionForCaller(resultForCaller, e);
- }
-
- context.result(JsonUtils.toJson(resultForCaller));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static Integer getTimeoutMillis(Context context)
- {
- Integer timeout = integerQueryParam(context, "_qStepTimeoutMillis");
- if(timeout == null)
- {
- timeout = ASYNC_STEP_TIMEOUT_MILLIS;
- }
- return timeout;
- }
-
-
-
- /*******************************************************************************
- ** Whether a step finished synchronously or asynchronously, return its data
- ** to the caller the same way.
- *******************************************************************************/
- private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult)
- {
- if(runProcessResult.getException().isPresent())
- {
- ////////////////////////////////////////////////////////////////
- // per code coverage, this path may never actually get hit... //
- ////////////////////////////////////////////////////////////////
- serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get());
- }
- resultForCaller.put("values", runProcessResult.getValues());
- runProcessResult.getProcessState().getNextStepName().ifPresent(lastStep -> resultForCaller.put("nextStep", lastStep));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static void serializeRunProcessExceptionForCaller(Map resultForCaller, Exception exception)
- {
- QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class);
-
- if(userFacingException != null)
- {
- LOG.info("User-facing exception in process", userFacingException);
- resultForCaller.put("error", userFacingException.getMessage()); // todo - put this somewhere else (make error an object w/ user-facing and/or other error?)
- }
- else
- {
- Throwable rootException = ExceptionUtils.getRootException(exception);
- LOG.warn("Uncaught Exception in process", exception);
- resultForCaller.put("error", "Original error message: " + rootException.getMessage());
- }
- }
-
-
-
- /*******************************************************************************
- ** take values from query-string params, and put them into the run process request
- ** todo - better from POST body, or with a "field-" type of prefix??
- **
- *******************************************************************************/
- private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest)
- {
- for(Map.Entry> queryParam : context.queryParamMap().entrySet())
- {
- String fieldName = queryParam.getKey();
- List values = queryParam.getValue();
- if(CollectionUtils.nullSafeHasContents(values))
- {
- runProcessRequest.addValue(fieldName, values.get(0));
- }
- }
- }
-
-
-
- /*******************************************************************************
- ** Run a step in a process (named in path param :processName)
- **
- *******************************************************************************/
- private static void processStep(Context context) throws QModuleDispatchException
- {
- String processUUID = context.pathParam("processUUID");
- String lastStep = context.pathParam("step");
- doProcessInitOrStep(context, processUUID, lastStep);
- }
-
-
-
- /*******************************************************************************
- ** Get status for a currently running process (step)
- *******************************************************************************/
- private static void processStatus(Context context)
- {
- String processUUID = context.pathParam("processUUID");
- String jobUUID = context.pathParam("jobUUID");
-
- LOG.info("Request for status of job " + jobUUID);
- Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
- if(optionalJobStatus.isEmpty())
- {
- handleException(context, new RuntimeException("Could not find status of process step job"));
- }
- else
- {
- Map resultForCaller = new HashMap<>();
- AsyncJobStatus jobStatus = optionalJobStatus.get();
-
- resultForCaller.put("jobStatus", jobStatus);
- LOG.info("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.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS));
- if(processState.isPresent())
- {
- RunProcessResult runProcessResult = new RunProcessResult(processState.get());
- serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
- }
- else
- {
- handleException(context, new RuntimeException("Could not find process results"));
- }
- }
- 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)
- {
- serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException());
- }
- }
-
- context.result(JsonUtils.toJson(resultForCaller));
- }
- }
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
new file mode 100644
index 00000000..b2e007ee
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
@@ -0,0 +1,409 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.javalin;
+
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
+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.async.JobGoingAsyncException;
+import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult;
+import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.state.StateType;
+import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+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 io.javalin.apibuilder.EndpointGroup;
+import io.javalin.http.Context;
+import org.apache.commons.lang.NotImplementedException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import static io.javalin.apibuilder.ApiBuilder.get;
+import static io.javalin.apibuilder.ApiBuilder.path;
+import static io.javalin.apibuilder.ApiBuilder.post;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QJavalinProcessHandler
+{
+ private static final Logger LOG = LogManager.getLogger(QJavalinProcessHandler.class);
+
+ private static int ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
+
+
+
+ /*******************************************************************************
+ ** Define the routes
+ *******************************************************************************/
+ public static EndpointGroup getRoutes()
+ {
+ return (() ->
+ {
+ path("/processes", () ->
+ {
+ path("/:processName", () ->
+ {
+ get("/init", QJavalinProcessHandler::processInit);
+ post("/init", QJavalinProcessHandler::processInit);
+
+ path("/:processUUID", () ->
+ {
+ post("/step/:step", QJavalinProcessHandler::processStep);
+ get("/status/:jobUUID", QJavalinProcessHandler::processStatus);
+ });
+ });
+ });
+ });
+ }
+
+
+
+ /*******************************************************************************
+ ** Init a process (named in path param :process)
+ **
+ *******************************************************************************/
+ public static void processInit(Context context) throws QException
+ {
+ doProcessInitOrStep(context, null, null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep)
+ {
+ Map resultForCaller = new HashMap<>();
+
+ try
+ {
+ if(processUUID == null)
+ {
+ processUUID = UUID.randomUUID().toString();
+ }
+ resultForCaller.put("processUUID", processUUID);
+
+ String processName = context.pathParam("processName");
+ LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
+ : "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
+
+ RunProcessRequest runProcessRequest = new RunProcessRequest(QJavalinImplementation.qInstance);
+ QJavalinImplementation.setupSession(context, runProcessRequest);
+ runProcessRequest.setProcessName(processName);
+ runProcessRequest.setFrontendStepBehavior(RunProcessRequest.FrontendStepBehavior.BREAK);
+ runProcessRequest.setProcessUUID(processUUID);
+ runProcessRequest.setStartAfterStep(startAfterStep);
+ populateRunProcessRequestWithValuesFromContext(context, runProcessRequest);
+
+ try
+ {
+ ////////////////////////////////////////
+ // run the process as an async action //
+ ////////////////////////////////////////
+ Integer timeout = getTimeoutMillis(context);
+ RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) ->
+ {
+ runProcessRequest.setAsyncJobCallback(callback);
+ return (new RunProcessAction().execute(runProcessRequest));
+ });
+
+ LOG.info("Process result error? " + runProcessResult.getException());
+ for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields())
+ {
+ LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName()));
+ }
+ serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
+ }
+ catch(JobGoingAsyncException jgae)
+ {
+ resultForCaller.put("jobUUID", jgae.getJobUUID());
+ }
+ }
+ 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. //
+ //////////////////////////////////////////////////////////////////////////////
+ serializeRunProcessExceptionForCaller(resultForCaller, e);
+ }
+
+ context.result(JsonUtils.toJson(resultForCaller));
+ }
+
+
+
+ /*******************************************************************************
+ ** Whether a step finished synchronously or asynchronously, return its data
+ ** to the caller the same way.
+ *******************************************************************************/
+ private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult)
+ {
+ if(runProcessResult.getException().isPresent())
+ {
+ ////////////////////////////////////////////////////////////////
+ // per code coverage, this path may never actually get hit... //
+ ////////////////////////////////////////////////////////////////
+ serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get());
+ }
+ resultForCaller.put("values", runProcessResult.getValues());
+ runProcessResult.getProcessState().getNextStepName().ifPresent(lastStep -> resultForCaller.put("nextStep", lastStep));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void serializeRunProcessExceptionForCaller(Map resultForCaller, Exception exception)
+ {
+ QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class);
+
+ if(userFacingException != null)
+ {
+ LOG.info("User-facing exception in process", userFacingException);
+ resultForCaller.put("error", userFacingException.getMessage()); // todo - put this somewhere else (make error an object w/ user-facing and/or other error?)
+ }
+ else
+ {
+ Throwable rootException = ExceptionUtils.getRootException(exception);
+ LOG.warn("Uncaught Exception in process", exception);
+ resultForCaller.put("error", "Original error message: " + rootException.getMessage());
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** take values from query-string params, and put them into the run process request
+ ** todo - better from POST body, or with a "field-" type of prefix??
+ **
+ *******************************************************************************/
+ private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) throws IOException
+ {
+ for(Map.Entry> queryParam : context.queryParamMap().entrySet())
+ {
+ String fieldName = queryParam.getKey();
+ List values = queryParam.getValue();
+ if(CollectionUtils.nullSafeHasContents(values))
+ {
+ runProcessRequest.addValue(fieldName, values.get(0));
+ }
+ }
+
+ QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessRequest);
+ if(initialRecordsFilter != null)
+ {
+ runProcessRequest.setCallback(new QProcessCallback()
+ {
+ @Override
+ public QQueryFilter getQueryFilter()
+ {
+ return (initialRecordsFilter);
+ }
+
+
+
+ @Override
+ public Map getFieldValues(List fields)
+ {
+ return (Collections.emptyMap());
+ }
+ });
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessRequest runProcessRequest) throws IOException
+ {
+ QInstance instance = runProcessRequest.getInstance();
+ QProcessMetaData process = instance.getProcess(runProcessRequest.getProcessName());
+ QTableMetaData table = instance.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();
+
+ String recordsParam = context.queryParam("recordsParam");
+ if(StringUtils.hasContent(recordsParam))
+ {
+ @SuppressWarnings("ConstantConditions")
+ String paramValue = context.queryParam(recordsParam);
+ if(!StringUtils.hasContent(paramValue))
+ {
+ throw (new IllegalArgumentException("Missing value in query parameter: " + recordsParam + " (which was specified as the recordsParam)"));
+ }
+
+ switch(recordsParam)
+ {
+ case "recordIds":
+ @SuppressWarnings("ConstantConditions")
+ Serializable[] idStrings = context.queryParam(recordsParam).split(",");
+ return (new QQueryFilter().withCriteria(new QFilterCriteria()
+ .withFieldName(primaryKeyField)
+ .withOperator(QCriteriaOperator.IN)
+ .withValues(Arrays.stream(idStrings).toList())));
+ case "filterJSON":
+ return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class));
+ case "filterId":
+ // return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class));
+ throw (new NotImplementedException("Saved filters are not yet implemented."));
+ default:
+ throw (new IllegalArgumentException("Unrecognized value [" + recordsParam + "] for query parameter: recordsParam"));
+ }
+ }
+
+ return (null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Run a step in a process (named in path param :processName)
+ **
+ *******************************************************************************/
+ public static void processStep(Context context) throws QModuleDispatchException
+ {
+ String processUUID = context.pathParam("processUUID");
+ String lastStep = context.pathParam("step");
+ doProcessInitOrStep(context, processUUID, lastStep);
+ }
+
+
+
+ /*******************************************************************************
+ ** Get status for a currently running process (step)
+ *******************************************************************************/
+ public static void processStatus(Context context)
+ {
+ String processUUID = context.pathParam("processUUID");
+ String jobUUID = context.pathParam("jobUUID");
+
+ LOG.info("Request for status of job " + jobUUID);
+ Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
+ if(optionalJobStatus.isEmpty())
+ {
+ QJavalinImplementation.handleException(context, new RuntimeException("Could not find status of process step job"));
+ }
+ else
+ {
+ Map resultForCaller = new HashMap<>();
+ AsyncJobStatus jobStatus = optionalJobStatus.get();
+
+ resultForCaller.put("jobStatus", jobStatus);
+ LOG.info("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.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS));
+ if(processState.isPresent())
+ {
+ RunProcessResult runProcessResult = new RunProcessResult(processState.get());
+ serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
+ }
+ else
+ {
+ QJavalinImplementation.handleException(context, new RuntimeException("Could not find process results"));
+ }
+ }
+ 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)
+ {
+ serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException());
+ }
+ }
+
+ context.result(JsonUtils.toJson(resultForCaller));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void setAsyncStepTimeoutMillis(int asyncStepTimeoutMillis)
+ {
+ ASYNC_STEP_TIMEOUT_MILLIS = asyncStepTimeoutMillis;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static Integer getTimeoutMillis(Context context)
+ {
+ Integer timeout = QJavalinImplementation.integerQueryParam(context, "_qStepTimeoutMillis");
+ if(timeout == null)
+ {
+ timeout = ASYNC_STEP_TIMEOUT_MILLIS;
+ }
+ return timeout;
+ }
+
+}
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
index 9fcc095e..6d002780 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -27,18 +27,14 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
-import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONObject;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -51,39 +47,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
** and actually makes http requests into it.
**
*******************************************************************************/
-class QJavalinImplementationTest
+class QJavalinImplementationTest extends QJavalinTestBase
{
- private static final int PORT = 6262;
- private static final String BASE_URL = "http://localhost:" + PORT;
- private static final int MORE_THAN_TIMEOUT = 500;
- private static final int LESS_THAN_TIMEOUT = 50;
-
-
-
- /*******************************************************************************
- ** Before the class (all) runs, start a javalin server.
- **
- *******************************************************************************/
- @BeforeAll
- public static void beforeAll()
- {
- QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance());
- QJavalinImplementation.setAsyncStepTimeoutMillis(250);
- qJavalinImplementation.startJavalinServer(PORT);
- }
-
-
-
- /*******************************************************************************
- ** Fully rebuild the test-database before each test runs, for completely known state.
- **
- *******************************************************************************/
- @BeforeEach
- public void beforeEach() throws Exception
- {
- TestUtils.primeTestDatabase();
- }
@@ -406,293 +372,4 @@ class QJavalinImplementationTest
}));
}
-
-
- /*******************************************************************************
- ** test running a process
- **
- *******************************************************************************/
- @Test
- public void test_processGreetInit()
- {
- HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init").asString();
- assertEquals(200, response.getStatus());
- JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
- assertNotNull(jsonObject);
- assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage"));
- }
-
-
-
- /*******************************************************************************
- ** test running a process with field values on the query string
- **
- *******************************************************************************/
- @Test
- public void test_processGreetInitWithQueryValues()
- {
- HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude").asString();
- assertEquals(200, response.getStatus());
- JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
- assertNotNull(jsonObject);
- assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage"));
- }
-
-
-
- /*******************************************************************************
- ** test init'ing a process that goes async
- **
- *******************************************************************************/
- @Test
- public void test_processInitGoingAsync() throws InterruptedException
- {
- String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP;
- HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString();
-
- JSONObject jsonObject = assertProcessStepWentAsyncResponse(response);
- String processUUID = jsonObject.getString("processUUID");
- String jobUUID = jsonObject.getString("jobUUID");
- assertNotNull(processUUID, "Process UUID should not be null.");
- assertNotNull(jobUUID, "Job UUID should not be null");
-
- /////////////////////////////////////////////
- // request job status before sleep is done //
- /////////////////////////////////////////////
- response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
- jsonObject = assertProcessStepRunningResponse(response);
-
- ///////////////////////////////////
- // sleep, to let that job finish //
- ///////////////////////////////////
- Thread.sleep(MORE_THAN_TIMEOUT);
-
- ////////////////////////////////////////////////////////
- // request job status again, get back results instead //
- ////////////////////////////////////////////////////////
- response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
- jsonObject = assertProcessStepCompleteResponse(response);
- }
-
-
-
- /*******************************************************************************
- ** test init'ing a process that does NOT goes async
- **
- *******************************************************************************/
- @Test
- public void test_processInitNotGoingAsync()
- {
- HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
- .header("Content-Type", "application/json").asString();
- assertProcessStepCompleteResponse(response);
- }
-
-
-
- /*******************************************************************************
- ** test running a step a process that goes async
- **
- *******************************************************************************/
- @Test
- public void test_processStepGoingAsync() throws InterruptedException
- {
- /////////////////////////////////////////////
- // first init the process, to get its UUID //
- /////////////////////////////////////////////
- String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
- HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT)
- .header("Content-Type", "application/json").asString();
-
- JSONObject jsonObject = assertProcessStepCompleteResponse(response);
- String processUUID = jsonObject.getString("processUUID");
- String nextStep = jsonObject.getString("nextStep");
- assertNotNull(processUUID, "Process UUID should not be null.");
- assertNotNull(nextStep, "There should be a next step");
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep)
- .header("Content-Type", "application/json").asString();
-
- jsonObject = assertProcessStepWentAsyncResponse(response);
- String jobUUID = jsonObject.getString("jobUUID");
-
- ///////////////////////////////////
- // sleep, to let that job finish //
- ///////////////////////////////////
- Thread.sleep(MORE_THAN_TIMEOUT);
-
- ///////////////////////////////
- // third, request job status //
- ///////////////////////////////
- response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
-
- jsonObject = assertProcessStepCompleteResponse(response);
- String nextStep2 = jsonObject.getString("nextStep");
- assertNotNull(nextStep2, "There be one more next step");
- assertNotEquals(nextStep, nextStep2, "The next step should be different this time.");
- }
-
-
-
- /*******************************************************************************
- ** test running a step a process that does NOT goes async
- **
- *******************************************************************************/
- @Test
- public void test_processStepNotGoingAsync()
- {
- /////////////////////////////////////////////
- // first init the process, to get its UUID //
- /////////////////////////////////////////////
- String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
- HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
- .header("Content-Type", "application/json").asString();
-
- JSONObject jsonObject = assertProcessStepCompleteResponse(response);
- String processUUID = jsonObject.getString("processUUID");
- String nextStep = jsonObject.getString("nextStep");
- assertNotNull(nextStep, "There should be a next step");
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // second, run the 'nextStep' (the backend step, that sleeps). run it with a short enough sleep so that it won't go async //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep)
- .header("Content-Type", "application/json").asString();
-
- jsonObject = assertProcessStepCompleteResponse(response);
- String nextStep2 = jsonObject.getString("nextStep");
- assertNotNull(nextStep2, "There be one more next step");
- assertNotEquals(nextStep, nextStep2, "The next step should be different this time.");
- }
-
-
-
- /*******************************************************************************
- ** test init'ing a process that goes async and then throws
- **
- *******************************************************************************/
- @Test
- public void test_processInitGoingAsyncThenThrowing() throws InterruptedException
- {
- String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW;
- HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString();
-
- JSONObject jsonObject = assertProcessStepWentAsyncResponse(response);
- String processUUID = jsonObject.getString("processUUID");
- String jobUUID = jsonObject.getString("jobUUID");
-
- /////////////////////////////////////////////
- // request job status before sleep is done //
- /////////////////////////////////////////////
- response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
- jsonObject = assertProcessStepRunningResponse(response);
-
- ///////////////////////////////////
- // sleep, to let that job finish //
- ///////////////////////////////////
- Thread.sleep(MORE_THAN_TIMEOUT);
-
- /////////////////////////////////////////////////////////////
- // request job status again, get back error status instead //
- /////////////////////////////////////////////////////////////
- response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
- jsonObject = assertProcessStepErrorResponse(response);
- }
-
-
-
- /*******************************************************************************
- ** test init'ing a process that does NOT goes async, but throws.
- **
- *******************************************************************************/
- @Test
- public void test_processInitNotGoingAsyncButThrowing()
- {
- HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
- .header("Content-Type", "application/json").asString();
- assertProcessStepErrorResponse(response);
- }
-
-
-
- /*******************************************************************************
- ** every time a process step (sync or async) has gone async, expect what the
- ** response should look like
- *******************************************************************************/
- private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response)
- {
- assertEquals(200, response.getStatus());
- JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
-
- assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID");
- assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID");
-
- assertFalse(jsonObject.has("values"), "Async-started response should NOT have values");
- assertFalse(jsonObject.has("error"), "Async-started response should NOT have error");
-
- return (jsonObject);
- }
-
-
-
- /*******************************************************************************
- ** every time a process step (sync or async) is still running, expect certain things
- ** to be (and not to be) in the json response.
- *******************************************************************************/
- private JSONObject assertProcessStepRunningResponse(HttpResponse response)
- {
- assertEquals(200, response.getStatus());
- JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
-
- assertTrue(jsonObject.has("jobStatus"), "Step Running response should have a jobStatus");
-
- assertFalse(jsonObject.has("values"), "Step Running response should NOT have values");
- assertFalse(jsonObject.has("error"), "Step Running response should NOT have error");
-
- assertEquals(AsyncJobState.RUNNING.name(), jsonObject.getJSONObject("jobStatus").getString("state"));
-
- return (jsonObject);
- }
-
-
-
- /*******************************************************************************
- ** every time a process step (sync or async) completes, expect certain things
- ** to be (and not to be) in the json response.
- *******************************************************************************/
- private JSONObject assertProcessStepCompleteResponse(HttpResponse response)
- {
- assertEquals(200, response.getStatus());
- JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
-
- assertTrue(jsonObject.has("values"), "Step Complete response should have values");
-
- assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID");
- assertFalse(jsonObject.has("error"), "Step Complete response should not have an error");
-
- return (jsonObject);
- }
-
-
-
- /*******************************************************************************
- ** every time a process step (sync or async) has an error, expect certain things
- ** to be (and not to be) in the json response.
- *******************************************************************************/
- private JSONObject assertProcessStepErrorResponse(HttpResponse response)
- {
- assertEquals(200, response.getStatus());
- JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
-
- assertTrue(jsonObject.has("error"), "Step Error response should have an error");
-
- assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID");
- assertFalse(jsonObject.has("values"), "Step Error response should not have values");
-
- return (jsonObject);
- }
-
}
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
new file mode 100644
index 00000000..b1644e49
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
@@ -0,0 +1,398 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.javalin;
+
+
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
+import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import kong.unirest.HttpResponse;
+import kong.unirest.Unirest;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for the javalin process handler methods.
+ *******************************************************************************/
+class QJavalinProcessHandlerTest extends QJavalinTestBase
+{
+ private static final int MORE_THAN_TIMEOUT = 500;
+ private static final int LESS_THAN_TIMEOUT = 50;
+
+
+
+ /*******************************************************************************
+ ** test running a process
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processGreetInit()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3").asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject);
+ assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage"));
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a process that requires rows, but we didn't tell it how to get them.
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processRequiresRowsButNotSpecified()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init").asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject);
+ assertTrue(jsonObject.has("error"));
+ assertTrue(jsonObject.getString("error").contains("missing input records"));
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a process and telling it rows to load via recordIds param
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processRequiresRowsWithRecordIdParam()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3").asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject);
+
+ // todo - once we know how to get records from a process, add that call
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a process and telling it rows to load via filter JSON
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processRequiresRowsWithFilterJSON()
+ {
+ QQueryFilter queryFilter = new QQueryFilter()
+ .withCriteria(new QFilterCriteria()
+ .withFieldName("id")
+ .withOperator(QCriteriaOperator.IN)
+ .withValues(List.of(3, 4, 5)));
+ String filterJSON = JsonUtils.toJson(queryFilter);
+
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(filterJSON, Charset.defaultCharset())).asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject);
+
+ // todo - once we know how to get records from a process, add that call
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a process with field values on the query string
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processGreetInitWithQueryValues()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString();
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject);
+ assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage"));
+ }
+
+
+
+ /*******************************************************************************
+ ** test init'ing a process that goes async
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processInitGoingAsync() throws InterruptedException
+ {
+ String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP;
+ HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString();
+
+ JSONObject jsonObject = assertProcessStepWentAsyncResponse(response);
+ String processUUID = jsonObject.getString("processUUID");
+ String jobUUID = jsonObject.getString("jobUUID");
+ assertNotNull(processUUID, "Process UUID should not be null.");
+ assertNotNull(jobUUID, "Job UUID should not be null");
+
+ /////////////////////////////////////////////
+ // request job status before sleep is done //
+ /////////////////////////////////////////////
+ response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
+ jsonObject = assertProcessStepRunningResponse(response);
+
+ ///////////////////////////////////
+ // sleep, to let that job finish //
+ ///////////////////////////////////
+ Thread.sleep(MORE_THAN_TIMEOUT);
+
+ ////////////////////////////////////////////////////////
+ // request job status again, get back results instead //
+ ////////////////////////////////////////////////////////
+ response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
+ jsonObject = assertProcessStepCompleteResponse(response);
+ }
+
+
+
+ /*******************************************************************************
+ ** test init'ing a process that does NOT goes async
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processInitNotGoingAsync()
+ {
+ HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
+ .header("Content-Type", "application/json").asString();
+ assertProcessStepCompleteResponse(response);
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a step a process that goes async
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processStepGoingAsync() throws InterruptedException
+ {
+ /////////////////////////////////////////////
+ // first init the process, to get its UUID //
+ /////////////////////////////////////////////
+ String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
+ HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT)
+ .header("Content-Type", "application/json").asString();
+
+ JSONObject jsonObject = assertProcessStepCompleteResponse(response);
+ String processUUID = jsonObject.getString("processUUID");
+ String nextStep = jsonObject.getString("nextStep");
+ assertNotNull(processUUID, "Process UUID should not be null.");
+ assertNotNull(nextStep, "There should be a next step");
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep)
+ .header("Content-Type", "application/json").asString();
+
+ jsonObject = assertProcessStepWentAsyncResponse(response);
+ String jobUUID = jsonObject.getString("jobUUID");
+
+ ///////////////////////////////////
+ // sleep, to let that job finish //
+ ///////////////////////////////////
+ Thread.sleep(MORE_THAN_TIMEOUT);
+
+ ///////////////////////////////
+ // third, request job status //
+ ///////////////////////////////
+ response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
+
+ jsonObject = assertProcessStepCompleteResponse(response);
+ String nextStep2 = jsonObject.getString("nextStep");
+ assertNotNull(nextStep2, "There be one more next step");
+ assertNotEquals(nextStep, nextStep2, "The next step should be different this time.");
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a step a process that does NOT goes async
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processStepNotGoingAsync()
+ {
+ /////////////////////////////////////////////
+ // first init the process, to get its UUID //
+ /////////////////////////////////////////////
+ String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
+ HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
+ .header("Content-Type", "application/json").asString();
+
+ JSONObject jsonObject = assertProcessStepCompleteResponse(response);
+ String processUUID = jsonObject.getString("processUUID");
+ String nextStep = jsonObject.getString("nextStep");
+ assertNotNull(nextStep, "There should be a next step");
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // second, run the 'nextStep' (the backend step, that sleeps). run it with a short enough sleep so that it won't go async //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep)
+ .header("Content-Type", "application/json").asString();
+
+ jsonObject = assertProcessStepCompleteResponse(response);
+ String nextStep2 = jsonObject.getString("nextStep");
+ assertNotNull(nextStep2, "There be one more next step");
+ assertNotEquals(nextStep, nextStep2, "The next step should be different this time.");
+ }
+
+
+
+ /*******************************************************************************
+ ** test init'ing a process that goes async and then throws
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processInitGoingAsyncThenThrowing() throws InterruptedException
+ {
+ String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW;
+ HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString();
+
+ JSONObject jsonObject = assertProcessStepWentAsyncResponse(response);
+ String processUUID = jsonObject.getString("processUUID");
+ String jobUUID = jsonObject.getString("jobUUID");
+
+ /////////////////////////////////////////////
+ // request job status before sleep is done //
+ /////////////////////////////////////////////
+ response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
+ jsonObject = assertProcessStepRunningResponse(response);
+
+ ///////////////////////////////////
+ // sleep, to let that job finish //
+ ///////////////////////////////////
+ Thread.sleep(MORE_THAN_TIMEOUT);
+
+ /////////////////////////////////////////////////////////////
+ // request job status again, get back error status instead //
+ /////////////////////////////////////////////////////////////
+ response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
+ jsonObject = assertProcessStepErrorResponse(response);
+ }
+
+
+
+ /*******************************************************************************
+ ** test init'ing a process that does NOT goes async, but throws.
+ **
+ *******************************************************************************/
+ @Test
+ public void test_processInitNotGoingAsyncButThrowing()
+ {
+ HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
+ .header("Content-Type", "application/json").asString();
+ assertProcessStepErrorResponse(response);
+ }
+
+
+
+ /*******************************************************************************
+ ** every time a process step (sync or async) has gone async, expect what the
+ ** response should look like
+ *******************************************************************************/
+ private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response)
+ {
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+
+ assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID");
+ assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID");
+
+ assertFalse(jsonObject.has("values"), "Async-started response should NOT have values");
+ assertFalse(jsonObject.has("error"), "Async-started response should NOT have error");
+
+ return (jsonObject);
+ }
+
+
+
+ /*******************************************************************************
+ ** every time a process step (sync or async) is still running, expect certain things
+ ** to be (and not to be) in the json response.
+ *******************************************************************************/
+ private JSONObject assertProcessStepRunningResponse(HttpResponse response)
+ {
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+
+ assertTrue(jsonObject.has("jobStatus"), "Step Running response should have a jobStatus");
+
+ assertFalse(jsonObject.has("values"), "Step Running response should NOT have values");
+ assertFalse(jsonObject.has("error"), "Step Running response should NOT have error");
+
+ assertEquals(AsyncJobState.RUNNING.name(), jsonObject.getJSONObject("jobStatus").getString("state"));
+
+ return (jsonObject);
+ }
+
+
+
+ /*******************************************************************************
+ ** every time a process step (sync or async) completes, expect certain things
+ ** to be (and not to be) in the json response.
+ *******************************************************************************/
+ private JSONObject assertProcessStepCompleteResponse(HttpResponse response)
+ {
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+
+ assertTrue(jsonObject.has("values"), "Step Complete response should have values");
+
+ assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID");
+ assertFalse(jsonObject.has("error"), "Step Complete response should not have an error");
+
+ return (jsonObject);
+ }
+
+
+
+ /*******************************************************************************
+ ** every time a process step (sync or async) has an error, expect certain things
+ ** to be (and not to be) in the json response.
+ *******************************************************************************/
+ private JSONObject assertProcessStepErrorResponse(HttpResponse response)
+ {
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+
+ assertTrue(jsonObject.has("error"), "Step Error response should have an error");
+
+ assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID");
+ assertFalse(jsonObject.has("values"), "Step Error response should not have values");
+
+ return (jsonObject);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java
new file mode 100644
index 00000000..168174c2
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java
@@ -0,0 +1,78 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.javalin;
+
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+
+/*******************************************************************************
+ ** base class for javalin implementation tests.
+ *******************************************************************************/
+public class QJavalinTestBase
+{
+ private static final int PORT = 6262;
+ protected static final String BASE_URL = "http://localhost:" + PORT;
+
+ private static QJavalinImplementation qJavalinImplementation;
+
+
+
+ /*******************************************************************************
+ ** Before the class (all) runs, start a javalin server.
+ **
+ *******************************************************************************/
+ @BeforeAll
+ public static void beforeAll()
+ {
+ qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance());
+ QJavalinProcessHandler.setAsyncStepTimeoutMillis(250);
+ qJavalinImplementation.startJavalinServer(PORT);
+ }
+
+
+
+ /*******************************************************************************
+ ** Before the class (all) runs, start a javalin server.
+ **
+ *******************************************************************************/
+ @AfterAll
+ public static void afterAll()
+ {
+ qJavalinImplementation.stopJavalinServer();
+ }
+
+
+
+ /*******************************************************************************
+ ** Fully rebuild the test-database before each test runs, for completely known state.
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ TestUtils.primeTestDatabase();
+ }
+
+}