Merge branch 'feature/QQQ-40-record-automations' into feature/sprint-10

This commit is contained in:
2022-08-31 15:12:54 -05:00
26 changed files with 2605 additions and 192 deletions

View File

@ -0,0 +1,78 @@
package com.kingsrook.qqq.backend.core.actions.automation;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
** enum of possible values for a record's Automation Status.
*******************************************************************************/
public enum AutomationStatus implements PossibleValueEnum<Integer>
{
PENDING_INSERT_AUTOMATIONS(1, "Pending Insert Automations"),
RUNNING_INSERT_AUTOMATIONS(2, "Running Insert Automations"),
FAILED_INSERT_AUTOMATIONS(3, "Failed Insert Automations"),
PENDING_UPDATE_AUTOMATIONS(4, "Pending Update Automations"),
RUNNING_UPDATE_AUTOMATIONS(5, "Running Update Automations"),
FAILED_UPDATE_AUTOMATIONS(6, "Failed Update Automations"),
OK(7, "OK");
private final Integer id;
private final String label;
/*******************************************************************************
**
*******************************************************************************/
AutomationStatus(int id, String label)
{
this.id = id;
this.label = label;
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return (id);
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return (label);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Integer getPossibleValueId()
{
return (getId());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return (getLabel());
}
}

View File

@ -0,0 +1,19 @@
package com.kingsrook.qqq.backend.core.actions.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** Base class for custom-codes to run as an automation action
*******************************************************************************/
public abstract class RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -0,0 +1,96 @@
package com.kingsrook.qqq.backend.core.actions.automation;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Utility class for updating the automation status data for records
*******************************************************************************/
public class RecordAutomationStatusUpdater
{
private static final Logger LOG = LogManager.getLogger(RecordAutomationStatusUpdater.class);
/*******************************************************************************
** for a list of records from a table, set their automation status - based on
** how the table is configured.
*******************************************************************************/
public static boolean setAutomationStatusInRecords(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
{
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
{
return (false);
}
if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS) || automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{
Exception e = new Exception();
for(StackTraceElement stackTraceElement : e.getStackTrace())
{
String className = stackTraceElement.getClassName();
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test"))
{
LOG.info("Avoiding re-setting automation status to PENDING while running an automation");
return (false);
}
}
}
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
for(QRecord record : records)
{
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
// todo - another field - for the automation timestamp??
}
}
return (true);
}
/*******************************************************************************
** for a list of records, update their automation status and actually Update the
** backend as well.
*******************************************************************************/
public static void setAutomationStatusInRecordsAndUpdate(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) throws QException
{
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
boolean didSetStatusField = setAutomationStatusInRecords(table, records, automationStatus);
if(didSetStatusField)
{
UpdateInput updateInput = new UpdateInput(instance);
updateInput.setSession(session);
updateInput.setTableName(table.getName());
updateInput.setRecords(records);
new UpdateAction().execute(updateInput);
}
}
else
{
// todo - verify if this is valid as other types are built
throw (new NotImplementedException("Updating record automation status is not implemented for table [" + table + "], tracking type: "
+ (automationDetails == null ? "null" : automationDetails.getStatusTracking().getType())));
}
}
}

View File

@ -0,0 +1,207 @@
package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Singleton that runs a Polling Automation Provider. Call its 'start' method
** to make it go. Likely you need to set a sessionSupplier before you start -
** so that threads that do work will have a valid session.
*******************************************************************************/
public class PollingAutomationExecutor
{
private static final Logger LOG = LogManager.getLogger(PollingAutomationExecutor.class);
private static PollingAutomationExecutor pollingAutomationExecutor = null;
private Integer initialDelayMillis = 3000;
private Integer delayMillis = 1000;
private Supplier<QSession> sessionSupplier;
private RunningState runningState = RunningState.STOPPED;
private ScheduledExecutorService service;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private PollingAutomationExecutor()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static PollingAutomationExecutor getInstance()
{
if(pollingAutomationExecutor == null)
{
pollingAutomationExecutor = new PollingAutomationExecutor();
}
return (pollingAutomationExecutor);
}
/*******************************************************************************
**
** @return true iff the schedule was started
*******************************************************************************/
public boolean start(QInstance instance, String providerName)
{
if(!runningState.equals(RunningState.STOPPED))
{
LOG.info("Request to start from an invalid running state [" + runningState + "]. Must be STOPPED.");
return (false);
}
LOG.info("Starting PollingAutomationExecutor");
service = Executors.newSingleThreadScheduledExecutor();
service.scheduleWithFixedDelay(new PollingAutomationRunner(instance, providerName, sessionSupplier), initialDelayMillis, delayMillis, TimeUnit.MILLISECONDS);
runningState = RunningState.RUNNING;
return (true);
}
/*******************************************************************************
** Stop, and don't wait to check if it worked or anything
*******************************************************************************/
public void stopAsync()
{
Runnable stopper = this::stop;
stopper.run();
}
/*******************************************************************************
** Issue a stop, and wait (a while) for it to succeed.
**
** @return true iff we see that the service fully stopped.
*******************************************************************************/
public boolean stop()
{
if(!runningState.equals(RunningState.RUNNING))
{
LOG.info("Request to stop from an invalid running state [" + runningState + "]. Must be RUNNING.");
return (false);
}
LOG.info("Stopping PollingAutomationExecutor");
runningState = RunningState.STOPPING;
service.shutdown();
try
{
if(service.awaitTermination(300, TimeUnit.SECONDS))
{
LOG.info("Successfully stopped PollingAutomationExecutor");
runningState = RunningState.STOPPED;
return (true);
}
LOG.info("Timed out waiting for service to fully terminate. Will be left in STOPPING state.");
}
catch(InterruptedException ie)
{
///////////////////////////////
// what does this ever mean? //
///////////////////////////////
}
return (false);
}
/*******************************************************************************
** Getter for initialDelayMillis
**
*******************************************************************************/
public Integer getInitialDelayMillis()
{
return initialDelayMillis;
}
/*******************************************************************************
** Setter for initialDelayMillis
**
*******************************************************************************/
public void setInitialDelayMillis(Integer initialDelayMillis)
{
this.initialDelayMillis = initialDelayMillis;
}
/*******************************************************************************
** Getter for delayMillis
**
*******************************************************************************/
public Integer getDelayMillis()
{
return delayMillis;
}
/*******************************************************************************
** Setter for delayMillis
**
*******************************************************************************/
public void setDelayMillis(Integer delayMillis)
{
this.delayMillis = delayMillis;
}
/*******************************************************************************
** Setter for sessionSupplier
**
*******************************************************************************/
public void setSessionSupplier(Supplier<QSession> sessionSupplier)
{
this.sessionSupplier = sessionSupplier;
}
/*******************************************************************************
** Getter for runningState
**
*******************************************************************************/
public RunningState getRunningState()
{
return runningState;
}
/*******************************************************************************
**
*******************************************************************************/
public enum RunningState
{
STOPPED,
RUNNING,
STOPPING,
}
}

View File

@ -0,0 +1,240 @@
package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.automation.RecordAutomationInput;
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.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Runnable for the Polling Automation Provider, that looks for records that
** need automations, and executes them.
*******************************************************************************/
class PollingAutomationRunner implements Runnable
{
private static final Logger LOG = LogManager.getLogger(PollingAutomationRunner.class);
private QInstance instance;
private String providerName;
private Supplier<QSession> sessionSupplier;
private List<QTableMetaData> managedTables = new ArrayList<>();
private Map<String, List<TableAutomationAction>> tableInsertActions = new HashMap<>();
private Map<String, List<TableAutomationAction>> tableUpdateActions = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public PollingAutomationRunner(QInstance instance, String providerName, Supplier<QSession> sessionSupplier)
{
this.instance = instance;
this.providerName = providerName;
this.sessionSupplier = sessionSupplier;
//////////////////////////////////////////////////////////////////////
// todo - share logic like this among any automation implementation //
//////////////////////////////////////////////////////////////////////
for(QTableMetaData table : instance.getTables().values())
{
if(table.getAutomationDetails() != null && this.providerName.equals(table.getAutomationDetails().getProviderName()))
{
managedTables.add(table);
///////////////////////////////////////////////////////////////////////////
// organize the table's actions by type //
// todo - in future, need user-defined actions here too (and refreshed!) //
///////////////////////////////////////////////////////////////////////////
for(TableAutomationAction action : table.getAutomationDetails().getActions())
{
if(TriggerEvent.POST_INSERT.equals(action.getTriggerEvent()))
{
tableInsertActions.putIfAbsent(table.getName(), new ArrayList<>());
tableInsertActions.get(table.getName()).add(action);
}
else if(TriggerEvent.POST_UPDATE.equals(action.getTriggerEvent()))
{
tableUpdateActions.putIfAbsent(table.getName(), new ArrayList<>());
tableUpdateActions.get(table.getName()).add(action);
}
}
//////////////////////////////
// sort actions by priority //
//////////////////////////////
if(tableInsertActions.containsKey(table.getName()))
{
tableInsertActions.get(table.getName()).sort(Comparator.comparing(TableAutomationAction::getPriority));
}
if(tableUpdateActions.containsKey(table.getName()))
{
tableUpdateActions.get(table.getName()).sort(Comparator.comparing(TableAutomationAction::getPriority));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
Thread.currentThread().setName(getClass().getSimpleName() + ">" + providerName);
LOG.info("Running " + this.getClass().getSimpleName() + "[providerName=" + providerName + "]");
for(QTableMetaData table : managedTables)
{
try
{
processTable(table);
}
catch(Exception e)
{
LOG.error("Error processing automations on table: " + table, e);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void processTable(QTableMetaData table) throws QException
{
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession();
processTableInsertOrUpdate(table, session, true);
processTableInsertOrUpdate(table, session, false);
}
/*******************************************************************************
**
*******************************************************************************/
private void processTableInsertOrUpdate(QTableMetaData table, QSession session, boolean isInsert) throws QException
{
AutomationStatus automationStatus = isInsert ? AutomationStatus.PENDING_INSERT_AUTOMATIONS : AutomationStatus.PENDING_UPDATE_AUTOMATIONS;
List<TableAutomationAction> actions = (isInsert ? tableInsertActions : tableUpdateActions).get(table.getName());
if(CollectionUtils.nullSafeIsEmpty(actions))
{
return;
}
LOG.info(" Query for records " + automationStatus + " in " + table);
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session); // todo - where the heck can we get this from??
queryInput.setTableName(table.getName());
for(TableAutomationAction action : actions)
{
QQueryFilter filter = action.getFilter();
if(filter == null)
{
filter = new QQueryFilter();
}
filter.addCriteria(new QFilterCriteria(table.getAutomationDetails().getStatusTracking().getFieldName(), QCriteriaOperator.IN, List.of(automationStatus.getId())));
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
// todo - pipe this query!!
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
LOG.info(" Processing " + queryOutput.getRecords().size() + " records in " + table + " for action " + action);
processRecords(table, actions, queryOutput.getRecords(), session, isInsert);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void processRecords(QTableMetaData table, List<TableAutomationAction> actions, List<QRecord> records, QSession session, boolean isInsert) throws QException
{
try
{
updateRecordAutomationStatus(table, session, records, isInsert ? AutomationStatus.RUNNING_INSERT_AUTOMATIONS : AutomationStatus.RUNNING_UPDATE_AUTOMATIONS);
for(TableAutomationAction action : actions)
{
////////////////////////////////////
// todo - what, re-query them? :( //
////////////////////////////////////
if(StringUtils.hasContent(action.getProcessName()))
{
//////////////////////////////////////////////////////////////////////////////////////////////
// todo - uh, how to make these records the input, where an extract step might be involved? //
// should extract step ... see record list and just use it? i think maybe? //
//////////////////////////////////////////////////////////////////////////////////////////////
throw (new NotImplementedException("processes for automation not yet implemented"));
}
else if(action.getCodeReference() != null)
{
LOG.info(" Executing action: [" + action.getName() + "] as code reference: " + action.getCodeReference());
RecordAutomationInput input = new RecordAutomationInput(instance);
input.setSession(session);
input.setTableName(table.getName());
input.setRecordList(records);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
recordAutomationHandler.execute(input);
}
}
updateRecordAutomationStatus(table, session, records, AutomationStatus.OK);
}
catch(Exception e)
{
updateRecordAutomationStatus(table, session, records, isInsert ? AutomationStatus.FAILED_INSERT_AUTOMATIONS : AutomationStatus.FAILED_UPDATE_AUTOMATIONS);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void updateRecordAutomationStatus(QTableMetaData table, QSession session, List<QRecord> records, AutomationStatus automationStatus) throws QException
{
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, automationStatus);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -31,6 +32,7 @@ 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.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -138,6 +140,42 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
public static RecordAutomationHandler getRecordAutomationHandler(TableAutomationAction action) throws QException
{
try
{
QCodeReference codeReference = action.getCodeReference();
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
Class<?> codeClass = Class.forName(codeReference.getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof RecordAutomationHandler recordAutomationHandler))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of RecordAutomationHandler"));
}
return (recordAutomationHandler);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting record automation handler for action [" + action.getName() + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
@ -48,6 +50,9 @@ public class InsertAction
*******************************************************************************/
public InsertOutput execute(InsertInput insertInput) throws QException
{
ActionHelper.validateSession(insertInput);
setAutomationStatusField(insertInput);
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
@ -57,6 +62,16 @@ public class InsertAction
/*******************************************************************************
** If the table being inserted into uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(InsertInput insertInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
@ -42,6 +44,7 @@ public class UpdateAction
public UpdateOutput execute(UpdateInput updateInput) throws QException
{
ActionHelper.validateSession(updateInput);
setAutomationStatusField(updateInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
@ -50,4 +53,15 @@ public class UpdateAction
// todo post-customization - can do whatever w/ the result if you want
return updateResult;
}
/*******************************************************************************
** If the table being updated uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(UpdateInput updateInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS);
}
}

View File

@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
@ -39,10 +40,14 @@ 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.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
@ -65,6 +70,8 @@ public class QInstanceValidator
private boolean printWarnings = false;
private List<String> errors = new ArrayList<>();
/*******************************************************************************
@ -96,14 +103,14 @@ public class QInstanceValidator
//////////////////////////////////////////////////////////////////////////
// do the validation checks - a good qInstance has all conditions TRUE! //
//////////////////////////////////////////////////////////////////////////
List<String> errors = new ArrayList<>();
try
{
validateBackends(qInstance, errors);
validateTables(qInstance, errors);
validateProcesses(qInstance, errors);
validateApps(qInstance, errors);
validatePossibleValueSources(qInstance, errors);
validateBackends(qInstance);
validateAutomationProviders(qInstance);
validateTables(qInstance);
validateProcesses(qInstance);
validateApps(qInstance);
validatePossibleValueSources(qInstance);
}
catch(Exception e)
{
@ -123,13 +130,13 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateBackends(QInstance qInstance, List<String> errors)
private void validateBackends(QInstance qInstance)
{
if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getBackends()), "At least 1 backend must be defined."))
if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getBackends()), "At least 1 backend must be defined."))
{
qInstance.getBackends().forEach((backendName, backend) ->
{
assertCondition(errors, Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
});
}
}
@ -139,42 +146,59 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateTables(QInstance qInstance, List<String> errors)
private void validateAutomationProviders(QInstance qInstance)
{
if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getTables()),
if(qInstance.getAutomationProviders() != null)
{
qInstance.getAutomationProviders().forEach((name, automationProvider) ->
{
assertCondition(Objects.equals(name, automationProvider.getName()), "Inconsistent naming for automationProvider: " + name + "/" + automationProvider.getName() + ".");
assertCondition(automationProvider.getType() != null, "Missing type for automationProvider: " + name);
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTables(QInstance qInstance)
{
if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getTables()),
"At least 1 table must be defined."))
{
qInstance.getTables().forEach((tableName, table) ->
{
assertCondition(errors, Objects.equals(tableName, table.getName()), "Inconsistent naming for table: " + tableName + "/" + table.getName() + ".");
assertCondition(Objects.equals(tableName, table.getName()), "Inconsistent naming for table: " + tableName + "/" + table.getName() + ".");
validateAppChildHasValidParentAppName(qInstance, errors, table);
validateAppChildHasValidParentAppName(qInstance, table);
////////////////////////////////////////
// validate the backend for the table //
////////////////////////////////////////
if(assertCondition(errors, StringUtils.hasContent(table.getBackendName()),
if(assertCondition(StringUtils.hasContent(table.getBackendName()),
"Missing backend name for table " + tableName + "."))
{
if(CollectionUtils.nullSafeHasContents(qInstance.getBackends()))
{
assertCondition(errors, qInstance.getBackendForTable(tableName) != null, "Unrecognized backend " + table.getBackendName() + " for table " + tableName + ".");
assertCondition(qInstance.getBackendForTable(tableName) != null, "Unrecognized backend " + table.getBackendName() + " for table " + tableName + ".");
}
}
//////////////////////////////////
// validate fields in the table //
//////////////////////////////////
if(assertCondition(errors, CollectionUtils.nullSafeHasContents(table.getFields()), "At least 1 field must be defined in table " + tableName + "."))
if(assertCondition(CollectionUtils.nullSafeHasContents(table.getFields()), "At least 1 field must be defined in table " + tableName + "."))
{
table.getFields().forEach((fieldName, field) ->
{
assertCondition(errors, Objects.equals(fieldName, field.getName()),
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
if(field.getPossibleValueSourceName() != null)
{
assertCondition(errors, qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
}
});
@ -189,10 +213,10 @@ public class QInstanceValidator
{
for(QFieldSection section : table.getSections())
{
validateSection(errors, table, section, fieldNamesInSections);
validateSection(table, section, fieldNamesInSections);
if(section.getTier().equals(Tier.T1))
{
assertCondition(errors, tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
tier1Section = section;
}
}
@ -202,7 +226,7 @@ public class QInstanceValidator
{
for(String fieldName : table.getFields().keySet())
{
assertCondition(errors, fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections.");
assertCondition(fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections.");
}
}
@ -213,7 +237,7 @@ public class QInstanceValidator
{
for(String recordLabelField : table.getRecordLabelFields())
{
assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table.");
assertCondition(table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table.");
}
}
@ -221,9 +245,17 @@ public class QInstanceValidator
{
for(Map.Entry<String, QCodeReference> entry : table.getCustomizers().entrySet())
{
validateTableCustomizer(errors, tableName, entry.getKey(), entry.getValue());
validateTableCustomizer(tableName, entry.getKey(), entry.getValue());
}
}
//////////////////////////////////////
// validate the table's automations //
//////////////////////////////////////
if(table.getAutomationDetails() != null)
{
validateTableAutomationDetails(qInstance, table);
}
});
}
}
@ -233,11 +265,112 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateTableCustomizer(List<String> errors, String tableName, String customizerName, QCodeReference codeReference)
private void validateTableAutomationDetails(QInstance qInstance, QTableMetaData table)
{
String tableName = table.getName();
String prefix = "Table " + tableName + " automationDetails ";
QTableAutomationDetails automationDetails = table.getAutomationDetails();
//////////////////////////////////////
// validate the automation provider //
//////////////////////////////////////
String providerName = automationDetails.getProviderName();
if(assertCondition(StringUtils.hasContent(providerName), prefix + " is missing a providerName"))
{
assertCondition(qInstance.getAutomationProvider(providerName) != null, " has an unrecognized providerName: " + providerName);
}
//////////////////////////////////
// validate the status tracking //
//////////////////////////////////
AutomationStatusTracking statusTracking = automationDetails.getStatusTracking();
if(assertCondition(statusTracking != null, prefix + "do not have statusTracking defined."))
{
if(assertCondition(statusTracking.getType() != null, prefix + "statusTracking is missing a type"))
{
if(statusTracking.getType().equals(AutomationStatusTrackingType.FIELD_IN_TABLE))
{
assertCondition(StringUtils.hasContent(statusTracking.getFieldName()), prefix + "statusTracking of type fieldInTable is missing its fieldName");
}
}
}
//////////////////////////
// validate the actions //
//////////////////////////
Set<String> usedNames = new HashSet<>();
if(automationDetails.getActions() != null)
{
automationDetails.getActions().forEach(action ->
{
assertCondition(StringUtils.hasContent(action.getName()), prefix + "has an action missing a name");
assertCondition(!usedNames.contains(action.getName()), prefix + "has more than one action named " + action.getName());
usedNames.add(action.getName());
String actionPrefix = prefix + "action " + action.getName() + " ";
assertCondition(action.getTriggerEvent() != null, actionPrefix + "is missing a triggerEvent");
/////////////////////////////////////////////////////
// validate the code or process used by the action //
/////////////////////////////////////////////////////
int numberSet = 0;
if(action.getCodeReference() != null)
{
numberSet++;
if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix))
{
validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandler.class);
}
}
if(action.getProcessName() != null)
{
numberSet++;
QProcessMetaData process = qInstance.getProcess(action.getProcessName());
if(assertCondition(process != null, actionPrefix + "has an unrecognized processName: " + action.getProcessName()))
{
if(process.getTableName() != null)
{
assertCondition(tableName.equals(process.getTableName()), actionPrefix + " references a process from a different table");
}
}
}
assertCondition(numberSet != 0, actionPrefix + "is missing both a codeReference and a processName");
assertCondition(!(numberSet > 1), actionPrefix + "has both a codeReference and a processName (which is not allowed)");
///////////////////////////////////////////
// validate the filter (if there is one) //
///////////////////////////////////////////
if(action.getFilter() != null && action.getFilter().getCriteria() != null)
{
action.getFilter().getCriteria().forEach((criteria) ->
{
if(assertCondition(StringUtils.hasContent(criteria.getFieldName()), actionPrefix + "has a filter criteria without a field name"))
{
assertNoException(() -> table.getField(criteria.getFieldName()), actionPrefix + "has a filter criteria referencing an unrecognized field: " + criteria.getFieldName());
}
assertCondition(criteria.getOperator() != null, actionPrefix + "has a filter criteria without an operator");
// todo - validate cardinality of values...
});
}
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTableCustomizer(String tableName, String customizerName, QCodeReference codeReference)
{
String prefix = "Table " + tableName + ", customizer " + customizerName + ": ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
if(!preAssertionsForCodeReference(codeReference, prefix))
{
return;
}
@ -245,18 +378,18 @@ public class QInstanceValidator
//////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks //
//////////////////////////////////////////////////////////////////////////////
if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
if(assertCondition(codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
{
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix);
Class<?> customizerClass = getClassForCodeReference(codeReference, prefix);
if(customizerClass != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass);
Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName);
if(tableCustomizer == null)
@ -273,7 +406,7 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null)
{
Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance);
Object castedObject = getCastedObject(prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance);
Consumer<Object> validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction();
if(castedObject != null && validationFunction != null)
@ -305,7 +438,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private <T> T getCastedObject(List<String> errors, String prefix, Class<T> expectedType, Object customizerInstance)
private <T> T getCastedObject(String prefix, Class<T> expectedType, Object customizerInstance)
{
T castedObject = null;
try
@ -314,7 +447,7 @@ public class QInstanceValidator
}
catch(ClassCastException e)
{
errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType);
errors.add(prefix + "CodeReference is not of the expected type: " + expectedType);
}
return castedObject;
}
@ -324,7 +457,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private Object getInstanceOfCodeReference(List<String> errors, String prefix, Class<?> customizerClass)
private Object getInstanceOfCodeReference(String prefix, Class<?> customizerClass)
{
Object customizerInstance = null;
try
@ -343,18 +476,18 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateSection(List<String> errors, QTableMetaData table, QFieldSection section, Set<String> fieldNamesInSections)
private void validateSection(QTableMetaData table, QFieldSection section, Set<String> fieldNamesInSections)
{
assertCondition(errors, StringUtils.hasContent(section.getName()), "Missing a name for field section in table " + table.getName() + ".");
assertCondition(errors, StringUtils.hasContent(section.getLabel()), "Missing a label for field section in table " + table.getLabel() + ".");
if(assertCondition(errors, CollectionUtils.nullSafeHasContents(section.getFieldNames()), "Table " + table.getName() + " section " + section.getName() + " does not have any fields."))
assertCondition(StringUtils.hasContent(section.getName()), "Missing a name for field section in table " + table.getName() + ".");
assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for field section in table " + table.getLabel() + ".");
if(assertCondition(CollectionUtils.nullSafeHasContents(section.getFieldNames()), "Table " + table.getName() + " section " + section.getName() + " does not have any fields."))
{
if(table.getFields() != null)
{
for(String fieldName : section.getFieldNames())
{
assertCondition(errors, table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table.");
assertCondition(errors, !fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections.");
assertCondition(table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table.");
assertCondition(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections.");
fieldNamesInSections.add(fieldName);
}
@ -367,33 +500,33 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateProcesses(QInstance qInstance, List<String> errors)
private void validateProcesses(QInstance qInstance)
{
if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses()))
{
qInstance.getProcesses().forEach((processName, process) ->
{
assertCondition(errors, Objects.equals(processName, process.getName()), "Inconsistent naming for process: " + processName + "/" + process.getName() + ".");
assertCondition(Objects.equals(processName, process.getName()), "Inconsistent naming for process: " + processName + "/" + process.getName() + ".");
validateAppChildHasValidParentAppName(qInstance, errors, process);
validateAppChildHasValidParentAppName(qInstance, process);
/////////////////////////////////////////////
// validate the table name for the process //
/////////////////////////////////////////////
if(process.getTableName() != null)
{
assertCondition(errors, qInstance.getTable(process.getTableName()) != null, "Unrecognized table " + process.getTableName() + " for process " + processName + ".");
assertCondition(qInstance.getTable(process.getTableName()) != null, "Unrecognized table " + process.getTableName() + " for process " + processName + ".");
}
///////////////////////////////////
// validate steps in the process //
///////////////////////////////////
if(assertCondition(errors, CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
{
int index = 0;
for(QStepMetaData step : process.getStepList())
{
assertCondition(errors, StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName);
assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName);
index++;
}
}
@ -406,26 +539,26 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateApps(QInstance qInstance, List<String> errors)
private void validateApps(QInstance qInstance)
{
if(CollectionUtils.nullSafeHasContents(qInstance.getApps()))
{
qInstance.getApps().forEach((appName, app) ->
{
assertCondition(errors, Objects.equals(appName, app.getName()), "Inconsistent naming for app: " + appName + "/" + app.getName() + ".");
assertCondition(Objects.equals(appName, app.getName()), "Inconsistent naming for app: " + appName + "/" + app.getName() + ".");
validateAppChildHasValidParentAppName(qInstance, errors, app);
validateAppChildHasValidParentAppName(qInstance, app);
Set<String> appsVisited = new HashSet<>();
visitAppCheckingForCycles(app, appsVisited, errors);
visitAppCheckingForCycles(app, appsVisited);
if(app.getChildren() != null)
{
Set<String> childNames = new HashSet<>();
for(QAppChildMetaData child : app.getChildren())
{
assertCondition(errors, Objects.equals(appName, child.getParentAppName()), "Child " + child.getName() + " of app " + appName + " does not have its parent app properly set.");
assertCondition(errors, !childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName());
assertCondition(Objects.equals(appName, child.getParentAppName()), "Child " + child.getName() + " of app " + appName + " does not have its parent app properly set.");
assertCondition(!childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName());
childNames.add(child.getName());
}
}
@ -438,14 +571,14 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validatePossibleValueSources(QInstance qInstance, List<String> errors)
private void validatePossibleValueSources(QInstance qInstance)
{
if(CollectionUtils.nullSafeHasContents(qInstance.getPossibleValueSources()))
{
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
{
assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// assert about fields that should and should not be set, based on possible value source type //
@ -455,30 +588,30 @@ public class QInstanceValidator
{
case ENUM ->
{
assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
assertCondition(errors, CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
}
case TABLE ->
{
assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
if(assertCondition(errors, StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
{
assertCondition(errors, qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
assertCondition(qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
}
}
case CUSTOM ->
{
assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
{
assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider.");
validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference());
assertCondition(QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider.");
validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
@ -493,11 +626,9 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateCustomPossibleValueSourceCode(List<String> errors, String pvsName, QCodeReference codeReference)
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?> expectedClass)
{
String prefix = "PossibleValueSource " + pvsName + " custom code reference: ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
if(!preAssertionsForCodeReference(codeReference, prefix))
{
return;
}
@ -505,25 +636,25 @@ public class QInstanceValidator
//////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks //
//////////////////////////////////////////////////////////////////////////////
if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
if(assertCondition(codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA code references are supported at this time."))
{
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix);
Class<?> customizerClass = getClassForCodeReference(codeReference, prefix);
if(customizerClass != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass);
Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null)
{
getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance);
getCastedObject(prefix, expectedClass, customizerInstance);
}
}
}
@ -534,7 +665,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private Class<?> getClassForCodeReference(List<String> errors, QCodeReference codeReference, String prefix)
private Class<?> getClassForCodeReference(QCodeReference codeReference, String prefix)
{
Class<?> customizerClass = null;
try
@ -553,15 +684,15 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private boolean preAssertionsForCodeReference(List<String> errors, QCodeReference codeReference, String prefix)
private boolean preAssertionsForCodeReference(QCodeReference codeReference, String prefix)
{
boolean okay = true;
if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name"))
if(!assertCondition(StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name"))
{
okay = false;
}
if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type"))
if(!assertCondition(codeReference.getCodeType() != null, prefix + " is missing a code type"))
{
okay = false;
}
@ -575,9 +706,9 @@ public class QInstanceValidator
** Check if an app's child list can recursively be traversed without finding a
** duplicate, which would indicate a cycle (e.g., an error)
*******************************************************************************/
private void visitAppCheckingForCycles(QAppMetaData app, Set<String> appsVisited, List<String> errors)
private void visitAppCheckingForCycles(QAppMetaData app, Set<String> appsVisited)
{
if(assertCondition(errors, !appsVisited.contains(app.getName()), "Circular app reference detected, involving " + app.getName()))
if(assertCondition(!appsVisited.contains(app.getName()), "Circular app reference detected, involving " + app.getName()))
{
appsVisited.add(app.getName());
if(app.getChildren() != null)
@ -586,7 +717,7 @@ public class QInstanceValidator
{
if(child instanceof QAppMetaData childApp)
{
visitAppCheckingForCycles(childApp, appsVisited, errors);
visitAppCheckingForCycles(childApp, appsVisited);
}
}
}
@ -598,11 +729,11 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateAppChildHasValidParentAppName(QInstance qInstance, List<String> errors, QAppChildMetaData appChild)
private void validateAppChildHasValidParentAppName(QInstance qInstance, QAppChildMetaData appChild)
{
if(appChild.getParentAppName() != null)
{
assertCondition(errors, qInstance.getApp(appChild.getParentAppName()) != null, "Unrecognized parent app " + appChild.getParentAppName() + " for " + appChild.getName() + ".");
assertCondition(qInstance.getApp(appChild.getParentAppName()) != null, "Unrecognized parent app " + appChild.getParentAppName() + " for " + appChild.getName() + ".");
}
}
@ -613,7 +744,7 @@ public class QInstanceValidator
** But if it's false, add the provided message to the list of errors (and return false,
** e.g., in case you need to stop evaluating rules to avoid exceptions).
*******************************************************************************/
private boolean assertCondition(List<String> errors, boolean condition, String message)
private boolean assertCondition(boolean condition, String message)
{
if(!condition)
{
@ -625,6 +756,41 @@ public class QInstanceValidator
/*******************************************************************************
** For the given lambda, if it doesn't throw an exception, then we're all good (and return true).
** But if it throws, add the provided message to the list of errors (and return false,
** e.g., in case you need to stop evaluating rules to avoid exceptions).
*******************************************************************************/
private boolean assertNoException(UnsafeLambda unsafeLambda, String message)
{
try
{
unsafeLambda.run();
return (true);
}
catch(Exception e)
{
errors.add(message);
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
@FunctionalInterface
interface UnsafeLambda
{
/*******************************************************************************
**
*******************************************************************************/
void run() throws Exception;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,138 @@
package com.kingsrook.qqq.backend.core.model.automation;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
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.automation.TableAutomationAction;
/*******************************************************************************
** Input data for the RecordAutomationHandler interface.
*******************************************************************************/
public class RecordAutomationInput extends AbstractTableActionInput
{
private TableAutomationAction action;
////////////////////////////////////////////
// todo - why both? pick one? or don't? //
// maybe - if recordList is null and primaryKeyList isn't, then do the record query in here?
////////////////////////////////////////////
private List<QRecord> recordList;
private List<Serializable> primaryKeyList;
/*******************************************************************************
**
*******************************************************************************/
public RecordAutomationInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for action
**
*******************************************************************************/
public TableAutomationAction getAction()
{
return action;
}
/*******************************************************************************
** Setter for action
**
*******************************************************************************/
public void setAction(TableAutomationAction action)
{
this.action = action;
}
/*******************************************************************************
** Fluent setter for action
**
*******************************************************************************/
public RecordAutomationInput withAction(TableAutomationAction action)
{
this.action = action;
return (this);
}
/*******************************************************************************
** Getter for recordList
**
*******************************************************************************/
public List<QRecord> getRecordList()
{
return recordList;
}
/*******************************************************************************
** Setter for recordList
**
*******************************************************************************/
public void setRecordList(List<QRecord> recordList)
{
this.recordList = recordList;
}
/*******************************************************************************
** Fluent setter for recordList
**
*******************************************************************************/
public RecordAutomationInput withRecordList(List<QRecord> recordList)
{
this.recordList = recordList;
return (this);
}
/*******************************************************************************
** Getter for primaryKeyList
**
*******************************************************************************/
public List<Serializable> getPrimaryKeyList()
{
return primaryKeyList;
}
/*******************************************************************************
** Setter for primaryKeyList
**
*******************************************************************************/
public void setPrimaryKeyList(List<Serializable> primaryKeyList)
{
this.primaryKeyList = primaryKeyList;
}
/*******************************************************************************
** Fluent setter for primaryKeyList
**
*******************************************************************************/
public RecordAutomationInput withPrimaryKeyList(List<Serializable> primaryKeyList)
{
this.primaryKeyList = primaryKeyList;
return (this);
}
}

View File

@ -26,7 +26,6 @@ package com.kingsrook.qqq.backend.core.model.metadata;
** Enum to define the possible authentication types
**
*******************************************************************************/
@SuppressWarnings("rawtypes")
public enum QAuthenticationType
{
AUTH_0("auth0"),

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -51,6 +52,7 @@ public class QInstance
private Map<String, QBackendMetaData> backends = new HashMap<>();
private QAuthenticationMetaData authentication = null;
private Map<String, QAutomationProviderMetaData> automationProviders = new HashMap<>();
////////////////////////////////////////////////////////////////////////////////////////////
// Important to use LinkedHashmap here, to preserve the order in which entries are added. //
@ -104,17 +106,6 @@ public class QInstance
/*******************************************************************************
** Setter for hasBeenValidated
**
*******************************************************************************/
public void setHasBeenValidated(QInstanceValidationKey key)
{
this.hasBeenValidated = true;
}
/*******************************************************************************
**
*******************************************************************************/
@ -153,6 +144,28 @@ public class QInstance
/*******************************************************************************
** Getter for backends
**
*******************************************************************************/
public Map<String, QBackendMetaData> getBackends()
{
return backends;
}
/*******************************************************************************
** Setter for backends
**
*******************************************************************************/
public void setBackends(Map<String, QBackendMetaData> backends)
{
this.backends = backends;
}
/*******************************************************************************
**
*******************************************************************************/
@ -196,6 +209,28 @@ public class QInstance
/*******************************************************************************
** Getter for tables
**
*******************************************************************************/
public Map<String, QTableMetaData> getTables()
{
return tables;
}
/*******************************************************************************
** Setter for tables
**
*******************************************************************************/
public void setTables(Map<String, QTableMetaData> tables)
{
this.tables = tables;
}
/*******************************************************************************
**
*******************************************************************************/
@ -234,6 +269,28 @@ public class QInstance
/*******************************************************************************
** Getter for possibleValueSources
**
*******************************************************************************/
public Map<String, QPossibleValueSource> getPossibleValueSources()
{
return possibleValueSources;
}
/*******************************************************************************
** Setter for possibleValueSources
**
*******************************************************************************/
public void setPossibleValueSources(Map<String, QPossibleValueSource> possibleValueSources)
{
this.possibleValueSources = possibleValueSources;
}
/*******************************************************************************
**
*******************************************************************************/
@ -288,6 +345,28 @@ public class QInstance
/*******************************************************************************
** Getter for processes
**
*******************************************************************************/
public Map<String, QProcessMetaData> getProcesses()
{
return processes;
}
/*******************************************************************************
** Setter for processes
**
*******************************************************************************/
public void setProcesses(Map<String, QProcessMetaData> processes)
{
this.processes = processes;
}
/*******************************************************************************
**
*******************************************************************************/
@ -326,94 +405,6 @@ public class QInstance
/*******************************************************************************
** Getter for backends
**
*******************************************************************************/
public Map<String, QBackendMetaData> getBackends()
{
return backends;
}
/*******************************************************************************
** Setter for backends
**
*******************************************************************************/
public void setBackends(Map<String, QBackendMetaData> backends)
{
this.backends = backends;
}
/*******************************************************************************
** Getter for tables
**
*******************************************************************************/
public Map<String, QTableMetaData> getTables()
{
return tables;
}
/*******************************************************************************
** Setter for tables
**
*******************************************************************************/
public void setTables(Map<String, QTableMetaData> tables)
{
this.tables = tables;
}
/*******************************************************************************
** Getter for possibleValueSources
**
*******************************************************************************/
public Map<String, QPossibleValueSource> getPossibleValueSources()
{
return possibleValueSources;
}
/*******************************************************************************
** Setter for possibleValueSources
**
*******************************************************************************/
public void setPossibleValueSources(Map<String, QPossibleValueSource> possibleValueSources)
{
this.possibleValueSources = possibleValueSources;
}
/*******************************************************************************
** Getter for processes
**
*******************************************************************************/
public Map<String, QProcessMetaData> getProcesses()
{
return processes;
}
/*******************************************************************************
** Setter for processes
**
*******************************************************************************/
public void setProcesses(Map<String, QProcessMetaData> processes)
{
this.processes = processes;
}
/*******************************************************************************
** Getter for apps
**
@ -436,6 +427,62 @@ public class QInstance
/*******************************************************************************
**
*******************************************************************************/
public void addAutomationProvider(QAutomationProviderMetaData automationProvider)
{
this.addAutomationProvider(automationProvider.getName(), automationProvider);
}
/*******************************************************************************
**
*******************************************************************************/
public void addAutomationProvider(String name, QAutomationProviderMetaData automationProvider)
{
if(this.automationProviders.containsKey(name))
{
throw (new IllegalArgumentException("Attempted to add a second automationProvider with name: " + name));
}
this.automationProviders.put(name, automationProvider);
}
/*******************************************************************************
**
*******************************************************************************/
public QAutomationProviderMetaData getAutomationProvider(String name)
{
return (this.automationProviders.get(name));
}
/*******************************************************************************
** Getter for automationProviders
**
*******************************************************************************/
public Map<String, QAutomationProviderMetaData> getAutomationProviders()
{
return automationProviders;
}
/*******************************************************************************
** Setter for automationProviders
**
*******************************************************************************/
public void setAutomationProviders(Map<String, QAutomationProviderMetaData> automationProviders)
{
this.automationProviders = automationProviders;
}
/*******************************************************************************
** Getter for hasBeenValidated
**
@ -447,6 +494,17 @@ public class QInstance
/*******************************************************************************
** Setter for hasBeenValidated
**
*******************************************************************************/
public void setHasBeenValidated(QInstanceValidationKey key)
{
this.hasBeenValidated = true;
}
/*******************************************************************************
** Getter for authentication
**

View File

@ -0,0 +1,43 @@
package com.kingsrook.qqq.backend.core.model.metadata.automation;
/*******************************************************************************
** Metadata specifically for the polling automation provider.
*******************************************************************************/
public class PollingAutomationProviderMetaData extends QAutomationProviderMetaData
{
/*******************************************************************************
**
*******************************************************************************/
public PollingAutomationProviderMetaData()
{
super();
setType(QAutomationProviderType.POLLING);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public PollingAutomationProviderMetaData withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public PollingAutomationProviderMetaData withType(QAutomationProviderType type)
{
setType(type);
return (this);
}
}

View File

@ -0,0 +1,77 @@
package com.kingsrook.qqq.backend.core.model.metadata.automation;
/*******************************************************************************
** Meta-data definition of a qqq service to drive record automations.
*******************************************************************************/
public class QAutomationProviderMetaData
{
private String name;
private QAutomationProviderType type;
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
/*******************************************************************************
** Setter for name
**
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
**
*******************************************************************************/
public QAutomationProviderMetaData withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public QAutomationProviderType getType()
{
return type;
}
/*******************************************************************************
** Setter for type
**
*******************************************************************************/
public void setType(QAutomationProviderType type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
**
*******************************************************************************/
public QAutomationProviderMetaData withType(QAutomationProviderType type)
{
this.type = type;
return (this);
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.model.metadata.automation;
/*******************************************************************************
** Enum to define the possible automation provider types
**
*******************************************************************************/
public enum QAutomationProviderType
{
POLLING("polling"),
SYNCHRONOUS("synchronous"),
ASYNCHRONOUS("asynchronous"),
MQ("mq"),
AMAZON_SQS("sqs");
private final String name;
/*******************************************************************************
** enum constructor
*******************************************************************************/
QAutomationProviderType(String name)
{
this.name = name;
}
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return this.name;
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.code;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
@ -60,6 +61,17 @@ public class QCodeReference implements Serializable
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "QCodeReference{name='" + name + "'}";
}
/*******************************************************************************
** Constructor that just takes a java class, and infers the other fields.
*******************************************************************************/
@ -76,6 +88,10 @@ public class QCodeReference implements Serializable
{
this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER;
}
else if(RecordAutomationHandler.class.isAssignableFrom(javaClass))
{
this.codeUsage = QCodeUsage.RECORD_AUTOMATION_HANDLER;
}
else
{
throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName()));

View File

@ -30,5 +30,6 @@ public enum QCodeUsage
{
BACKEND_STEP, // a backend-step in a process
CUSTOMIZER, // a function to customize part of a QQQ table's behavior
POSSIBLE_VALUE_PROVIDER // code that drives a custom possibleValueSource
POSSIBLE_VALUE_PROVIDER, // code that drives a custom possibleValueSource
RECORD_AUTOMATION_HANDLER // code that executes record automations
}

View File

@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
/*******************************************************************************
@ -64,6 +65,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
private Map<String, QFieldMetaData> fields;
private QTableBackendDetails backendDetails;
private QTableAutomationDetails automationDetails;
private Map<String, QCodeReference> customizers;
@ -410,6 +412,40 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
/*******************************************************************************
** Getter for automationDetails
**
*******************************************************************************/
public QTableAutomationDetails getAutomationDetails()
{
return automationDetails;
}
/*******************************************************************************
** Setter for automationDetails
**
*******************************************************************************/
public void setAutomationDetails(QTableAutomationDetails automationDetails)
{
this.automationDetails = automationDetails;
}
/*******************************************************************************
** Fluent Setter for automationDetails
**
*******************************************************************************/
public QTableMetaData withAutomationDetails(QTableAutomationDetails automationDetails)
{
this.automationDetails = automationDetails;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,84 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
/*******************************************************************************
** Table-automation meta-data to define how this table's per-record automation
** status is tracked.
*******************************************************************************/
public class AutomationStatusTracking
{
private AutomationStatusTrackingType type;
private String fieldName; // used when type is FIELD_IN_TABLE
// todo - fields for additional types (e.g., 1-1 table, shared-table)
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public AutomationStatusTrackingType getType()
{
return type;
}
/*******************************************************************************
** Setter for type
**
*******************************************************************************/
public void setType(AutomationStatusTrackingType type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
**
*******************************************************************************/
public AutomationStatusTracking withType(AutomationStatusTrackingType type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for fieldName
**
*******************************************************************************/
public String getFieldName()
{
return fieldName;
}
/*******************************************************************************
** Setter for fieldName
**
*******************************************************************************/
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
/*******************************************************************************
** Fluent setter for fieldName
**
*******************************************************************************/
public AutomationStatusTracking withFieldName(String fieldName)
{
this.fieldName = fieldName;
return (this);
}
}

View File

@ -0,0 +1,11 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
/*******************************************************************************
** Enum of possible types of per-table, per-record automation-status tracking.
*******************************************************************************/
public enum AutomationStatusTrackingType
{
FIELD_IN_TABLE
// todo - additional types (e.g., 1-1 table, shared-table)
}

View File

@ -0,0 +1,133 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
import java.util.ArrayList;
import java.util.List;
/*******************************************************************************
** Details about how this table's record automations are set up.
*******************************************************************************/
public class QTableAutomationDetails
{
private AutomationStatusTracking statusTracking;
private String providerName;
private List<TableAutomationAction> actions;
/*******************************************************************************
** Getter for statusTracking
**
*******************************************************************************/
public AutomationStatusTracking getStatusTracking()
{
return statusTracking;
}
/*******************************************************************************
** Setter for statusTracking
**
*******************************************************************************/
public void setStatusTracking(AutomationStatusTracking statusTracking)
{
this.statusTracking = statusTracking;
}
/*******************************************************************************
** Fluent setter for statusTracking
**
*******************************************************************************/
public QTableAutomationDetails withStatusTracking(AutomationStatusTracking statusTracking)
{
this.statusTracking = statusTracking;
return (this);
}
/*******************************************************************************
** Getter for providerName
**
*******************************************************************************/
public String getProviderName()
{
return providerName;
}
/*******************************************************************************
** Setter for providerName
**
*******************************************************************************/
public void setProviderName(String providerName)
{
this.providerName = providerName;
}
/*******************************************************************************
** Fluent setter for providerName
**
*******************************************************************************/
public QTableAutomationDetails withProviderName(String providerName)
{
this.providerName = providerName;
return (this);
}
/*******************************************************************************
** Getter for actions
**
*******************************************************************************/
public List<TableAutomationAction> getActions()
{
return actions;
}
/*******************************************************************************
** Setter for actions
**
*******************************************************************************/
public void setActions(List<TableAutomationAction> actions)
{
this.actions = actions;
}
/*******************************************************************************
** Fluent setter for actions
**
*******************************************************************************/
public QTableAutomationDetails withActions(List<TableAutomationAction> actions)
{
this.actions = actions;
return (this);
}
/*******************************************************************************
** Fluently add an action to this table's automations.
*******************************************************************************/
public QTableAutomationDetails withAction(TableAutomationAction action)
{
if(this.actions == null)
{
this.actions = new ArrayList<>();
}
this.actions.add(action);
return (this);
}
}

View File

@ -0,0 +1,232 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Definition of a specific action to run against a table
*******************************************************************************/
public class TableAutomationAction
{
private String name;
private TriggerEvent triggerEvent;
private Integer priority = 500;
private QQueryFilter filter;
////////////////////////////////
// mutually-exclusive options //
////////////////////////////////
private QCodeReference codeReference;
private String processName;
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "TableAutomationAction{name='" + name + "'}";}
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
/*******************************************************************************
** Setter for name
**
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
**
*******************************************************************************/
public TableAutomationAction withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for triggerEvent
**
*******************************************************************************/
public TriggerEvent getTriggerEvent()
{
return triggerEvent;
}
/*******************************************************************************
** Setter for triggerEvent
**
*******************************************************************************/
public void setTriggerEvent(TriggerEvent triggerEvent)
{
this.triggerEvent = triggerEvent;
}
/*******************************************************************************
** Fluent setter for triggerEvent
**
*******************************************************************************/
public TableAutomationAction withTriggerEvent(TriggerEvent triggerEvent)
{
this.triggerEvent = triggerEvent;
return (this);
}
/*******************************************************************************
** Getter for priority
**
*******************************************************************************/
public Integer getPriority()
{
return priority;
}
/*******************************************************************************
** Setter for priority
**
*******************************************************************************/
public void setPriority(Integer priority)
{
this.priority = priority;
}
/*******************************************************************************
** Fluent setter for priority
**
*******************************************************************************/
public TableAutomationAction withPriority(Integer priority)
{
this.priority = priority;
return (this);
}
/*******************************************************************************
** Getter for filter
**
*******************************************************************************/
public QQueryFilter getFilter()
{
return filter;
}
/*******************************************************************************
** Setter for filter
**
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
**
*******************************************************************************/
public TableAutomationAction withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for codeReference
**
*******************************************************************************/
public QCodeReference getCodeReference()
{
return codeReference;
}
/*******************************************************************************
** Setter for codeReference
**
*******************************************************************************/
public void setCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
**
*******************************************************************************/
public TableAutomationAction withCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Getter for processName
**
*******************************************************************************/
public String getProcessName()
{
return processName;
}
/*******************************************************************************
** Setter for processName
**
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
**
*******************************************************************************/
public TableAutomationAction withProcessName(String processName)
{
this.processName = processName;
return (this);
}
}

View File

@ -0,0 +1,12 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
/*******************************************************************************
** Possible events that can trigger a record automation.
*******************************************************************************/
public enum TriggerEvent
{
POST_INSERT,
POST_UPDATE,
PRE_DELETE
}

View File

@ -0,0 +1,233 @@
package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.automation.RecordAutomationInput;
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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for PollingAutomationExecutor
*******************************************************************************/
class PollingAutomationExecutorTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfterEach()
{
MemoryRecordStore.getInstance().reset();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsert() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
/////////////////////////////////////////////////////////////////////////////
// insert 2 people - one who should be updated by the check-age automation //
/////////////////////////////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(new QSession());
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("firstName", "John").withValue("birthDate", LocalDate.of(1970, Month.JANUARY, 1)),
new QRecord().withValue("id", 2).withValue("firstName", "Jim").withValue("birthDate", LocalDate.now().minusDays(30))
));
new InsertAction().execute(insertInput);
////////////////////////////////////////////////
// have the polling executor run "for awhile" //
////////////////////////////////////////////////
runPollingAutomationExecutorForAwhile(qInstance);
/////////////////////////////////////////////////
// query for the records - assert their status //
/////////////////////////////////////////////////
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(new QSession());
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
Optional<QRecord> optionalPerson1 = queryOutput.getRecords().stream().filter(r -> r.getValueInteger("id") == 1).findFirst();
assertThat(optionalPerson1).isPresent();
QRecord person1 = optionalPerson1.get();
assertThat(person1.getValueString("firstName")).isEqualTo("John");
assertThat(person1.getValueInteger(TestUtils.standardQqqAutomationStatusField().getName())).isEqualTo(AutomationStatus.OK.getId());
Optional<QRecord> optionalPerson2 = queryOutput.getRecords().stream().filter(r -> r.getValueInteger("id") == 2).findFirst();
assertThat(optionalPerson2).isPresent();
QRecord person2 = optionalPerson2.get();
assertThat(person2.getValueString("firstName")).isEqualTo("Jim" + TestUtils.CheckAge.SUFFIX_FOR_MINORS);
assertThat(person2.getValueInteger(TestUtils.standardQqqAutomationStatusField().getName())).isEqualTo(AutomationStatus.OK.getId());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUpdate() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
///////////////////////////////////////////////////////////////////////////////
// insert 2 people - one who should be logged by logger-on-update automation //
///////////////////////////////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(new QSession());
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("firstName", "Tim"),
new QRecord().withValue("id", 2).withValue("firstName", "Darin")
));
new InsertAction().execute(insertInput);
////////////////////////////////////////////////
// have the polling executor run "for awhile" //
////////////////////////////////////////////////
runPollingAutomationExecutorForAwhile(qInstance);
//////////////////////////////////////////////////
// assert that the update-automation didn't run //
//////////////////////////////////////////////////
assertThat(TestUtils.LogPersonUpdate.updatedIds).isNullOrEmpty();
UpdateInput updateInput = new UpdateInput(qInstance);
updateInput.setSession(new QSession());
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
updateInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("lastName", "now with a LastName"),
new QRecord().withValue("id", 2).withValue("lastName", "now with a LastName")
));
new UpdateAction().execute(updateInput);
////////////////////////////////////////////////
// have the polling executor run "for awhile" //
////////////////////////////////////////////////
runPollingAutomationExecutorForAwhile(qInstance);
///////////////////////////////////////////////////
// assert that the update-automation DID run now //
///////////////////////////////////////////////////
assertThat(TestUtils.LogPersonUpdate.updatedIds).contains(2);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSessionSupplier() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
//////////////////////////////////////////////////////////////////////
// make the person-memory table's insert-action run a class in here //
//////////////////////////////////////////////////////////////////////
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.getAutomationDetails().getActions().get(0)
.setCodeReference(new QCodeReference(CaptureSessionIdAutomationHandler.class));
/////////////////////
// insert a person //
/////////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(new QSession());
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("firstName", "Tim")
));
new InsertAction().execute(insertInput);
String uuid = UUID.randomUUID().toString();
QSession session = new QSession();
session.setIdReference(uuid);
PollingAutomationExecutor.getInstance().setSessionSupplier(() -> session);
////////////////////////////////////////////////
// have the polling executor run "for awhile" //
////////////////////////////////////////////////
runPollingAutomationExecutorForAwhile(qInstance);
/////////////////////////////////////////////////////////////////////////////////////////////////////
// assert that the uuid we put in our session was present in the CaptureSessionIdAutomationHandler //
/////////////////////////////////////////////////////////////////////////////////////////////////////
assertThat(CaptureSessionIdAutomationHandler.sessionId).isEqualTo(uuid);
}
/*******************************************************************************
**
*******************************************************************************/
public static class CaptureSessionIdAutomationHandler extends RecordAutomationHandler
{
static String sessionId;
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
sessionId = recordAutomationInput.getSession().getIdReference();
}
}
/*******************************************************************************
**
*******************************************************************************/
private void runPollingAutomationExecutorForAwhile(QInstance qInstance)
{
PollingAutomationExecutor pollingAutomationExecutor = PollingAutomationExecutor.getInstance();
pollingAutomationExecutor.setInitialDelayMillis(0);
pollingAutomationExecutor.setDelayMillis(100);
pollingAutomationExecutor.start(qInstance, TestUtils.POLLING_AUTOMATION);
SleepUtils.sleep(1, TimeUnit.SECONDS);
pollingAutomationExecutor.stop();
}
}

View File

@ -30,6 +30,9 @@ import java.util.function.Consumer;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
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.code.QCodeReference;
@ -43,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
@ -162,11 +166,13 @@ class QInstanceValidatorTest
qInstance.getBackend("default").setName("notDefault");
qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople");
qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates");
qInstance.getAutomationProvider(TestUtils.POLLING_AUTOMATION).setName("notPolling");
},
"Inconsistent naming for table",
"Inconsistent naming for backend",
"Inconsistent naming for process",
"Inconsistent naming for possibleValueSource"
"Inconsistent naming for possibleValueSource",
"Inconsistent naming for automationProvider"
);
}
@ -296,7 +302,7 @@ class QInstanceValidatorTest
"Instance of CodeReference could not be created");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)),
"CodeReference could not be casted");
"CodeReference is not of the expected type");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)),
"Error validating customizer type parameters");
@ -649,6 +655,230 @@ class QInstanceValidatorTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAutomationProviderType()
{
assertValidationFailureReasons((qInstance) -> qInstance.getAutomationProvider(TestUtils.POLLING_AUTOMATION).setType(null),
"Missing type for automationProvider");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationProviderName()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setProviderName(null),
"is missing a providerName");
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setProviderName(""),
"is missing a providerName");
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setProviderName("notARealProvider"),
"unrecognized providerName");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationStatusTracking()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setStatusTracking(null),
"do not have statusTracking");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationStatusTrackingType()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getStatusTracking().setType(null),
"statusTracking is missing a type");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationStatusTrackingFieldName()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getStatusTracking().setFieldName(null),
"missing its fieldName");
assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getStatusTracking().setFieldName(""),
"missing its fieldName");
//////////////////////////////////////////////////
// todo - make sure it's a field in the table?? //
//////////////////////////////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationActionsNames()
{
assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setName(null),
"action missing a name");
assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setName(""),
"action missing a name");
assertValidationFailureReasons((qInstance) ->
{
List<TableAutomationAction> actions = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getActions();
actions.add(actions.get(0));
},
"more than one action named");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationActionTriggerEvent()
{
assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setTriggerEvent(null),
"missing a triggerEvent");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationActionCodeReference()
{
assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setCodeReference(new QCodeReference()),
"missing a code reference name", "missing a code type");
assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setCodeReference(new QCodeReference(TestUtils.CustomPossibleValueSource.class)),
"is not of the expected type");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationActionProcessName()
{
assertValidationFailureReasons((qInstance) ->
{
TableAutomationAction action = getAction0(qInstance);
action.setCodeReference(null);
action.setProcessName("notAProcess");
},
"unrecognized processName");
assertValidationSuccess((qInstance) ->
{
qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
TableAutomationAction action = getAction0(qInstance);
action.setCodeReference(null);
action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
});
assertValidationFailureReasons((qInstance) ->
{
TableAutomationAction action = getAction0(qInstance);
action.setCodeReference(null);
action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
},
"different table");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationActionCodeReferenceAndProcessName()
{
assertValidationFailureReasons((qInstance) ->
{
TableAutomationAction action = getAction0(qInstance);
action.setCodeReference(null);
action.setProcessName(null);
},
"missing both");
assertValidationFailureReasons((qInstance) ->
{
qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
TableAutomationAction action = getAction0(qInstance);
action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
},
"has both");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableAutomationActionFilter()
{
assertValidationFailureReasons((qInstance) ->
{
TableAutomationAction action = getAction0(qInstance);
action.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria())
);
},
"without a field name", "without an operator");
assertValidationFailureReasons((qInstance) ->
{
TableAutomationAction action = getAction0(qInstance);
action.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("notAField", QCriteriaOperator.EQUALS, Collections.emptyList()))
);
},
"unrecognized field");
assertValidationSuccess((qInstance) ->
{
TableAutomationAction action = getAction0(qInstance);
action.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(1701)))
);
});
}
/*******************************************************************************
**
*******************************************************************************/
private TableAutomationAction getAction0(QInstance qInstance)
{
return qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getActions().get(0);
}
/*******************************************************************************
** Run a little setup code on a qInstance; then validate it, and assert that it
** failed validation with reasons that match the supplied vararg-reasons (but allow
@ -690,7 +920,8 @@ class QInstanceValidatorTest
if(!allowExtraReasons)
{
int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size();
assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + "\nActual reasons: " + e.getReasons());
assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons)
+ "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", e.getReasons()) : "--"));
}
for(String reason : reasons)

View File

@ -23,26 +23,39 @@ package com.kingsrook.qqq.backend.core.utils;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
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.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.automation.PollingAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
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.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
@ -52,7 +65,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
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.core.model.metadata.tables.automation.AutomationStatusTracking;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
@ -61,6 +79,8 @@ import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockB
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -69,6 +89,8 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend
*******************************************************************************/
public class TestUtils
{
private static final Logger LOG = LogManager.getLogger(TestUtils.class);
public static final String DEFAULT_BACKEND_NAME = "default";
public static final String MEMORY_BACKEND_NAME = "memory";
@ -83,11 +105,15 @@ public class TestUtils
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge";
public static final String TABLE_NAME_PERSON_FILE = "personFile";
public static final String TABLE_NAME_PERSON_MEMORY = "personMemory";
public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly";
public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type
public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type
public static final String POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS = "automationStatus";
public static final String POLLING_AUTOMATION = "polling";
@ -104,9 +130,11 @@ public class TestUtils
qInstance.addTable(defineTablePerson());
qInstance.addTable(definePersonFileTable());
qInstance.addTable(definePersonMemoryTable());
qInstance.addTable(defineTableIdAndNameOnly());
qInstance.addTable(defineTableShape());
qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
qInstance.addPossibleValueSource(defineShapePossibleValueSource());
qInstance.addPossibleValueSource(defineCustomPossibleValueSource());
@ -117,6 +145,8 @@ public class TestUtils
qInstance.addProcess(new BasicETLProcess().defineProcessMetaData());
qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData());
qInstance.addAutomationProvider(definePollingAutomationProvider());
defineApps(qInstance);
return (qInstance);
@ -124,6 +154,18 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
private static QAutomationProviderMetaData definePollingAutomationProvider()
{
return (new PollingAutomationProviderMetaData()
.withName(POLLING_AUTOMATION)
);
}
/*******************************************************************************
**
*******************************************************************************/
@ -148,6 +190,21 @@ public class TestUtils
/*******************************************************************************
** Define the "automationStatus" possible value source used in standard tests
**
*******************************************************************************/
private static QPossibleValueSource defineAutomationStatusPossibleValueSource()
{
return new QPossibleValueSource()
.withName(POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS)
.withType(QPossibleValueSourceType.ENUM)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY)
.withValuesFromEnum(AutomationStatus.values());
}
/*******************************************************************************
** Define the "states" possible value source used in standard tests
**
@ -252,6 +309,30 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
public static QFieldMetaData standardQqqAutomationStatusField()
{
return (new QFieldMetaData("qqqAutomationStatus", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS));
}
/*******************************************************************************
**
*******************************************************************************/
private static QTableAutomationDetails defineStandardAutomationDetails()
{
return (new QTableAutomationDetails()
.withProviderName(POLLING_AUTOMATION)
.withStatusTracking(new AutomationStatusTracking()
.withType(AutomationStatusTrackingType.FIELD_IN_TABLE)
.withFieldName("qqqAutomationStatus")));
}
/*******************************************************************************
** Define the 'shape' table used in standard tests.
*******************************************************************************/
@ -289,6 +370,101 @@ public class TestUtils
/*******************************************************************************
** Define a 3nd version of the 'person' table, backed by the in-memory backend
*******************************************************************************/
public static QTableMetaData definePersonMemoryTable()
{
return (new QTableMetaData()
.withName(TABLE_NAME_PERSON_MEMORY)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withFields(TestUtils.defineTablePerson().getFields()))
.withField(standardQqqAutomationStatusField())
.withAutomationDetails(defineStandardAutomationDetails()
.withAction(new TableAutomationAction()
.withName("checkAgeOnInsert")
.withTriggerEvent(TriggerEvent.POST_INSERT)
.withCodeReference(new QCodeReference(CheckAge.class))
)
.withAction(new TableAutomationAction()
.withName("logOnUpdatePerFilter")
.withTriggerEvent(TriggerEvent.POST_UPDATE)
.withFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Darin"))))
.withCodeReference(new QCodeReference(LogPersonUpdate.class))
)
);
}
/*******************************************************************************
**
*******************************************************************************/
public static class CheckAge extends RecordAutomationHandler
{
public static String SUFFIX_FOR_MINORS = " (a minor)";
/*******************************************************************************
**
*******************************************************************************/
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
LocalDate limitDate = LocalDate.now().minusYears(18);
List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : recordAutomationInput.getRecordList())
{
LocalDate birthDate = record.getValueLocalDate("birthDate");
if(birthDate != null && birthDate.isAfter(limitDate))
{
LOG.info("Person [" + record.getValueInteger("id") + "] is a minor - updating their firstName to state such.");
recordsToUpdate.add(new QRecord()
.withValue("id", record.getValue("id"))
.withValue("firstName", record.getValueString("firstName") + SUFFIX_FOR_MINORS)
);
}
}
if(!recordsToUpdate.isEmpty())
{
UpdateInput updateInput = new UpdateInput(recordAutomationInput.getInstance());
updateInput.setSession(recordAutomationInput.getSession());
updateInput.setTableName(recordAutomationInput.getTableName());
updateInput.setRecords(recordsToUpdate);
new UpdateAction().execute(updateInput);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class LogPersonUpdate extends RecordAutomationHandler
{
public static List<Integer> updatedIds = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
for(QRecord record : recordAutomationInput.getRecordList())
{
updatedIds.add(record.getValueInteger("id"));
LOG.info("Person [" + record.getValueInteger("id") + ":" + record.getValueString("firstName") + "] has been updated.");
}
}
}
/*******************************************************************************
** Define simple table with just an id and name
*******************************************************************************/