QQQ-14 feedback from code review

This commit is contained in:
2022-06-24 16:05:34 -05:00
parent 09dd6e6147
commit be74eacb13
16 changed files with 164 additions and 246 deletions

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.interfaces.FunctionBody; import com.kingsrook.qqq.backend.core.interfaces.FunctionBody;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest;
@ -54,9 +55,6 @@ public class RunFunctionAction
{ {
ActionHelper.validateSession(runFunctionRequest); ActionHelper.validateSession(runFunctionRequest);
///////////////////////////////////////////////////////
//
///////////////////////////////////////////////////////
QProcessMetaData process = runFunctionRequest.getInstance().getProcess(runFunctionRequest.getProcessName()); QProcessMetaData process = runFunctionRequest.getInstance().getProcess(runFunctionRequest.getProcessName());
if(process == null) if(process == null)
{ {
@ -88,10 +86,10 @@ public class RunFunctionAction
** via the callback ** via the callback
** **
*******************************************************************************/ *******************************************************************************/
private void ensureInputFieldsAreInRequest(RunFunctionRequest runFunctionRequest, QFunctionMetaData function) private void ensureInputFieldsAreInRequest(RunFunctionRequest runFunctionRequest, QFunctionMetaData function) throws QException
{ {
QFunctionInputMetaData inputMetaData = function.getInputMetaData(); QFunctionInputMetaData inputMetaData = function.getInputMetaData();
if (inputMetaData == null) if(inputMetaData == null)
{ {
return; return;
} }
@ -109,7 +107,13 @@ public class RunFunctionAction
if(!fieldsToGet.isEmpty()) if(!fieldsToGet.isEmpty())
{ {
Map<String, Serializable> fieldValues = runFunctionRequest.getCallback().getFieldValues(fieldsToGet); QProcessCallback callback = runFunctionRequest.getCallback();
if(callback == null)
{
throw (new QException("Function is missing values for fields, but no callback was present to request fields from a user"));
}
Map<String, Serializable> fieldValues = callback.getFieldValues(fieldsToGet);
for(Map.Entry<String, Serializable> entry : fieldValues.entrySet()) for(Map.Entry<String, Serializable> entry : fieldValues.entrySet())
{ {
runFunctionRequest.addValue(entry.getKey(), entry.getValue()); runFunctionRequest.addValue(entry.getKey(), entry.getValue());
@ -138,7 +142,14 @@ public class RunFunctionAction
// todo - handle this being async (e.g., http) // todo - handle this being async (e.g., http)
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data // seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
// then this function can re-run, hopefully with the needed data. // then this function can re-run, hopefully with the needed data.
queryRequest.setFilter(runFunctionRequest.getCallback().getQueryFilter());
QProcessCallback callback = runFunctionRequest.getCallback();
if(callback == null)
{
throw (new QException("Function is missing input records, but no callback was present to get a query filter from a user"));
}
queryRequest.setFilter(callback.getQueryFilter());
QueryResult queryResult = new QueryAction().execute(queryRequest); QueryResult queryResult = new QueryAction().execute(queryRequest);
runFunctionRequest.setRecords(queryResult.getRecords()); runFunctionRequest.setRecords(queryResult.getRecords());

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions;
import java.util.List; import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest;
@ -145,10 +146,11 @@ public class RunProcessAction
** Load the process state into a function request from the state provider ** Load the process state into a function request from the state provider
** **
*******************************************************************************/ *******************************************************************************/
private void loadState(UUIDStateKey stateKey, RunFunctionRequest runFunctionRequest) private void loadState(UUIDStateKey stateKey, RunFunctionRequest runFunctionRequest) throws QException
{ {
ProcessState processState = getStateProvider().get(ProcessState.class, stateKey); Optional<ProcessState> processState = getStateProvider().get(ProcessState.class, stateKey);
runFunctionRequest.seedFromProcessState(processState); runFunctionRequest.seedFromProcessState(processState
.orElseThrow(() -> new QException("Could not find process state in state provider.")));
} }
} }

View File

@ -1,58 +0,0 @@
/*
* 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.core.callbacks;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
/*******************************************************************************
** Simple implementation of a callback, that does no-op (returns empty objects).
** Useful for scaffolding, perhaps tests.
*******************************************************************************/
public class NoopCallback implements QProcessCallback
{
/*******************************************************************************
** Get the filter query for this callback.
*******************************************************************************/
@Override
public QQueryFilter getQueryFilter()
{
return (new QQueryFilter());
}
/*******************************************************************************
** Get the field values for this callback.
*******************************************************************************/
@Override
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
{
return (Collections.emptyMap());
}
}

View File

@ -28,12 +28,17 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult;
/******************************************************************************* /*******************************************************************************
** TODO - document! ** Simple interface that a "custom" function (as in, a component of a Process)
** must implement.
*******************************************************************************/ *******************************************************************************/
public interface FunctionBody public interface FunctionBody
{ {
/******************************************************************************* /*******************************************************************************
** TODO - document! ** Execute the function - using the request as input, and the result as output.
**
** TODO - think about - why take the Result object as a param, instead of return it?
** Is this way easier for inter-language operability maybe?
* Also - there's way too much "process-specific gunk" in the Request object - can we simplify it?
*******************************************************************************/ *******************************************************************************/
void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) throws QException; void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) throws QException;
} }

View File

@ -42,6 +42,7 @@ public class QBackendMetaData
// @JsonFilter("secretsFilter") // @JsonFilter("secretsFilter")
/******************************************************************************* /*******************************************************************************
** Default Constructor. ** Default Constructor.
*******************************************************************************/ *******************************************************************************/
@ -51,17 +52,6 @@ public class QBackendMetaData
/*******************************************************************************
** Copy Constructor. Meant for use by sub-classes. Should copy all fields!
*******************************************************************************/
protected QBackendMetaData(QBackendMetaData source)
{
this.name = source.name;
this.backendType = source.backendType;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -83,7 +73,7 @@ public class QBackendMetaData
/******************************************************************************* /*******************************************************************************
** ** Fluent setter, returning generically, to help sub-class fluent flows
*******************************************************************************/ *******************************************************************************/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T extends QBackendMetaData> T withName(String name) public <T extends QBackendMetaData> T withName(String name)
@ -136,7 +126,7 @@ public class QBackendMetaData
/******************************************************************************* /*******************************************************************************
** ** Fluent setter, returning generically, to help sub-class fluent flows
*******************************************************************************/ *******************************************************************************/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T extends QBackendMetaData> T withBackendType(String backendType) public <T extends QBackendMetaData> T withBackendType(String backendType)

View File

@ -59,7 +59,7 @@ public class QInstance
/******************************************************************************* /*******************************************************************************
** ** Get the backend for a given table name
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData getBackendForTable(String tableName) public QBackendMetaData getBackendForTable(String tableName)
{ {
@ -69,19 +69,16 @@ public class QInstance
throw (new IllegalArgumentException("No table with name [" + tableName + "] found in this instance.")); throw (new IllegalArgumentException("No table with name [" + tableName + "] found in this instance."));
} }
QBackendMetaData backend = backends.get(table.getBackendName());
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
// validation should already let us know that this is valid, so no need to check/throw here // // validation should already let us know that this is valid, so no need to check/throw here //
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
return (backends.get(table.getBackendName()));
return (backend);
} }
/******************************************************************************* /*******************************************************************************
** ** Get the list of processes associated with a given table name
*******************************************************************************/ *******************************************************************************/
public List<QProcessMetaData> getProcessesForTable(String tableName) public List<QProcessMetaData> getProcessesForTable(String tableName)
{ {

View File

@ -38,19 +38,30 @@ import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.modules.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/******************************************************************************* /*******************************************************************************
** ** Utility methods to help with deserializing JSON streams into QQQ models.
** Specifically meant to be used within a jackson custom deserializer (e.g.,
** an implementation of JsonDeserializer).
*******************************************************************************/ *******************************************************************************/
public class DeserializerUtils public class DeserializerUtils
{ {
private static final Logger LOG = LogManager.getLogger(DeserializerUtils.class);
/******************************************************************************* /*******************************************************************************
** ** For a given (jackson, JSON) treeNode, look at its backendType property,
** and return an instance of the corresponding QBackendModule.
*******************************************************************************/ *******************************************************************************/
public static QBackendModuleInterface getBackendModule(TreeNode treeNode) throws IOException public static QBackendModuleInterface getBackendModule(TreeNode treeNode) throws IOException
{ {
/////////////////////////////////////////////////////////////////////////////////
// validate the backendType property is present, as text, in the json treeNode //
/////////////////////////////////////////////////////////////////////////////////
TreeNode backendTypeTreeNode = treeNode.get("backendType"); TreeNode backendTypeTreeNode = treeNode.get("backendType");
if(backendTypeTreeNode == null || backendTypeTreeNode instanceof NullNode) if(backendTypeTreeNode == null || backendTypeTreeNode instanceof NullNode)
{ {
@ -61,44 +72,67 @@ public class DeserializerUtils
{ {
throw new IOException("backendType is not a string value (is: " + backendTypeTreeNode.getClass().getSimpleName() + ")"); throw new IOException("backendType is not a string value (is: " + backendTypeTreeNode.getClass().getSimpleName() + ")");
} }
else
{
String backendType = textNode.asText();
try try
{ {
return new QBackendModuleDispatcher().getQBackendModule(backendType); /////////////////////////////////////////////////////////////////////////////////////////////////
} // get the value of the backendType json node, and use it to look up the qBackendModule object //
catch(QModuleDispatchException e) /////////////////////////////////////////////////////////////////////////////////////////////////
{ String backendType = textNode.asText();
throw (new IOException(e)); return new QBackendModuleDispatcher().getQBackendModule(backendType);
} }
catch(QModuleDispatchException e)
{
throw (new IOException(e));
} }
} }
/******************************************************************************* /*******************************************************************************
** Using reflection, create & populate an instance of a class, based on the
** properties in a jackson/json treeNode.
** **
*******************************************************************************/ *******************************************************************************/
public static <T> T reflectivelyDeserialize(Class<T> outputClass, TreeNode treeNode) throws IOException public static <T> T reflectivelyDeserialize(Class<T> outputClass, TreeNode treeNode) throws IOException
{ {
try try
{ {
/////////////////////////////////
// construct the output object //
/////////////////////////////////
T output = outputClass.getConstructor().newInstance(); T output = outputClass.getConstructor().newInstance();
/////////////////////////////////////////////////////////////////////////////////////////////////
// set up a mapping between field names, and lambdas which will take a String (from the json), //
// and set it in the output object, doing type conversion as needed. //
// do this by iterating over methods on the output class that look like setters. //
/////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Consumer<String>> setterMap = new HashMap<>(); Map<String, Consumer<String>> setterMap = new HashMap<>();
for(Method method : outputClass.getMethods()) for(Method method : outputClass.getMethods())
{ {
/////////////////////////////////////////////////////////////
// setters start with the word "set", and have 1 parameter //
/////////////////////////////////////////////////////////////
if(method.getName().startsWith("set") && method.getParameterTypes().length == 1) if(method.getName().startsWith("set") && method.getParameterTypes().length == 1)
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// get the parameter type, and the name of the field (remove set from the method name, and downshift the first letter) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Class<?> parameterType = method.getParameterTypes()[0]; Class<?> parameterType = method.getParameterTypes()[0];
String fieldName = method.getName().substring(3, 4).toLowerCase(Locale.ROOT) + method.getName().substring(4); String fieldName = method.getName().substring(3, 4).toLowerCase(Locale.ROOT) + method.getName().substring(4);
///////////////////////////////////////////////////////////////////////////////////
// put the entry in the map - where the value here is a consumer lambda function //
///////////////////////////////////////////////////////////////////////////////////
setterMap.put(fieldName, (String value) -> setterMap.put(fieldName, (String value) ->
{ {
try try
{ {
//////////////////////////////////////////////////////////////////////////////////////////////////
// based on the parameter type, handle it differently - either type-converting (e.g., parseInt) //
// or gracefully ignoring, or failing. //
//////////////////////////////////////////////////////////////////////////////////////////////////
if(parameterType.equals(String.class)) if(parameterType.equals(String.class))
{ {
method.invoke(output, value); method.invoke(output, value);
@ -121,23 +155,20 @@ public class DeserializerUtils
} }
else if(parameterType.equals(Class.class)) else if(parameterType.equals(Class.class))
{ {
//////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// specifically do NOT try to handle Class type arguments // // specifically do NOT try to handle Class type arguments //
//////////////////////////////////////////////////////////// // we hit this when trying to de-serialize a QBackendMetaData, and we found its setBackendType(Class) method //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
} }
else if(parameterType.getPackageName().startsWith("java.")) else if(parameterType.getPackageName().startsWith("java."))
{ {
//////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we hit this, we might want to add an else-if to handle the type // // if we hit this, we might want to add an else-if to handle the type. //
//////////////////////////////////////////////////////////////////////// // otherwise, either find some jackson annotation that makes sense, and apply it to the setter method, //
// or if no jackson annotation is right, then come up with annotation of our own. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
throw (new RuntimeException("Field " + fieldName + " is of an unhandled type " + parameterType.getName() + " when deserializing " + outputClass.getName())); throw (new RuntimeException("Field " + fieldName + " is of an unhandled type " + parameterType.getName() + " when deserializing " + outputClass.getName()));
} }
else
{
////////////////////////////////////
// gracefully ignore other types. //
////////////////////////////////////
}
} }
catch(IllegalAccessException | InvocationTargetException e) catch(IllegalAccessException | InvocationTargetException e)
{ {
@ -153,9 +184,8 @@ public class DeserializerUtils
} }
catch(Exception e) catch(Exception e)
{ {
throw (new IOException("Error reflectively deserializing table details", e)); throw (new IOException("Error deserializing json object into instance of " + outputClass.getName(), e));
} }
} }
@ -169,24 +199,35 @@ public class DeserializerUtils
*******************************************************************************/ *******************************************************************************/
private static void deserializeBean(TreeNode treeNode, Map<String, Consumer<String>> setterMap) throws IOException private static void deserializeBean(TreeNode treeNode, Map<String, Consumer<String>> setterMap) throws IOException
{ {
///////////////////////////////////////////////////////
// iterate over fields in the json object (treeNode) //
///////////////////////////////////////////////////////
Iterator<String> fieldNamesIterator = treeNode.fieldNames(); Iterator<String> fieldNamesIterator = treeNode.fieldNames();
while(fieldNamesIterator.hasNext()) while(fieldNamesIterator.hasNext())
{ {
String fieldName = fieldNamesIterator.next(); String fieldName = fieldNamesIterator.next();
//////////////////////////////////////////////////////////////////////////
// error if we find a field in the json that we don't have a setter for //
//////////////////////////////////////////////////////////////////////////
if(!setterMap.containsKey(fieldName)) if(!setterMap.containsKey(fieldName))
{ {
throw (new IllegalArgumentException("Unexpected value: " + fieldName)); throw (new IOException("Unexpected field (no corresponding setter): " + fieldName));
} }
// call the setter -
TreeNode fieldNode = treeNode.get(fieldName); TreeNode fieldNode = treeNode.get(fieldName);
if(fieldNode instanceof NullNode) if(fieldNode instanceof NullNode)
{ {
setterMap.get(fieldName).accept(null); setterMap.get(fieldName).accept(null);
} }
else if(fieldNode instanceof TextNode textNode)
{
setterMap.get(fieldName).accept(textNode.asText());
}
else else
{ {
setterMap.get(fieldName).accept(((TextNode) fieldNode).asText()); throw (new IOException("Unexpected node type (" + fieldNode.getClass() + ") for field: " + fieldName));
} }
} }
} }

View File

@ -1,73 +0,0 @@
/*
* 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.core.processes.implementations.etl.basic;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback;
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
/*******************************************************************************
** Provide callback functionality for the BasicETL process
*******************************************************************************/
public class BasicETLCallback implements QProcessCallback
{
/*******************************************************************************
** Get the filter query for this callback.
*******************************************************************************/
@Override
public QQueryFilter getQueryFilter()
{
// todo - possibly get something from params? through state? added as a method arg?
return null;
}
/*******************************************************************************
** Get the field values for this callback.
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
@Override
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
{
Map<String, Serializable> rs = new HashMap<>();
for(QFieldMetaData field : fields)
{
// TODO - replace this whole thing with our params mechanism
// TODO - add default methods to the interface that throw, presumably?
rs.put(field.getName(), switch(field.getName())
{
case BasicETLProcess.FIELD_SOURCE_TABLE -> "personFile";
case BasicETLProcess.FIELD_DESTINATION_TABLE -> "person";
default -> throw new IllegalArgumentException("Unhandled field: " + field.getName());
});
}
return (rs);
}
}

View File

@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
*******************************************************************************/ *******************************************************************************/
public class BasicETLProcess public class BasicETLProcess
{ {
public static final String PROCESS_NAME = "etl.basic";
public static final String FIELD_SOURCE_TABLE = "sourceTable"; public static final String FIELD_SOURCE_TABLE = "sourceTable";
public static final String FIELD_DESTINATION_TABLE = "destinationTable"; public static final String FIELD_DESTINATION_TABLE = "destinationTable";
public static final String FIELD_RECORD_COUNT = "recordCount"; public static final String FIELD_RECORD_COUNT = "recordCount";
@ -70,7 +71,7 @@ public class BasicETLProcess
.addField(new QFieldMetaData(FIELD_RECORD_COUNT, QFieldType.INTEGER))); .addField(new QFieldMetaData(FIELD_RECORD_COUNT, QFieldType.INTEGER)));
return new QProcessMetaData() return new QProcessMetaData()
.withName("etl.basic") .withName(PROCESS_NAME)
.addFunction(extractFunction) .addFunction(extractFunction)
.addFunction(loadFunction); .addFunction(loadFunction);
} }

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.state;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
/******************************************************************************* /*******************************************************************************
@ -63,7 +64,7 @@ public class InMemoryStateProvider implements StateProviderInterface
/******************************************************************************* /*******************************************************************************
** ** Put a block of data, under a key, into the state store.
*******************************************************************************/ *******************************************************************************/
@Override @Override
public <T extends Serializable> void put(AbstractStateKey key, T data) public <T extends Serializable> void put(AbstractStateKey key, T data)
@ -74,14 +75,14 @@ public class InMemoryStateProvider implements StateProviderInterface
/******************************************************************************* /*******************************************************************************
** ** Get a block of data, under a key, from the state store.
*******************************************************************************/ *******************************************************************************/
@Override @Override
public <T extends Serializable> T get(Class<? extends T> type, AbstractStateKey key) public <T extends Serializable> Optional<T> get(Class<? extends T> type, AbstractStateKey key)
{ {
try try
{ {
return type.cast(map.get(key)); return Optional.ofNullable(type.cast(map.get(key)));
} }
catch(ClassCastException cce) catch(ClassCastException cce)
{ {

View File

@ -23,10 +23,22 @@ package com.kingsrook.qqq.backend.core.state;
import java.io.Serializable; import java.io.Serializable;
import java.util.Optional;
/******************************************************************************* /*******************************************************************************
** QQQ state provider interface. Provides standard interface for various
** implementations of how to store & retrieve user/process state data, like
** sessions, or process data. Not like permanent record data - that is done in
** Backend modules.
** **
** Different implementations may be: in-memory (non-persistent!!), or on-disk
** (with the tradeoffs that has), in-database, in-cache-system, etc.
**
** Things which probably haven't been thought about here include:
** - multi-layering. e.g., always have an in-memory layer on top of a more
** persistent backend, but then how to avoid staleness in-memory?
* - cleanup. when do we ever purge things to avoid running out of memory/storage?
*******************************************************************************/ *******************************************************************************/
public interface StateProviderInterface public interface StateProviderInterface
{ {
@ -39,5 +51,5 @@ public interface StateProviderInterface
/******************************************************************************* /*******************************************************************************
** Get a block of data, under a key, from the state store. ** Get a block of data, under a key, from the state store.
*******************************************************************************/ *******************************************************************************/
<T extends Serializable> T get(Class<? extends T> type, AbstractStateKey key); <T extends Serializable> Optional<T> get(Class<? extends T> type, AbstractStateKey key);
} }

View File

@ -26,6 +26,7 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -63,7 +64,7 @@ public class TempFileStateProvider implements StateProviderInterface
/******************************************************************************* /*******************************************************************************
** ** Put a block of data, under a key, into the state store.
*******************************************************************************/ *******************************************************************************/
@Override @Override
public <T extends Serializable> void put(AbstractStateKey key, T data) public <T extends Serializable> void put(AbstractStateKey key, T data)
@ -83,19 +84,19 @@ public class TempFileStateProvider implements StateProviderInterface
/******************************************************************************* /*******************************************************************************
** ** Get a block of data, under a key, from the state store.
*******************************************************************************/ *******************************************************************************/
@Override @Override
public <T extends Serializable> T get(Class<? extends T> type, AbstractStateKey key) public <T extends Serializable> Optional<T> get(Class<? extends T> type, AbstractStateKey key)
{ {
try try
{ {
String json = FileUtils.readFileToString(new File("/tmp/" + key.toString())); String json = FileUtils.readFileToString(new File("/tmp/" + key.toString()));
return JsonUtils.toObject(json, type); return (Optional.of(JsonUtils.toObject(json, type)));
} }
catch(FileNotFoundException fnfe) catch(FileNotFoundException fnfe)
{ {
return (null); return (Optional.empty());
} }
catch(IOException ie) catch(IOException ie)
{ {

View File

@ -26,11 +26,6 @@ import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -50,17 +45,12 @@ class BasicETLProcessTest
@Test @Test
public void test() throws QException public void test() throws QException
{ {
BasicETLProcess basicETLProcess = new BasicETLProcess(); RunProcessRequest request = new RunProcessRequest(TestUtils.defineInstance());
QProcessMetaData processMetaData = basicETLProcess.defineProcessMetaData();
QInstance instance = TestUtils.defineInstance();
RunProcessRequest request = new RunProcessRequest(instance);
instance.addProcess(processMetaData);
defineFileBackendAndPersonFileTable(instance);
request.setSession(TestUtils.getMockSession()); request.setSession(TestUtils.getMockSession());
request.setProcessName(processMetaData.getName()); request.setProcessName(BasicETLProcess.PROCESS_NAME);
request.setCallback(new BasicETLCallback()); // todo - uh, maybe a method on the process to get its callback? request.addValue(BasicETLProcess.FIELD_SOURCE_TABLE, TestUtils.defineTablePerson().getName());
request.addValue(BasicETLProcess.FIELD_DESTINATION_TABLE, TestUtils.definePersonFileTable().getName());
RunProcessResult result = new RunProcessAction().execute(request); RunProcessResult result = new RunProcessAction().execute(request);
assertNotNull(result); assertNotNull(result);
assertNull(result.getError()); assertNull(result.getError());
@ -68,27 +58,4 @@ class BasicETLProcessTest
} }
/*******************************************************************************
** Define the 'person' table used in standard tests.
*******************************************************************************/
public static void defineFileBackendAndPersonFileTable(QInstance instance)
{
QTableMetaData personFileTable = new QTableMetaData()
.withName("personFile")
.withLabel("Person File")
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("firstName", QFieldType.STRING))
.withField(new QFieldMetaData("lastName", QFieldType.STRING))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE))
.withField(new QFieldMetaData("email", QFieldType.STRING))
.withField(new QFieldMetaData("homeState", QFieldType.STRING).withPossibleValueSourceName("state"));
instance.addTable(personFileTable);
}
} }

View File

@ -44,7 +44,7 @@ public class InMemoryStateProviderTest
InMemoryStateProvider stateProvider = InMemoryStateProvider.getInstance(); InMemoryStateProvider stateProvider = InMemoryStateProvider.getInstance();
UUIDStateKey key = new UUIDStateKey(); UUIDStateKey key = new UUIDStateKey();
Assertions.assertNull(stateProvider.get(QRecord.class, key), "Key not found in state should return null"); Assertions.assertTrue(stateProvider.get(QRecord.class, key).isEmpty(), "Key not found in state should return empty");
} }
@ -61,7 +61,7 @@ public class InMemoryStateProviderTest
QRecord qRecord = new QRecord().withValue("uuid", uuid); QRecord qRecord = new QRecord().withValue("uuid", uuid);
stateProvider.put(key, qRecord); stateProvider.put(key, qRecord);
QRecord qRecordFromState = stateProvider.get(QRecord.class, key); QRecord qRecordFromState = stateProvider.get(QRecord.class, key).get();
Assertions.assertEquals(uuid, qRecordFromState.getValueString("uuid"), "Should read value from state persistence"); Assertions.assertEquals(uuid, qRecordFromState.getValueString("uuid"), "Should read value from state persistence");
} }

View File

@ -44,7 +44,7 @@ public class TempFileStateProviderTest
TempFileStateProvider stateProvider = TempFileStateProvider.getInstance(); TempFileStateProvider stateProvider = TempFileStateProvider.getInstance();
UUIDStateKey key = new UUIDStateKey(); UUIDStateKey key = new UUIDStateKey();
Assertions.assertNull(stateProvider.get(QRecord.class, key), "Key not found in state should return null"); Assertions.assertTrue(stateProvider.get(QRecord.class, key).isEmpty(), "Key not found in state should return empty");
} }
@ -61,7 +61,7 @@ public class TempFileStateProviderTest
QRecord qRecord = new QRecord().withValue("uuid", uuid); QRecord qRecord = new QRecord().withValue("uuid", uuid);
stateProvider.put(key, qRecord); stateProvider.put(key, qRecord);
QRecord qRecordFromState = stateProvider.get(QRecord.class, key); QRecord qRecordFromState = stateProvider.get(QRecord.class, key).get();
Assertions.assertEquals(uuid, qRecordFromState.getValueString("uuid"), "Should read value from state persistence"); Assertions.assertEquals(uuid, qRecordFromState.getValueString("uuid"), "Should read value from state persistence");
} }

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.utils;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
@ -43,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView;
import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.mock.MockAuthenticationModule; import com.kingsrook.qqq.backend.core.modules.mock.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
/******************************************************************************* /*******************************************************************************
@ -53,6 +55,8 @@ public class TestUtils
{ {
public static String DEFAULT_BACKEND_NAME = "default"; public static String DEFAULT_BACKEND_NAME = "default";
/******************************************************************************* /*******************************************************************************
** Define the instance used in standard tests. ** Define the instance used in standard tests.
** **
@ -63,9 +67,11 @@ public class TestUtils
qInstance.setAuthentication(defineAuthentication()); qInstance.setAuthentication(defineAuthentication());
qInstance.addBackend(defineBackend()); qInstance.addBackend(defineBackend());
qInstance.addTable(defineTablePerson()); qInstance.addTable(defineTablePerson());
qInstance.addTable(definePersonFileTable());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeople());
qInstance.addProcess(defineProcessAddToPeoplesAge()); qInstance.addProcess(defineProcessAddToPeoplesAge());
qInstance.addProcess(new BasicETLProcess().defineProcessMetaData());
return (qInstance); return (qInstance);
} }
@ -118,7 +124,7 @@ public class TestUtils
return new QTableMetaData() return new QTableMetaData()
.withName("person") .withName("person")
.withLabel("Person") .withLabel("Person")
.withBackendName(defineBackend().getName()) .withBackendName(DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
@ -132,6 +138,21 @@ public class TestUtils
/*******************************************************************************
** Define a 2nd version of the 'person' table for this test (pretend it's backed by a file)
*******************************************************************************/
public static QTableMetaData definePersonFileTable()
{
return (new QTableMetaData()
.withName("personFile")
.withLabel("Person File")
.withBackendName(DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id")
.withFields(TestUtils.defineTablePerson().getFields()));
}
/******************************************************************************* /*******************************************************************************
** Define the 'greet people' process ** Define the 'greet people' process
*******************************************************************************/ *******************************************************************************/
@ -143,7 +164,7 @@ public class TestUtils
.addFunction(new QFunctionMetaData() .addFunction(new QFunctionMetaData()
.withName("prepare") .withName("prepare")
.withCode(new QCodeReference() .withCode(new QCodeReference()
.withName("com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody") .withName(MockFunctionBody.class.getName())
.withCodeType(QCodeType.JAVA) .withCodeType(QCodeType.JAVA)
.withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context?
.withInputData(new QFunctionInputMetaData() .withInputData(new QFunctionInputMetaData()