mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 21:20:45 +00:00
Moving qqq-middleware-javalin into its own subdir
This commit is contained in:
@ -0,0 +1,808 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.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 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.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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.apibuilder.EndpointGroup;
|
||||
import io.javalin.http.Context;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import static io.javalin.apibuilder.ApiBuilder.delete;
|
||||
import static io.javalin.apibuilder.ApiBuilder.get;
|
||||
import static io.javalin.apibuilder.ApiBuilder.patch;
|
||||
import static io.javalin.apibuilder.ApiBuilder.path;
|
||||
import static io.javalin.apibuilder.ApiBuilder.post;
|
||||
import static io.javalin.apibuilder.ApiBuilder.put;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QQQ Javalin implementation. Given a QInstance, defines all routes needed
|
||||
** to respond to http requests and route down into the qqq backend.
|
||||
**
|
||||
*******************************************************************************/
|
||||
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;
|
||||
|
||||
private static int DEFAULT_PORT = 8001;
|
||||
|
||||
private static Javalin service;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void main(String[] args)
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
// todo - parse args to look up metaData and prime instance
|
||||
// qInstance.addBackend(QMetaDataProvider.getQBackend());
|
||||
|
||||
new QJavalinImplementation(qInstance).startJavalinServer(DEFAULT_PORT);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QJavalinImplementation(QInstance qInstance)
|
||||
{
|
||||
QJavalinImplementation.qInstance = qInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QJavalinImplementation(String qInstanceFilePath) throws IOException
|
||||
{
|
||||
LOG.info("Loading qInstance from file (assuming json): " + qInstanceFilePath);
|
||||
String qInstanceJson = FileUtils.readFileToString(new File(qInstanceFilePath));
|
||||
QJavalinImplementation.qInstance = new QInstanceAdapter().jsonToQInstanceIncludingBackends(qInstanceJson);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void startJavalinServer(int port)
|
||||
{
|
||||
// todo port from arg
|
||||
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
|
||||
service = Javalin.create().start(port);
|
||||
service.routes(getRoutes());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void stopJavalinServer()
|
||||
{
|
||||
service.stop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setDefaultPort(int port)
|
||||
{
|
||||
QJavalinImplementation.DEFAULT_PORT = port;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public EndpointGroup getRoutes()
|
||||
{
|
||||
return (() ->
|
||||
{
|
||||
/////////////////////
|
||||
// metadata routes //
|
||||
/////////////////////
|
||||
path("/metaData", () ->
|
||||
{
|
||||
get("/", QJavalinImplementation::metaData);
|
||||
path("/table/{table}", () ->
|
||||
{
|
||||
get("", QJavalinImplementation::tableMetaData);
|
||||
});
|
||||
path("/process/{processName}", () ->
|
||||
{
|
||||
get("", QJavalinImplementation::processMetaData);
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////
|
||||
// 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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException
|
||||
{
|
||||
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
|
||||
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData());
|
||||
|
||||
try
|
||||
{
|
||||
Map<String, String> 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(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void dataDelete(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
String table = context.pathParam("table");
|
||||
List<Serializable> primaryKeys = new ArrayList<>();
|
||||
primaryKeys.add(context.pathParam("primaryKey"));
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput(qInstance);
|
||||
setupSession(context, deleteInput);
|
||||
deleteInput.setTableName(table);
|
||||
deleteInput.setPrimaryKeys(primaryKeys);
|
||||
|
||||
DeleteAction deleteAction = new DeleteAction();
|
||||
DeleteOutput deleteResult = deleteAction.execute(deleteInput);
|
||||
|
||||
context.result(JsonUtils.toJson(deleteResult));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void dataUpdate(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
String table = context.pathParam("table");
|
||||
List<QRecord> recordList = new ArrayList<>();
|
||||
QRecord record = new QRecord();
|
||||
record.setTableName(table);
|
||||
recordList.add(record);
|
||||
|
||||
Map<?, ?> map = context.bodyAsClass(Map.class);
|
||||
for(Map.Entry<?, ?> entry : map.entrySet())
|
||||
{
|
||||
if(StringUtils.hasContent(String.valueOf(entry.getValue())))
|
||||
{
|
||||
record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
QTableMetaData tableMetaData = qInstance.getTable(table);
|
||||
|
||||
record.setValue(tableMetaData.getPrimaryKeyField(), context.pathParam("primaryKey"));
|
||||
|
||||
UpdateInput updateInput = new UpdateInput(qInstance);
|
||||
setupSession(context, updateInput);
|
||||
updateInput.setTableName(table);
|
||||
updateInput.setRecords(recordList);
|
||||
|
||||
UpdateAction updateAction = new UpdateAction();
|
||||
UpdateOutput updateResult = updateAction.execute(updateInput);
|
||||
|
||||
context.result(JsonUtils.toJson(updateResult));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void dataInsert(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
String table = context.pathParam("table");
|
||||
List<QRecord> recordList = new ArrayList<>();
|
||||
QRecord record = new QRecord();
|
||||
record.setTableName(table);
|
||||
recordList.add(record);
|
||||
|
||||
Map<?, ?> map = context.bodyAsClass(Map.class);
|
||||
for(Map.Entry<?, ?> entry : map.entrySet())
|
||||
{
|
||||
if(StringUtils.hasContent(String.valueOf(entry.getValue())))
|
||||
{
|
||||
record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
InsertInput insertInput = new InsertInput(qInstance);
|
||||
setupSession(context, insertInput);
|
||||
insertInput.setTableName(table);
|
||||
insertInput.setRecords(recordList);
|
||||
|
||||
InsertAction insertAction = new InsertAction();
|
||||
InsertOutput insertOutput = insertAction.execute(insertInput);
|
||||
|
||||
context.result(JsonUtils.toJson(insertOutput));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
********************************************************************************/
|
||||
private static void dataGet(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
String tableName = context.pathParam("table");
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
String primaryKey = context.pathParam("primaryKey");
|
||||
QueryInput queryInput = new QueryInput(qInstance);
|
||||
|
||||
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)
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// setup a filter for the primaryKey = the path-pram //
|
||||
///////////////////////////////////////////////////////
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria()
|
||||
.withFieldName(table.getPrimaryKeyField())
|
||||
.withOperator(QCriteriaOperator.EQUALS)
|
||||
.withValues(List.of(primaryKey))));
|
||||
|
||||
QueryAction queryAction = new QueryAction();
|
||||
QueryOutput queryOutput = queryAction.execute(queryInput);
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// throw a not found error if the record isn't found //
|
||||
///////////////////////////////////////////////////////
|
||||
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(queryOutput.getRecords().get(0)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* Filter parameter is a serialized QQueryFilter object, that is to say:
|
||||
* <pre>
|
||||
* filter=
|
||||
* {"criteria":[
|
||||
* {"fieldName":"id","operator":"EQUALS","values":[1]},
|
||||
* {"fieldName":"name","operator":"IN","values":["Darin","James"]}
|
||||
* ]
|
||||
* }
|
||||
* </pre>
|
||||
*******************************************************************************/
|
||||
static void dataCount(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
CountInput countInput = new CountInput(qInstance);
|
||||
setupSession(context, countInput);
|
||||
countInput.setTableName(context.pathParam("table"));
|
||||
|
||||
String filter = stringQueryParam(context, "filter");
|
||||
if(filter != null)
|
||||
{
|
||||
countInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
|
||||
}
|
||||
|
||||
CountAction countAction = new CountAction();
|
||||
CountOutput countOutput = countAction.execute(countInput);
|
||||
|
||||
context.result(JsonUtils.toJson(countOutput));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* Filter parameter is a serialized QQueryFilter object, that is to say:
|
||||
* <pre>
|
||||
* filter=
|
||||
* {"criteria":[
|
||||
* {"fieldName":"id","operator":"EQUALS","values":[1]},
|
||||
* {"fieldName":"name","operator":"IN","values":["Darin","James"]}
|
||||
* ],
|
||||
* "orderBys":[
|
||||
* {"fieldName":"age","isAscending":true}
|
||||
* ]}
|
||||
* </pre>
|
||||
*******************************************************************************/
|
||||
static void dataQuery(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
queryInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
|
||||
}
|
||||
|
||||
QueryAction queryAction = new QueryAction();
|
||||
QueryOutput queryOutput = queryAction.execute(queryInput);
|
||||
|
||||
context.result(JsonUtils.toJson(queryOutput));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void metaData(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
MetaDataInput metaDataInput = new MetaDataInput(qInstance);
|
||||
setupSession(context, metaDataInput);
|
||||
MetaDataAction metaDataAction = new MetaDataAction();
|
||||
MetaDataOutput metaDataOutput = metaDataAction.execute(metaDataInput);
|
||||
|
||||
context.result(JsonUtils.toJson(metaDataOutput));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void tableMetaData(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(qInstance);
|
||||
setupSession(context, tableMetaDataInput);
|
||||
tableMetaDataInput.setTableName(context.pathParam("table"));
|
||||
TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
|
||||
TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput);
|
||||
|
||||
context.result(JsonUtils.toJson(tableMetaDataOutput));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processMetaData(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessMetaDataInput processMetaDataInput = new ProcessMetaDataInput(qInstance);
|
||||
setupSession(context, processMetaDataInput);
|
||||
processMetaDataInput.setProcessName(context.pathParam("processName"));
|
||||
ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction();
|
||||
ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput);
|
||||
|
||||
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<String> 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)
|
||||
{
|
||||
handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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)
|
||||
{
|
||||
int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND).getCode();
|
||||
context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("User-facing exception", e);
|
||||
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);
|
||||
int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode();
|
||||
context.status(code).result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Returns Integer if context has a valid int query parameter by the given name,
|
||||
** Returns null if no param (or empty value).
|
||||
** Throws QValueException for malformed numbers.
|
||||
*******************************************************************************/
|
||||
public static Integer integerQueryParam(Context context, String name) throws QValueException
|
||||
{
|
||||
String value = context.queryParam(name);
|
||||
if(StringUtils.hasContent(value))
|
||||
{
|
||||
return (ValueUtils.getValueAsInteger(value));
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Returns String if context has a valid query parameter by the given name,
|
||||
* Returns null if no param (or empty value).
|
||||
*******************************************************************************/
|
||||
private static String stringQueryParam(Context context, String name)
|
||||
{
|
||||
String value = context.queryParam(name);
|
||||
if(StringUtils.hasContent(value))
|
||||
{
|
||||
return (value);
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,489 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.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.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
|
||||
import com.kingsrook.qqq.backend.core.actions.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.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.QInstance;
|
||||
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;
|
||||
import static io.javalin.apibuilder.ApiBuilder.get;
|
||||
import static io.javalin.apibuilder.ApiBuilder.path;
|
||||
import static io.javalin.apibuilder.ApiBuilder.post;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** methods for handling qqq processes in javalin.
|
||||
*******************************************************************************/
|
||||
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);
|
||||
get("/records", QJavalinProcessHandler::processRecords);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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<String, Object> 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 + "]");
|
||||
|
||||
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);
|
||||
RunProcessOutput runProcessOutput = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) ->
|
||||
{
|
||||
runProcessInput.setAsyncJobCallback(callback);
|
||||
return (new RunProcessAction().execute(runProcessInput));
|
||||
});
|
||||
|
||||
LOG.info("Process result error? " + runProcessOutput.getException());
|
||||
for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessInput.getProcessName()).getOutputFields())
|
||||
{
|
||||
LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessOutput.getValues().get(outputField.getName()));
|
||||
}
|
||||
|
||||
serializeRunProcessResultForCaller(resultForCaller, runProcessOutput);
|
||||
}
|
||||
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<String, Object> resultForCaller, RunProcessOutput runProcessOutput)
|
||||
{
|
||||
if(runProcessOutput.getException().isPresent())
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// per code coverage, this path may never actually get hit... //
|
||||
////////////////////////////////////////////////////////////////
|
||||
serializeRunProcessExceptionForCaller(resultForCaller, runProcessOutput.getException().get());
|
||||
}
|
||||
resultForCaller.put("values", runProcessOutput.getValues());
|
||||
runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void serializeRunProcessExceptionForCaller(Map<String, Object> 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", "Error message: " + rootException.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** take values from query-string params, and put them into the run process request
|
||||
** todo - make query params have a "field-" type of prefix??
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessInput runProcessInput) throws IOException
|
||||
{
|
||||
//////////////////////////
|
||||
// process query string //
|
||||
//////////////////////////
|
||||
for(Map.Entry<String, List<String>> queryParam : context.queryParamMap().entrySet())
|
||||
{
|
||||
String fieldName = queryParam.getKey();
|
||||
List<String> values = queryParam.getValue();
|
||||
if(CollectionUtils.nullSafeHasContents(values))
|
||||
{
|
||||
runProcessInput.addValue(fieldName, values.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// process form/post body //
|
||||
////////////////////////////
|
||||
for(Map.Entry<String, List<String>> formParam : context.formParamMap().entrySet())
|
||||
{
|
||||
String fieldName = formParam.getKey();
|
||||
List<String> 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)
|
||||
{
|
||||
runProcessInput.setCallback(new QProcessCallback()
|
||||
{
|
||||
@Override
|
||||
public QQueryFilter getQueryFilter()
|
||||
{
|
||||
return (initialRecordsFilter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
|
||||
{
|
||||
return (Collections.emptyMap());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessInput runProcessInput) throws IOException
|
||||
{
|
||||
QInstance instance = runProcessInput.getInstance();
|
||||
QProcessMetaData process = instance.getProcess(runProcessInput.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 = paramValue.split(",");
|
||||
return (new QQueryFilter().withCriteria(new QFilterCriteria()
|
||||
.withFieldName(primaryKeyField)
|
||||
.withOperator(QCriteriaOperator.IN)
|
||||
.withValues(Arrays.stream(idStrings).toList())));
|
||||
case "filterJSON":
|
||||
return (JsonUtils.toObject(paramValue, 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");
|
||||
|
||||
Map<String, Object> resultForCaller = new HashMap<>();
|
||||
resultForCaller.put("processUUID", processUUID);
|
||||
|
||||
LOG.info("Request for status of process " + processUUID + ", job " + jobUUID);
|
||||
Optional<AsyncJobStatus> optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
|
||||
if(optionalJobStatus.isEmpty())
|
||||
{
|
||||
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job"));
|
||||
}
|
||||
else
|
||||
{
|
||||
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> processState = RunProcessAction.getState(processUUID);
|
||||
if(processState.isPresent())
|
||||
{
|
||||
RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
|
||||
serializeRunProcessResultForCaller(resultForCaller, runProcessOutput);
|
||||
}
|
||||
else
|
||||
{
|
||||
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID));
|
||||
}
|
||||
}
|
||||
else if(jobStatus.getState().equals(AsyncJobState.ERROR))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(jobStatus.getCaughtException() != null)
|
||||
{
|
||||
serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.result(JsonUtils.toJson(resultForCaller));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processRecords(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
String processUUID = context.pathParam("processUUID");
|
||||
Integer skip = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "skip"), 0);
|
||||
Integer limit = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "limit"), 20);
|
||||
|
||||
// todo - potential optimization - if a future state provider could take advantage of it,
|
||||
// we might pass the skip & limit in to a method that fetch just those 'n' rows from state, rather than the whole thing?
|
||||
Optional<ProcessState> optionalProcessState = RunProcessAction.getState(processUUID);
|
||||
if(optionalProcessState.isEmpty())
|
||||
{
|
||||
throw (new Exception("Could not find process results."));
|
||||
}
|
||||
ProcessState processState = optionalProcessState.get();
|
||||
|
||||
List<QRecord> records = processState.getRecords();
|
||||
if(CollectionUtils.nullSafeIsEmpty(records))
|
||||
{
|
||||
throw (new Exception("No records were found for the process."));
|
||||
}
|
||||
|
||||
Map<String, Object> resultForCaller = new HashMap<>();
|
||||
List<QRecord> recordPage = CollectionUtils.safelyGetPage(records, skip, limit);
|
||||
resultForCaller.put("records", recordPage);
|
||||
resultForCaller.put("totalRecords", records.size());
|
||||
context.result(JsonUtils.toJson(resultForCaller));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
QJavalinImplementation.handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,473 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.javalin;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
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;
|
||||
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;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for the QJavalinImplementation
|
||||
**
|
||||
** based on https://javalin.io/tutorials/testing - starts a javalin instance
|
||||
** and actually makes http requests into it.
|
||||
**
|
||||
*******************************************************************************/
|
||||
class QJavalinImplementationTest extends QJavalinTestBase
|
||||
{
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test the top-level meta-data endpoint
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_metaData()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData").asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("tables"));
|
||||
JSONObject tables = jsonObject.getJSONObject("tables");
|
||||
assertEquals(1, tables.length());
|
||||
JSONObject personTable = tables.getJSONObject("person");
|
||||
assertTrue(personTable.has("name"));
|
||||
assertEquals("person", personTable.getString("name"));
|
||||
assertTrue(personTable.has("label"));
|
||||
assertEquals("Person", personTable.getString("label"));
|
||||
assertFalse(personTable.getBoolean("isHidden"));
|
||||
|
||||
JSONObject processes = jsonObject.getJSONObject("processes");
|
||||
assertTrue(processes.getJSONObject("simpleSleep").getBoolean("isHidden"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test the table-level meta-data endpoint
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableMetaData()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/table/person").asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||
JSONObject table = jsonObject.getJSONObject("table");
|
||||
assertEquals("person", table.getString("name"));
|
||||
assertEquals("Person", table.getString("label"));
|
||||
assertEquals("id", table.getString("primaryKeyField"));
|
||||
JSONObject fields = table.getJSONObject("fields");
|
||||
JSONObject field0 = fields.getJSONObject("id");
|
||||
assertEquals("id", field0.getString("name"));
|
||||
assertEquals("INTEGER", field0.getString("type"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test the table-level meta-data endpoint for a non-real name
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableMetaData_notFound()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/table/notAnActualTable").asString();
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||
String error = jsonObject.getString("error");
|
||||
assertTrue(error.contains("not found"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test the process-level meta-data endpoint
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_processMetaData()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/process/greetInteractive").asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||
JSONObject process = jsonObject.getJSONObject("process");
|
||||
assertEquals("greetInteractive", process.getString("name"));
|
||||
assertEquals("Greet Interactive", process.getString("label"));
|
||||
assertEquals("person", process.getString("tableName"));
|
||||
|
||||
JSONArray frontendSteps = process.getJSONArray("frontendSteps");
|
||||
JSONObject setupStep = frontendSteps.getJSONObject(0);
|
||||
assertEquals("Setup", setupStep.getString("label"));
|
||||
JSONArray setupFields = setupStep.getJSONArray("formFields");
|
||||
assertEquals(2, setupFields.length());
|
||||
assertTrue(setupFields.toList().stream().anyMatch(field -> "greetingPrefix".equals(((Map<?, ?>) field).get("name"))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test the process-level meta-data endpoint for a non-real name
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_processMetaData_notFound()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/process/notAnActualProcess").asString();
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||
String error = jsonObject.getString("error");
|
||||
assertTrue(error.contains("not found"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a table get (single record)
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataGet()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/1").asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("values"));
|
||||
assertEquals("person", jsonObject.getString("tableName"));
|
||||
JSONObject values = jsonObject.getJSONObject("values");
|
||||
assertTrue(values.has("firstName"));
|
||||
assertTrue(values.has("id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a table get (single record) for an id that isn't found
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataGetNotFound()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/98765").asString();
|
||||
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||
String error = jsonObject.getString("error");
|
||||
assertEquals("Could not find Person with Id of 98765", error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a table get (single record) for an id that isn't the expected type
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataGetWrongIdType()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/not-an-integer").asString();
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a table count
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataCount()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/count").asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("count"));
|
||||
int count = jsonObject.getInt("count");
|
||||
assertEquals(5, count);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a table query
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataQuery()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person").asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("records"));
|
||||
JSONArray records = jsonObject.getJSONArray("records");
|
||||
assertEquals(5, records.length());
|
||||
JSONObject record0 = records.getJSONObject(0);
|
||||
assertTrue(record0.has("values"));
|
||||
assertEquals("person", record0.getString("tableName"));
|
||||
JSONObject values0 = record0.getJSONObject("values");
|
||||
assertTrue(values0.has("firstName"));
|
||||
assertTrue(values0.has("id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a table query using an actual filter.
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataQueryWithFilter()
|
||||
{
|
||||
String filterJson = getFirstNameEqualsTimFilterJSON();
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("records"));
|
||||
JSONArray records = jsonObject.getJSONArray("records");
|
||||
assertEquals(1, records.length());
|
||||
JSONObject record0 = records.getJSONObject(0);
|
||||
JSONObject values0 = record0.getJSONObject("values");
|
||||
assertEquals("Tim", values0.getString("firstName"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getFirstNameEqualsTimFilterJSON()
|
||||
{
|
||||
return """
|
||||
{"criteria":[{"fieldName":"firstName","operator":"EQUALS","values":["Tim"]}]}""";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test an insert
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataInsert()
|
||||
{
|
||||
Map<String, Serializable> body = new HashMap<>();
|
||||
body.put("firstName", "Bobby");
|
||||
body.put("lastName", "Hull");
|
||||
body.put("email", "bobby@hull.com");
|
||||
|
||||
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/person")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("records"));
|
||||
JSONArray records = jsonObject.getJSONArray("records");
|
||||
assertEquals(1, records.length());
|
||||
JSONObject record0 = records.getJSONObject(0);
|
||||
assertTrue(record0.has("values"));
|
||||
assertEquals("person", record0.getString("tableName"));
|
||||
JSONObject values0 = record0.getJSONObject("values");
|
||||
assertTrue(values0.has("firstName"));
|
||||
assertEquals("Bobby", values0.getString("firstName"));
|
||||
assertTrue(values0.has("id"));
|
||||
assertEquals(6, values0.getInt("id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test an update
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataUpdate()
|
||||
{
|
||||
Map<String, Serializable> body = new HashMap<>();
|
||||
body.put("firstName", "Free");
|
||||
//? body.put("id", 4);
|
||||
|
||||
HttpResponse<String> response = Unirest.patch(BASE_URL + "/data/person/4")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.asString();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertTrue(jsonObject.has("records"));
|
||||
JSONArray records = jsonObject.getJSONArray("records");
|
||||
assertEquals(1, records.length());
|
||||
JSONObject record0 = records.getJSONObject(0);
|
||||
assertTrue(record0.has("values"));
|
||||
assertEquals("person", record0.getString("tableName"));
|
||||
JSONObject values0 = record0.getJSONObject("values");
|
||||
assertEquals(4, values0.getInt("id"));
|
||||
assertEquals("Free", values0.getString("firstName"));
|
||||
// mmm, whole record isn't loaded. should it be? assertEquals("Samples", values0.getString("lastName"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test a delete
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_dataDelete() throws Exception
|
||||
{
|
||||
HttpResponse<String> response = Unirest.delete(BASE_URL + "/data/person/3").asString();
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertNotNull(jsonObject);
|
||||
assertEquals(1, jsonObject.getInt("deletedRecordCount"));
|
||||
TestUtils.runTestSql("SELECT id FROM person", (rs -> {
|
||||
int rowsFound = 0;
|
||||
while(rs.next())
|
||||
{
|
||||
rowsFound++;
|
||||
assertNotEquals(3, rs.getInt(1));
|
||||
}
|
||||
assertEquals(4, rowsFound);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testExportCsvPerFileName()
|
||||
{
|
||||
HttpResponse<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,479 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.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.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;
|
||||
import org.json.JSONArray;
|
||||
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<String> 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<String> 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<String> 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);
|
||||
String processUUID = jsonObject.getString("processUUID");
|
||||
|
||||
getProcessRecords(processUUID, 2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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<String> 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);
|
||||
String processUUID = jsonObject.getString("processUUID");
|
||||
|
||||
getProcessRecords(processUUID, 3);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private JSONObject getProcessRecords(String processUUID, int expectedNoOfRecords)
|
||||
{
|
||||
return (getProcessRecords(processUUID, expectedNoOfRecords, 0, 20));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private JSONObject getProcessRecords(String processUUID, int expectedNoOfRecords, int skip, int limit)
|
||||
{
|
||||
HttpResponse<String> response;
|
||||
JSONObject jsonObject;
|
||||
response = Unirest.get(BASE_URL + "/processes/greet/" + processUUID + "/records?skip=" + skip + "&limit=" + limit).asString();
|
||||
jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
assertNotNull(jsonObject);
|
||||
|
||||
if(expectedNoOfRecords == 0)
|
||||
{
|
||||
assertFalse(jsonObject.has("records"));
|
||||
}
|
||||
else
|
||||
{
|
||||
assertTrue(jsonObject.has("records"));
|
||||
JSONArray records = jsonObject.getJSONArray("records");
|
||||
assertEquals(expectedNoOfRecords, records.length());
|
||||
}
|
||||
return (jsonObject);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running a process with field values on the query string
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_processGreetInitWithQueryValues()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.post(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<String> 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<String> 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<String> response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT)
|
||||
.header("Content-Type", "application/json").asString();
|
||||
|
||||
JSONObject jsonObject = assertProcessStepCompleteResponse(response);
|
||||
String processUUID = jsonObject.getString("processUUID");
|
||||
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<String> 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<String> 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<String> 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 (or init) has gone async, expect what the
|
||||
** response should look like
|
||||
*******************************************************************************/
|
||||
private JSONObject assertProcessStepWentAsyncResponse(HttpResponse<String> 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<String> 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<String> 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<String> 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test getting records back from a process
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_processRecords()
|
||||
{
|
||||
HttpResponse<String> 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);
|
||||
String processUUID = jsonObject.getString("processUUID");
|
||||
|
||||
jsonObject = getProcessRecords(processUUID, 2);
|
||||
JSONArray records = jsonObject.getJSONArray("records");
|
||||
JSONObject record0 = records.getJSONObject(0);
|
||||
JSONObject values = record0.getJSONObject("values");
|
||||
assertTrue(values.has("id"));
|
||||
assertTrue(values.has("firstName"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test getting records back from a process with skip & Limit
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_processRecordsSkipAndLimit()
|
||||
{
|
||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=1,2,3,4,5").asString();
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||
String processUUID = jsonObject.getString("processUUID");
|
||||
|
||||
getProcessRecords(processUUID, 5);
|
||||
getProcessRecords(processUUID, 1, 4, 5);
|
||||
getProcessRecords(processUUID, 0, 5, 5);
|
||||
}
|
||||
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,403 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.javalin;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
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.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.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;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility methods for unit tests.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TestUtils
|
||||
{
|
||||
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
|
||||
public static final String PROCESS_NAME_SIMPLE_SLEEP = "simpleSleep";
|
||||
public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow";
|
||||
public static final String PROCESS_NAME_SLEEP_INTERACTIVE = "sleepInteractive";
|
||||
|
||||
public static final String STEP_NAME_SLEEPER = "sleeper";
|
||||
public static final String STEP_NAME_THROWER = "thrower";
|
||||
|
||||
public static final String SCREEN_0 = "screen0";
|
||||
public static final String SCREEN_1 = "screen1";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Prime a test database (e.g., h2, in-memory)
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void primeTestDatabase() throws Exception
|
||||
{
|
||||
ConnectionManager connectionManager = new ConnectionManager();
|
||||
Connection connection = connectionManager.getConnection(TestUtils.defineBackend());
|
||||
InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
|
||||
assertNotNull(primeTestDatabaseSqlStream);
|
||||
List<String> lines = (List<String>) IOUtils.readLines(primeTestDatabaseSqlStream);
|
||||
lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList();
|
||||
String joinedSQL = String.join("\n", lines);
|
||||
for(String sql : joinedSQL.split(";"))
|
||||
{
|
||||
QueryManager.executeUpdate(connection, sql);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run an SQL Query in the test database
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
|
||||
{
|
||||
ConnectionManager connectionManager = new ConnectionManager();
|
||||
Connection connection = connectionManager.getConnection(defineBackend());
|
||||
QueryManager.executeStatement(connection, sql, resultSetProcessor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define the q-instance for testing (h2 rdbms and 'person' table)
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QInstance defineInstance()
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.setAuthentication(defineAuthentication());
|
||||
qInstance.addBackend(defineBackend());
|
||||
qInstance.addTable(defineTablePerson());
|
||||
qInstance.addProcess(defineProcessGreetPeople());
|
||||
qInstance.addProcess(defineProcessGreetPeopleInteractive());
|
||||
qInstance.addProcess(defineProcessSimpleSleep());
|
||||
qInstance.addProcess(defineProcessScreenThenSleep());
|
||||
qInstance.addProcess(defineProcessSimpleThrow());
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define the authentication used in standard tests - using 'mock' type.
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QAuthenticationMetaData defineAuthentication()
|
||||
{
|
||||
return new QAuthenticationMetaData()
|
||||
.withName("mock")
|
||||
.withType(QAuthenticationType.MOCK);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define the h2 rdbms backend
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static RDBMSBackendMetaData defineBackend()
|
||||
{
|
||||
RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData()
|
||||
.withVendor("h2")
|
||||
.withHostName("mem")
|
||||
.withDatabaseName("test_database")
|
||||
.withUsername("sa")
|
||||
.withPassword("");
|
||||
rdbmsBackendMetaData.setName("default");
|
||||
return (rdbmsBackendMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define the person table
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QTableMetaData defineTablePerson()
|
||||
{
|
||||
return new QTableMetaData()
|
||||
.withName("person")
|
||||
.withLabel("Person")
|
||||
.withBackendName(defineBackend().getName())
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date"))
|
||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date"))
|
||||
.withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name"))
|
||||
.withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name"))
|
||||
.withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
|
||||
.withField(new QFieldMetaData("email", QFieldType.STRING));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define the 'greet people' process
|
||||
*******************************************************************************/
|
||||
private static QProcessMetaData defineProcessGreetPeople()
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName("greet")
|
||||
.withTableName("person")
|
||||
.addStep(new QBackendStepMetaData()
|
||||
.withName("prepare")
|
||||
.withCode(new QCodeReference()
|
||||
.withName(MockBackendStep.class.getName())
|
||||
.withCodeType(QCodeType.JAVA)
|
||||
.withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context?
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.withRecordListMetaData(new QRecordListMetaData().withTableName("person"))
|
||||
.withFieldList(List.of(
|
||||
new QFieldMetaData("greetingPrefix", QFieldType.STRING),
|
||||
new QFieldMetaData("greetingSuffix", QFieldType.STRING)
|
||||
)))
|
||||
.withOutputMetaData(new QFunctionOutputMetaData()
|
||||
.withRecordListMetaData(new QRecordListMetaData()
|
||||
.withTableName("person")
|
||||
.addField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
||||
)
|
||||
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define an interactive version of the 'greet people' process
|
||||
*******************************************************************************/
|
||||
private static QProcessMetaData defineProcessGreetPeopleInteractive()
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE)
|
||||
.withTableName("person")
|
||||
|
||||
.addStep(new QFrontendStepMetaData()
|
||||
.withName("setup")
|
||||
.withFormField(new QFieldMetaData("greetingPrefix", QFieldType.STRING))
|
||||
.withFormField(new QFieldMetaData("greetingSuffix", QFieldType.STRING))
|
||||
)
|
||||
|
||||
.addStep(new QBackendStepMetaData()
|
||||
.withName("doWork")
|
||||
.withCode(new QCodeReference()
|
||||
.withName(MockBackendStep.class.getName())
|
||||
.withCodeType(QCodeType.JAVA)
|
||||
.withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context?
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.withRecordListMetaData(new QRecordListMetaData().withTableName("person"))
|
||||
.withFieldList(List.of(
|
||||
new QFieldMetaData("greetingPrefix", QFieldType.STRING),
|
||||
new QFieldMetaData("greetingSuffix", QFieldType.STRING)
|
||||
)))
|
||||
.withOutputMetaData(new QFunctionOutputMetaData()
|
||||
.withRecordListMetaData(new QRecordListMetaData()
|
||||
.withTableName("person")
|
||||
.addField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
||||
)
|
||||
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
||||
)
|
||||
|
||||
.addStep(new QFrontendStepMetaData()
|
||||
.withName("results")
|
||||
.withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define a process with just one step that sleeps
|
||||
*******************************************************************************/
|
||||
private static QProcessMetaData defineProcessSimpleSleep()
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(PROCESS_NAME_SIMPLE_SLEEP)
|
||||
.withIsHidden(true)
|
||||
.addStep(SleeperStep.getMetaData());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define a process with a screen, then a sleep step
|
||||
*******************************************************************************/
|
||||
private static QProcessMetaData defineProcessScreenThenSleep()
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(PROCESS_NAME_SLEEP_INTERACTIVE)
|
||||
.addStep(new QFrontendStepMetaData()
|
||||
.withName(SCREEN_0)
|
||||
.withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)))
|
||||
.addStep(SleeperStep.getMetaData())
|
||||
.addStep(new QFrontendStepMetaData()
|
||||
.withName(SCREEN_1)
|
||||
.withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Define a process with just one step that sleeps and then throws
|
||||
*******************************************************************************/
|
||||
private static QProcessMetaData defineProcessSimpleThrow()
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(PROCESS_NAME_SIMPLE_THROW)
|
||||
.addStep(ThrowerStep.getMetaData());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Testing backend step - just sleeps however long you ask it to (or, throws if
|
||||
** you don't provide a number of seconds to sleep).
|
||||
*******************************************************************************/
|
||||
public static class SleeperStep implements BackendStep
|
||||
{
|
||||
public static final String FIELD_SLEEP_MILLIS = "sleepMillis";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute the backend step - using the request as input, and the result as output.
|
||||
**
|
||||
******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(runBackendStepInput.getValueInteger(FIELD_SLEEP_MILLIS));
|
||||
}
|
||||
catch(InterruptedException e)
|
||||
{
|
||||
throw (new QException("Interrupted while sleeping..."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QBackendStepMetaData getMetaData()
|
||||
{
|
||||
return (new QBackendStepMetaData()
|
||||
.withName(STEP_NAME_SLEEPER)
|
||||
.withCode(new QCodeReference()
|
||||
.withName(SleeperStep.class.getName())
|
||||
.withCodeType(QCodeType.JAVA)
|
||||
.withCodeUsage(QCodeUsage.BACKEND_STEP))
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.addField(new QFieldMetaData(SleeperStep.FIELD_SLEEP_MILLIS, QFieldType.INTEGER))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Testing backend step - just throws an exception after however long you ask it to sleep.
|
||||
*******************************************************************************/
|
||||
public static class ThrowerStep implements BackendStep
|
||||
{
|
||||
public static final String FIELD_SLEEP_MILLIS = "sleepMillis";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute the backend step - using the request as input, and the result as output.
|
||||
**
|
||||
******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
int sleepMillis;
|
||||
try
|
||||
{
|
||||
sleepMillis = runBackendStepInput.getValueInteger(FIELD_SLEEP_MILLIS);
|
||||
}
|
||||
catch(QValueException qve)
|
||||
{
|
||||
sleepMillis = 50;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Thread.sleep(sleepMillis);
|
||||
}
|
||||
catch(InterruptedException e)
|
||||
{
|
||||
throw (new QException("Interrupted while sleeping..."));
|
||||
}
|
||||
|
||||
throw (new QException("I always throw."));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QBackendStepMetaData getMetaData()
|
||||
{
|
||||
return (new QBackendStepMetaData()
|
||||
.withName(STEP_NAME_THROWER)
|
||||
.withCode(new QCodeReference()
|
||||
.withName(ThrowerStep.class.getName())
|
||||
.withCodeType(QCodeType.JAVA)
|
||||
.withCodeUsage(QCodeUsage.BACKEND_STEP))
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.addField(new QFieldMetaData(ThrowerStep.FIELD_SLEEP_MILLIS, QFieldType.INTEGER))));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
--
|
||||
-- QQQ - Low-code Application Framework for Engineers.
|
||||
-- Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
-- 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
-- contact@kingsrook.com
|
||||
-- https://github.com/Kingsrook/
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as
|
||||
-- published by the Free Software Foundation, either version 3 of the
|
||||
-- License, or (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU Affero General Public License for more details.
|
||||
--
|
||||
-- You should have received a copy of the GNU Affero General Public License
|
||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS person;
|
||||
CREATE TABLE person
|
||||
(
|
||||
id INT AUTO_INCREMENT,
|
||||
create_date TIMESTAMP DEFAULT now(),
|
||||
modify_date TIMESTAMP DEFAULT now(),
|
||||
|
||||
first_name VARCHAR(80) NOT NULL,
|
||||
last_name VARCHAR(80) NOT NULL,
|
||||
birth_date DATE,
|
||||
email VARCHAR(250) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com');
|
||||
INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com');
|
||||
INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com');
|
||||
INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (4, 'Tyler', 'Samples', '1990-01-01', 'tsamples@mmltholdings.com');
|
||||
INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com');
|
Reference in New Issue
Block a user