diff --git a/.circleci/config.yml b/.circleci/config.yml
index fe4c371c..658f75d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -24,6 +24,8 @@ commands:
name: Run Maven
command: |
mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >>
+ - store_artifacts:
+ path: target/site/jacoco
- run:
name: Save test results
command: |
diff --git a/.gitignore b/.gitignore
index 39736a21..2c7054c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
target/
*.iml
+.env
#############################################
@@ -28,3 +29,4 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+.DS_Store
diff --git a/pom.xml b/pom.xml
index 3a7158aa..6b94a87c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
com.kingsrook.qqq
qqq-middleware-javalin
- 0.1.0
+ 0.2.0
scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git
@@ -53,12 +53,12 @@
com.kingsrook.qqq
qqq-backend-core
- 0.1.0
+ 0.2.0
com.kingsrook.qqq
qqq-backend-module-rdbms
- 0.1.0
+ 0.2.0
test
@@ -66,7 +66,7 @@
io.javalin
javalin
- 3.13.13
+ 4.6.1
com.konghq
@@ -85,6 +85,12 @@
slf4j-simple
1.7.36
+
+ org.assertj
+ assertj-core
+ 3.23.1
+ test
+
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 437f982f..2dddf8d6 100644
--- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -24,50 +24,61 @@ package com.kingsrook.qqq.backend.javalin;
import java.io.File;
import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-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.TableMetaDataAction;
-import com.kingsrook.qqq.backend.core.actions.UpdateAction;
+import java.util.Objects;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
+import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction;
+import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
+import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
-import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest;
-import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest;
-import com.kingsrook.qqq.backend.core.model.actions.count.CountResult;
-import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest;
-import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult;
-import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest;
-import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult;
-import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest;
-import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataResult;
-import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataRequest;
-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.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.actions.query.QueryRequest;
-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.actions.AbstractActionInput;
+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.actions.metadata.ProcessMetaDataInput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher;
-import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -97,6 +108,7 @@ public class QJavalinImplementation
private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class);
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
+ private static final String SESSION_ID_COOKIE_NAME = "sessionId";
static QInstance qInstance;
@@ -182,36 +194,46 @@ public class QJavalinImplementation
{
return (() ->
{
+ /////////////////////
+ // metadata routes //
+ /////////////////////
path("/metaData", () ->
{
get("/", QJavalinImplementation::metaData);
- path("/table/:table", () ->
+ path("/table/{table}", () ->
{
get("", QJavalinImplementation::tableMetaData);
});
- path("/process/:processName", () ->
+ path("/process/{processName}", () ->
{
get("", QJavalinImplementation::processMetaData);
});
});
- path("/data", () ->
- {
- path("/:table", () ->
- {
- get("/", QJavalinImplementation::dataQuery);
- post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single.
- get("/count", QJavalinImplementation::dataCount);
- // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records.
- path("/:primaryKey", () ->
- {
- get("", QJavalinImplementation::dataGet);
- patch("", QJavalinImplementation::dataUpdate);
- put("", QJavalinImplementation::dataUpdate); // todo - want different semantics??
- delete("", QJavalinImplementation::dataDelete);
- });
+ /////////////////////////
+ // table (data) routes //
+ /////////////////////////
+ path("/data/{table}", () ->
+ {
+ get("/", QJavalinImplementation::dataQuery);
+ post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single.
+ get("/count", QJavalinImplementation::dataCount);
+ get("/export", QJavalinImplementation::dataExportWithoutFilename);
+ get("/export/{filename}", QJavalinImplementation::dataExportWithFilename);
+
+ // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records.
+ path("/{primaryKey}", () ->
+ {
+ get("", QJavalinImplementation::dataGet);
+ patch("", QJavalinImplementation::dataUpdate);
+ put("", QJavalinImplementation::dataUpdate); // todo - want different semantics??
+ delete("", QJavalinImplementation::dataDelete);
});
});
+
+ ////////////////////
+ // process routes //
+ ////////////////////
path("", QJavalinProcessHandler.getRoutes());
});
}
@@ -221,18 +243,30 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
- static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException
+ static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
- QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData());
- // todo - does this need some per-provider logic actually? mmm...
- Map authenticationContext = new HashMap<>();
- authenticationContext.put("sessionId", context.cookie("sessionId"));
- QSession session = authenticationModule.createSession(authenticationContext);
- request.setSession(session);
+ try
+ {
+ Map authenticationContext = new HashMap<>();
+ authenticationContext.put(SESSION_ID_COOKIE_NAME, context.cookie(SESSION_ID_COOKIE_NAME));
+ QSession session = authenticationModule.createSession(qInstance, authenticationContext);
+ input.setSession(session);
- context.cookie("sessionId", session.getIdReference(), SESSION_COOKIE_AGE);
+ context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE);
+ }
+ catch(QAuthenticationException qae)
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // if exception caught, clear out the cookie so the frontend will reauthorize //
+ ////////////////////////////////////////////////////////////////////////////////
+ if(authenticationModule instanceof Auth0AuthenticationModule)
+ {
+ context.removeCookie(SESSION_ID_COOKIE_NAME);
+ }
+ }
}
@@ -244,17 +278,17 @@ public class QJavalinImplementation
{
try
{
- String table = context.pathParam("table");
+ String table = context.pathParam("table");
List primaryKeys = new ArrayList<>();
primaryKeys.add(context.pathParam("primaryKey"));
- DeleteRequest deleteRequest = new DeleteRequest(qInstance);
- setupSession(context, deleteRequest);
- deleteRequest.setTableName(table);
- deleteRequest.setPrimaryKeys(primaryKeys);
+ DeleteInput deleteInput = new DeleteInput(qInstance);
+ setupSession(context, deleteInput);
+ deleteInput.setTableName(table);
+ deleteInput.setPrimaryKeys(primaryKeys);
DeleteAction deleteAction = new DeleteAction();
- DeleteResult deleteResult = deleteAction.execute(deleteRequest);
+ DeleteOutput deleteResult = deleteAction.execute(deleteInput);
context.result(JsonUtils.toJson(deleteResult));
}
@@ -273,9 +307,9 @@ public class QJavalinImplementation
{
try
{
- String table = context.pathParam("table");
+ String table = context.pathParam("table");
List recordList = new ArrayList<>();
- QRecord record = new QRecord();
+ QRecord record = new QRecord();
record.setTableName(table);
recordList.add(record);
@@ -292,13 +326,13 @@ public class QJavalinImplementation
record.setValue(tableMetaData.getPrimaryKeyField(), context.pathParam("primaryKey"));
- UpdateRequest updateRequest = new UpdateRequest(qInstance);
- setupSession(context, updateRequest);
- updateRequest.setTableName(table);
- updateRequest.setRecords(recordList);
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ setupSession(context, updateInput);
+ updateInput.setTableName(table);
+ updateInput.setRecords(recordList);
UpdateAction updateAction = new UpdateAction();
- UpdateResult updateResult = updateAction.execute(updateRequest);
+ UpdateOutput updateResult = updateAction.execute(updateInput);
context.result(JsonUtils.toJson(updateResult));
}
@@ -317,9 +351,9 @@ public class QJavalinImplementation
{
try
{
- String table = context.pathParam("table");
+ String table = context.pathParam("table");
List recordList = new ArrayList<>();
- QRecord record = new QRecord();
+ QRecord record = new QRecord();
record.setTableName(table);
recordList.add(record);
@@ -332,15 +366,15 @@ public class QJavalinImplementation
}
}
- InsertRequest insertRequest = new InsertRequest(qInstance);
- setupSession(context, insertRequest);
- insertRequest.setTableName(table);
- insertRequest.setRecords(recordList);
+ InsertInput insertInput = new InsertInput(qInstance);
+ setupSession(context, insertInput);
+ insertInput.setTableName(table);
+ insertInput.setRecords(recordList);
InsertAction insertAction = new InsertAction();
- InsertResult insertResult = insertAction.execute(insertRequest);
+ InsertOutput insertOutput = insertAction.execute(insertInput);
- context.result(JsonUtils.toJson(insertResult));
+ context.result(JsonUtils.toJson(insertOutput));
}
catch(Exception e)
{
@@ -357,13 +391,13 @@ public class QJavalinImplementation
{
try
{
- String tableName = context.pathParam("table");
- QTableMetaData table = qInstance.getTable(tableName);
- String primaryKey = context.pathParam("primaryKey");
- QueryRequest queryRequest = new QueryRequest(qInstance);
+ String tableName = context.pathParam("table");
+ QTableMetaData table = qInstance.getTable(tableName);
+ String primaryKey = context.pathParam("primaryKey");
+ QueryInput queryInput = new QueryInput(qInstance);
- setupSession(context, queryRequest);
- queryRequest.setTableName(tableName);
+ setupSession(context, queryInput);
+ queryInput.setTableName(tableName);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
@@ -371,25 +405,25 @@ public class QJavalinImplementation
///////////////////////////////////////////////////////
// setup a filter for the primaryKey = the path-pram //
///////////////////////////////////////////////////////
- queryRequest.setFilter(new QQueryFilter()
+ queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria()
.withFieldName(table.getPrimaryKeyField())
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(primaryKey))));
QueryAction queryAction = new QueryAction();
- QueryResult queryResult = queryAction.execute(queryRequest);
+ QueryOutput queryOutput = queryAction.execute(queryInput);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
- if(queryResult.getRecords().isEmpty())
+ if(queryOutput.getRecords().isEmpty())
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
- context.result(JsonUtils.toJson(queryResult.getRecords().get(0)));
+ context.result(JsonUtils.toJson(queryOutput.getRecords().get(0)));
}
catch(Exception e)
{
@@ -415,20 +449,20 @@ public class QJavalinImplementation
{
try
{
- CountRequest countRequest = new CountRequest(qInstance);
- setupSession(context, countRequest);
- countRequest.setTableName(context.pathParam("table"));
+ CountInput countInput = new CountInput(qInstance);
+ setupSession(context, countInput);
+ countInput.setTableName(context.pathParam("table"));
String filter = stringQueryParam(context, "filter");
if(filter != null)
{
- countRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
+ countInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
}
CountAction countAction = new CountAction();
- CountResult countResult = countAction.execute(countRequest);
+ CountOutput countOutput = countAction.execute(countInput);
- context.result(JsonUtils.toJson(countResult));
+ context.result(JsonUtils.toJson(countOutput));
}
catch(Exception e)
{
@@ -456,22 +490,22 @@ public class QJavalinImplementation
{
try
{
- QueryRequest queryRequest = new QueryRequest(qInstance);
- setupSession(context, queryRequest);
- queryRequest.setTableName(context.pathParam("table"));
- queryRequest.setSkip(integerQueryParam(context, "skip"));
- queryRequest.setLimit(integerQueryParam(context, "limit"));
+ QueryInput queryInput = new QueryInput(qInstance);
+ setupSession(context, queryInput);
+ queryInput.setTableName(context.pathParam("table"));
+ queryInput.setSkip(integerQueryParam(context, "skip"));
+ queryInput.setLimit(integerQueryParam(context, "limit"));
String filter = stringQueryParam(context, "filter");
if(filter != null)
{
- queryRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
+ queryInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
}
QueryAction queryAction = new QueryAction();
- QueryResult queryResult = queryAction.execute(queryRequest);
+ QueryOutput queryOutput = queryAction.execute(queryInput);
- context.result(JsonUtils.toJson(queryResult));
+ context.result(JsonUtils.toJson(queryOutput));
}
catch(Exception e)
{
@@ -488,12 +522,12 @@ public class QJavalinImplementation
{
try
{
- MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance);
- setupSession(context, metaDataRequest);
+ MetaDataInput metaDataInput = new MetaDataInput(qInstance);
+ setupSession(context, metaDataInput);
MetaDataAction metaDataAction = new MetaDataAction();
- MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest);
+ MetaDataOutput metaDataOutput = metaDataAction.execute(metaDataInput);
- context.result(JsonUtils.toJson(metaDataResult));
+ context.result(JsonUtils.toJson(metaDataOutput));
}
catch(Exception e)
{
@@ -510,13 +544,13 @@ public class QJavalinImplementation
{
try
{
- TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance);
- setupSession(context, tableMetaDataRequest);
- tableMetaDataRequest.setTableName(context.pathParam("table"));
+ TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(qInstance);
+ setupSession(context, tableMetaDataInput);
+ tableMetaDataInput.setTableName(context.pathParam("table"));
TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
- TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest);
+ TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput);
- context.result(JsonUtils.toJson(tableMetaDataResult));
+ context.result(JsonUtils.toJson(tableMetaDataOutput));
}
catch(Exception e)
{
@@ -533,13 +567,153 @@ public class QJavalinImplementation
{
try
{
- ProcessMetaDataRequest processMetaDataRequest = new ProcessMetaDataRequest(qInstance);
- setupSession(context, processMetaDataRequest);
- processMetaDataRequest.setProcessName(context.pathParam("processName"));
+ ProcessMetaDataInput processMetaDataInput = new ProcessMetaDataInput(qInstance);
+ setupSession(context, processMetaDataInput);
+ processMetaDataInput.setProcessName(context.pathParam("processName"));
ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction();
- ProcessMetaDataResult processMetaDataResult = processMetaDataAction.execute(processMetaDataRequest);
+ ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput);
- context.result(JsonUtils.toJson(processMetaDataResult));
+ context.result(JsonUtils.toJson(processMetaDataOutput));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void dataExportWithFilename(Context context)
+ {
+ String filename = context.pathParam("filename");
+ dataExport(context, Optional.of(filename));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void dataExportWithoutFilename(Context context)
+ {
+ dataExport(context, Optional.empty());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void dataExport(Context context, Optional optionalFilename)
+ {
+ try
+ {
+ //////////////////////////////////////////
+ // read params from the request context //
+ //////////////////////////////////////////
+ String tableName = context.pathParam("table");
+ String format = context.queryParam("format");
+ String filter = context.queryParam("filter");
+ Integer limit = integerQueryParam(context, "limit");
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // if a format query param wasn't given, then try to get file extension from file name //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ if(!StringUtils.hasContent(format) && optionalFilename.isPresent() && StringUtils.hasContent(optionalFilename.get()))
+ {
+ String filename = optionalFilename.get();
+ if(filename.contains("."))
+ {
+ format = filename.substring(filename.lastIndexOf(".") + 1);
+ }
+ }
+
+ ReportFormat reportFormat;
+ try
+ {
+ reportFormat = ReportFormat.fromString(format);
+ }
+ catch(QUserFacingException e)
+ {
+ handleException(HttpStatus.Code.BAD_REQUEST, context, e);
+ return;
+ }
+
+ String filename = optionalFilename.orElse(tableName + "." + reportFormat.toString().toLowerCase(Locale.ROOT));
+
+ /////////////////////////////////////////////
+ // set up the report action's input object //
+ /////////////////////////////////////////////
+ ReportInput reportInput = new ReportInput(qInstance);
+ setupSession(context, reportInput);
+ reportInput.setTableName(tableName);
+ reportInput.setReportFormat(reportFormat);
+ reportInput.setFilename(filename);
+ reportInput.setLimit(limit);
+
+ String fields = stringQueryParam(context, "fields");
+ if(StringUtils.hasContent(fields))
+ {
+ reportInput.setFieldNames(List.of(fields.split(",")));
+ }
+
+ if(filter != null)
+ {
+ reportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ // set up the I/O pipe streams. //
+ // Critically, we must NOT open the outputStream in a try-with-resources. The thread that writes to //
+ // the stream must close it when it's done writing. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ PipedOutputStream pipedOutputStream = new PipedOutputStream();
+ PipedInputStream pipedInputStream = new PipedInputStream();
+ pipedOutputStream.connect(pipedInputStream);
+ reportInput.setReportOutputStream(pipedOutputStream);
+
+ ReportAction reportAction = new ReportAction();
+ reportAction.preExecute(reportInput);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // start the async job. //
+ // Critically, this must happen before the pipedInputStream is passed to the javalin result method //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ new AsyncJobManager().startJob("Javalin>ReportAction", (o) ->
+ {
+ try
+ {
+ reportAction.execute(reportInput);
+ return (true);
+ }
+ catch(Exception e)
+ {
+ pipedOutputStream.write(("Error generating report: " + e.getMessage()).getBytes());
+ pipedOutputStream.close();
+ return (false);
+ }
+ });
+
+ ////////////////////////////////////////////
+ // set the response content type & stream //
+ ////////////////////////////////////////////
+ context.contentType(reportFormat.getMimeType());
+ context.header("Content-Disposition", "filename=" + filename);
+ context.result(pipedInputStream);
+
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // we'd like to check to see if the job failed, and if so, to give the user an error... //
+ // but if we "block" here, then piped streams seem to never flush, so we deadlock things. //
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // AsyncJobStatus asyncJobStatus = asyncJobManager.waitForJob(jobUUID);
+ // if(asyncJobStatus.getState().equals(AsyncJobState.ERROR))
+ // {
+ // System.out.println("Well, here we are...");
+ // throw (new QUserFacingException("Error running report: " + asyncJobStatus.getCaughtException().getMessage()));
+ // }
}
catch(Exception e)
{
@@ -553,27 +727,46 @@ 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)
{
- context.status(HttpStatus.NOT_FOUND_404)
- .result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
+ int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND).getCode();
+ context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
}
else
{
LOG.info("User-facing exception", e);
- context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
- .result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
+ int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode();
+ context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
}
}
else
{
+ if(e instanceof QAuthenticationException)
+ {
+ context.status(HttpStatus.UNAUTHORIZED_401).result("{\"error\":\"" + e.getMessage() + "\"}");
+ return;
+ }
+
+ ////////////////////////////////
+ // default exception handling //
+ ////////////////////////////////
LOG.warn("Exception in javalin request", e);
- context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
- .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}");
+ int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode();
+ context.status(code).result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}");
}
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
index aaac1a36..08175cdb 100644
--- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java
@@ -33,32 +33,37 @@ import java.util.Objects;
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.actions.processes.QProcessCallback;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
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.actions.processes.QUploadedFile;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.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.metadata.fields.QFieldMetaData;
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.state.StateType;
+import com.kingsrook.qqq.backend.core.state.TempFileStateProvider;
+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 io.javalin.http.UploadedFile;
import org.apache.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -87,15 +92,15 @@ public class QJavalinProcessHandler
{
path("/processes", () ->
{
- path("/:processName", () ->
+ path("/{processName}", () ->
{
get("/init", QJavalinProcessHandler::processInit);
post("/init", QJavalinProcessHandler::processInit);
- path("/:processUUID", () ->
+ path("/{processUUID}", () ->
{
- post("/step/:step", QJavalinProcessHandler::processStep);
- get("/status/:jobUUID", QJavalinProcessHandler::processStatus);
+ post("/step/{step}", QJavalinProcessHandler::processStep);
+ get("/status/{jobUUID}", QJavalinProcessHandler::processStatus);
get("/records", QJavalinProcessHandler::processRecords);
});
});
@@ -135,31 +140,31 @@ public class QJavalinProcessHandler
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);
+ RunProcessInput runProcessInput = new RunProcessInput(QJavalinImplementation.qInstance);
+ QJavalinImplementation.setupSession(context, runProcessInput);
+ runProcessInput.setProcessName(processName);
+ runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
+ runProcessInput.setProcessUUID(processUUID);
+ runProcessInput.setStartAfterStep(startAfterStep);
+ populateRunProcessRequestWithValuesFromContext(context, runProcessInput);
////////////////////////////////////////
// run the process as an async action //
////////////////////////////////////////
Integer timeout = getTimeoutMillis(context);
- RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) ->
+ RunProcessOutput runProcessOutput = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) ->
{
- runProcessRequest.setAsyncJobCallback(callback);
- return (new RunProcessAction().execute(runProcessRequest));
+ runProcessInput.setAsyncJobCallback(callback);
+ return (new RunProcessAction().execute(runProcessInput));
});
- LOG.info("Process result error? " + runProcessResult.getException());
- for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields())
+ LOG.info("Process result error? " + runProcessOutput.getException());
+ for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessInput.getProcessName()).getOutputFields())
{
- LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName()));
+ LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessOutput.getValues().get(outputField.getName()));
}
- serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
+ serializeRunProcessResultForCaller(resultForCaller, runProcessOutput);
}
catch(JobGoingAsyncException jgae)
{
@@ -185,17 +190,17 @@ public class QJavalinProcessHandler
** Whether a step finished synchronously or asynchronously, return its data
** to the caller the same way.
*******************************************************************************/
- private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult)
+ private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessOutput runProcessOutput)
{
- if(runProcessResult.getException().isPresent())
+ if(runProcessOutput.getException().isPresent())
{
////////////////////////////////////////////////////////////////
// per code coverage, this path may never actually get hit... //
////////////////////////////////////////////////////////////////
- serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get());
+ serializeRunProcessExceptionForCaller(resultForCaller, runProcessOutput.getException().get());
}
- resultForCaller.put("values", runProcessResult.getValues());
- runProcessResult.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
+ resultForCaller.put("values", runProcessOutput.getValues());
+ runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
}
@@ -216,7 +221,7 @@ public class QJavalinProcessHandler
{
Throwable rootException = ExceptionUtils.getRootException(exception);
LOG.warn("Uncaught Exception in process", exception);
- resultForCaller.put("error", "Original error message: " + rootException.getMessage());
+ resultForCaller.put("error", "Error message: " + rootException.getMessage());
}
}
@@ -224,25 +229,59 @@ public class QJavalinProcessHandler
/*******************************************************************************
** 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??
+ ** todo - make query params have a "field-" type of prefix??
**
*******************************************************************************/
- private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) throws IOException
+ private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessInput runProcessInput) throws IOException
{
+ //////////////////////////
+ // process query string //
+ //////////////////////////
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));
+ runProcessInput.addValue(fieldName, values.get(0));
}
}
- QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessRequest);
+ ////////////////////////////
+ // process form/post body //
+ ////////////////////////////
+ for(Map.Entry> formParam : context.formParamMap().entrySet())
+ {
+ String fieldName = formParam.getKey();
+ List values = formParam.getValue();
+ if(CollectionUtils.nullSafeHasContents(values))
+ {
+ runProcessInput.addValue(fieldName, values.get(0));
+ }
+ }
+
+ ////////////////////////////
+ // process uploaded files //
+ ////////////////////////////
+ for(UploadedFile uploadedFile : context.uploadedFiles())
+ {
+ QUploadedFile qUploadedFile = new QUploadedFile();
+ qUploadedFile.setBytes(uploadedFile.getContent().readAllBytes());
+ qUploadedFile.setFilename(uploadedFile.getFilename());
+
+ UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE);
+ TempFileStateProvider.getInstance().put(key, qUploadedFile);
+ LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key);
+ runProcessInput.addValue(QUploadedFile.DEFAULT_UPLOADED_FILE_FIELD_NAME, key);
+ }
+
+ /////////////////////////////////////////////////////////////
+ // deal with params that specify an initial-records filter //
+ /////////////////////////////////////////////////////////////
+ QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessInput);
if(initialRecordsFilter != null)
{
- runProcessRequest.setCallback(new QProcessCallback()
+ runProcessInput.setCallback(new QProcessCallback()
{
@Override
public QQueryFilter getQueryFilter()
@@ -266,10 +305,10 @@ public class QJavalinProcessHandler
/*******************************************************************************
**
*******************************************************************************/
- private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessRequest runProcessRequest) throws IOException
+ private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessInput runProcessInput) throws IOException
{
- QInstance instance = runProcessRequest.getInstance();
- QProcessMetaData process = instance.getProcess(runProcessRequest.getProcessName());
+ QInstance instance = runProcessInput.getInstance();
+ QProcessMetaData process = instance.getProcess(runProcessInput.getProcessName());
QTableMetaData table = instance.getTable(process.getTableName());
if(table == null)
@@ -331,11 +370,12 @@ public class QJavalinProcessHandler
*******************************************************************************/
public static void processStatus(Context context)
{
- Map resultForCaller = new HashMap<>();
-
String processUUID = context.pathParam("processUUID");
String jobUUID = context.pathParam("jobUUID");
+ Map resultForCaller = new HashMap<>();
+ resultForCaller.put("processUUID", processUUID);
+
LOG.info("Request for status of process " + processUUID + ", job " + jobUUID);
Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
if(optionalJobStatus.isEmpty())
@@ -358,8 +398,8 @@ public class QJavalinProcessHandler
Optional processState = RunProcessAction.getState(processUUID);
if(processState.isPresent())
{
- RunProcessResult runProcessResult = new RunProcessResult(processState.get());
- serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
+ RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
+ serializeRunProcessResultForCaller(resultForCaller, runProcessOutput);
}
else
{
@@ -412,6 +452,7 @@ public class QJavalinProcessHandler
Map resultForCaller = new HashMap<>();
List recordPage = CollectionUtils.safelyGetPage(records, skip, limit);
resultForCaller.put("records", recordPage);
+ resultForCaller.put("totalRecords", records.size());
context.result(JsonUtils.toJson(resultForCaller));
}
catch(Exception e)
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 459ff2bb..a7351f52 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -27,6 +27,7 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
@@ -34,6 +35,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -52,8 +54,6 @@ class QJavalinImplementationTest extends QJavalinTestBase
{
-
-
/*******************************************************************************
** test the top-level meta-data endpoint
**
@@ -269,7 +269,7 @@ class QJavalinImplementationTest extends QJavalinTestBase
@Test
public void test_dataQueryWithFilter()
{
- String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}";
+ String filterJson = getFirstNameEqualsTimFilterJSON();
HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString();
assertEquals(200, response.getStatus());
@@ -284,6 +284,17 @@ class QJavalinImplementationTest extends QJavalinTestBase
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getFirstNameEqualsTimFilterJSON()
+ {
+ return """
+ {"criteria":[{"fieldName":"firstName","operator":"EQUALS","values":["Tim"]}]}""";
+ }
+
+
+
/*******************************************************************************
** test an insert
**
@@ -362,8 +373,7 @@ class QJavalinImplementationTest extends QJavalinTestBase
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
- assertEquals(1, jsonObject.getJSONArray("records").length());
- assertEquals(3, jsonObject.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
+ assertEquals(1, jsonObject.getInt("deletedRecordCount"));
TestUtils.runTestSql("SELECT id FROM person", (rs -> {
int rowsFound = 0;
while(rs.next())
@@ -375,4 +385,89 @@ class QJavalinImplementationTest extends QJavalinTestBase
}));
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExportCsvPerFileName()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/MyPersonExport.csv").asString();
+ assertEquals(200, response.getStatus());
+ assertEquals("text/csv", response.getHeaders().get("Content-Type").get(0));
+ assertEquals("filename=MyPersonExport.csv", response.getHeaders().get("Content-Disposition").get(0));
+ String[] csvLines = response.getBody().split("\n");
+ assertEquals(6, csvLines.length);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExportNoFormat()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/").asString();
+ assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
+ assertThat(response.getBody()).contains("Report format was not specified");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExportExcelPerFormatQueryParam()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/?format=xlsx").asString();
+ assertEquals(200, response.getStatus());
+ assertEquals(ReportFormat.XLSX.getMimeType(), response.getHeaders().get("Content-Type").get(0));
+ assertEquals("filename=person.xlsx", response.getHeaders().get("Content-Disposition").get(0));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExportFilterQueryParam()
+ {
+ String filterJson = getFirstNameEqualsTimFilterJSON();
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/Favorite People.csv?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString();
+ assertEquals("filename=Favorite People.csv", response.getHeaders().get("Content-Disposition").get(0));
+ String[] csvLines = response.getBody().split("\n");
+ assertEquals(2, csvLines.length);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExportFieldsQueryParam()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString();
+ String[] csvLines = response.getBody().split("\n");
+ assertEquals("""
+ "Id","Birth Date\"""", csvLines[0]);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExportSupportedFormat()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/?format=docx").asString();
+ assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
+ assertThat(response.getBody()).contains("Unsupported report format");
+ }
+
}
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
index 91cc0479..3effbf77 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java
@@ -26,9 +26,9 @@ 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.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.utils.JsonUtils;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
index 133d1c07..818c2b53 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -27,18 +27,19 @@ import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
-import com.kingsrook.qqq.backend.core.interfaces.BackendStep;
-import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep;
-import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepRequest;
-import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepResult;
-import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
-import com.kingsrook.qqq.backend.core.model.metadata.QCodeType;
-import com.kingsrook.qqq.backend.core.model.metadata.QCodeUsage;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
+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.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
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.tables.QTableMetaData;
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.QFunctionInputMetaData;
@@ -134,7 +135,7 @@ public class TestUtils
{
return new QAuthenticationMetaData()
.withName("mock")
- .withType("mock");
+ .withType(QAuthenticationType.MOCK);
}
@@ -311,11 +312,11 @@ public class TestUtils
**
******************************************************************************/
@Override
- public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
try
{
- Thread.sleep(runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS));
+ Thread.sleep(runBackendStepInput.getValueInteger(FIELD_SLEEP_MILLIS));
}
catch(InterruptedException e)
{
@@ -357,12 +358,12 @@ public class TestUtils
**
******************************************************************************/
@Override
- public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
int sleepMillis;
try
{
- sleepMillis = runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS);
+ sleepMillis = runBackendStepInput.getValueInteger(FIELD_SLEEP_MILLIS);
}
catch(QValueException qve)
{