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.Optional;
import java.util.function.Function; 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.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; 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.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; 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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
@ -48,6 +50,9 @@ public class InsertAction
*******************************************************************************/ *******************************************************************************/
public InsertOutput execute(InsertInput insertInput) throws QException public InsertOutput execute(InsertInput insertInput) throws QException
{ {
ActionHelper.validateSession(insertInput);
setAutomationStatusField(insertInput);
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput); QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request? // todo pre-customization - just get to modify the request?
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput); 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.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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
@ -42,6 +44,7 @@ public class UpdateAction
public UpdateOutput execute(UpdateInput updateInput) throws QException public UpdateOutput execute(UpdateInput updateInput) throws QException
{ {
ActionHelper.validateSession(updateInput); ActionHelper.validateSession(updateInput);
setAutomationStatusField(updateInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend()); 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 // todo post-customization - can do whatever w/ the result if you want
return updateResult; 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.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; 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.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; 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.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; 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.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.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; 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.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; 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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@ -65,6 +70,8 @@ public class QInstanceValidator
private boolean printWarnings = false; 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! // // do the validation checks - a good qInstance has all conditions TRUE! //
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
List<String> errors = new ArrayList<>();
try try
{ {
validateBackends(qInstance, errors); validateBackends(qInstance);
validateTables(qInstance, errors); validateAutomationProviders(qInstance);
validateProcesses(qInstance, errors); validateTables(qInstance);
validateApps(qInstance, errors); validateProcesses(qInstance);
validatePossibleValueSources(qInstance, errors); validateApps(qInstance);
validatePossibleValueSources(qInstance);
} }
catch(Exception e) 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) -> 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.")) "At least 1 table must be defined."))
{ {
qInstance.getTables().forEach((tableName, table) -> 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 // // 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 + ".")) "Missing backend name for table " + tableName + "."))
{ {
if(CollectionUtils.nullSafeHasContents(qInstance.getBackends())) 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 // // 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) -> 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() + "."); "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
if(field.getPossibleValueSourceName() != null) 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 + "."); "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
} }
}); });
@ -189,10 +213,10 @@ public class QInstanceValidator
{ {
for(QFieldSection section : table.getSections()) for(QFieldSection section : table.getSections())
{ {
validateSection(errors, table, section, fieldNamesInSections); validateSection(table, section, fieldNamesInSections);
if(section.getTier().equals(Tier.T1)) 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; tier1Section = section;
} }
} }
@ -202,7 +226,7 @@ public class QInstanceValidator
{ {
for(String fieldName : table.getFields().keySet()) 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()) 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()) 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 + ": "; String prefix = "Table " + tableName + ", customizer " + customizerName + ": ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix)) if(!preAssertionsForCodeReference(codeReference, prefix))
{ {
return; return;
} }
@ -245,18 +378,18 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks // // 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 // // make sure the class can be loaded //
/////////////////////////////////////// ///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix); Class<?> customizerClass = getClassForCodeReference(codeReference, prefix);
if(customizerClass != null) if(customizerClass != null)
{ {
////////////////////////////////////////////////// //////////////////////////////////////////////////
// make sure the customizer can be instantiated // // make sure the customizer can be instantiated //
////////////////////////////////////////////////// //////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName); TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName);
if(tableCustomizer == null) if(tableCustomizer == null)
@ -273,7 +406,7 @@ public class QInstanceValidator
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null) 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(); Consumer<Object> validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction();
if(castedObject != null && validationFunction != null) 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; T castedObject = null;
try try
@ -314,7 +447,7 @@ public class QInstanceValidator
} }
catch(ClassCastException e) 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; 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; Object customizerInstance = null;
try 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(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() + "."); assertCondition(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.")) if(assertCondition(CollectionUtils.nullSafeHasContents(section.getFieldNames()), "Table " + table.getName() + " section " + section.getName() + " does not have any fields."))
{ {
if(table.getFields() != null) if(table.getFields() != null)
{ {
for(String fieldName : section.getFieldNames()) 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(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(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections.");
fieldNamesInSections.add(fieldName); 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())) if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses()))
{ {
qInstance.getProcesses().forEach((processName, process) -> 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 // // validate the table name for the process //
///////////////////////////////////////////// /////////////////////////////////////////////
if(process.getTableName() != null) 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 // // 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; int index = 0;
for(QStepMetaData step : process.getStepList()) 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++; 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())) if(CollectionUtils.nullSafeHasContents(qInstance.getApps()))
{ {
qInstance.getApps().forEach((appName, app) -> 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<>(); Set<String> appsVisited = new HashSet<>();
visitAppCheckingForCycles(app, appsVisited, errors); visitAppCheckingForCycles(app, appsVisited);
if(app.getChildren() != null) if(app.getChildren() != null)
{ {
Set<String> childNames = new HashSet<>(); Set<String> childNames = new HashSet<>();
for(QAppChildMetaData child : app.getChildren()) 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(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(!childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName());
childNames.add(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())) if(CollectionUtils.nullSafeHasContents(qInstance.getPossibleValueSources()))
{ {
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
{ {
assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) 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 // // 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 -> case ENUM ->
{ {
assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); assertCondition(!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(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 -> case TABLE ->
{ {
assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); assertCondition(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(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 -> case CUSTOM ->
{ {
assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); assertCondition(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(!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."); assertCondition(QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider.");
validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference()); validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
} }
} }
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); 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(codeReference, prefix))
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
{ {
return; return;
} }
@ -505,25 +636,25 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks // // 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 // // make sure the class can be loaded //
/////////////////////////////////////// ///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix); Class<?> customizerClass = getClassForCodeReference(codeReference, prefix);
if(customizerClass != null) if(customizerClass != null)
{ {
////////////////////////////////////////////////// //////////////////////////////////////////////////
// make sure the customizer can be instantiated // // 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 // // make sure the customizer instance can be cast to the expected type //
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
if(customizerInstance != null) 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; Class<?> customizerClass = null;
try 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; 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; 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; okay = false;
} }
@ -575,9 +706,9 @@ public class QInstanceValidator
** Check if an app's child list can recursively be traversed without finding a ** Check if an app's child list can recursively be traversed without finding a
** duplicate, which would indicate a cycle (e.g., an error) ** 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()); appsVisited.add(app.getName());
if(app.getChildren() != null) if(app.getChildren() != null)
@ -586,7 +717,7 @@ public class QInstanceValidator
{ {
if(child instanceof QAppMetaData childApp) 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) 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, ** 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). ** 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) 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 ** Enum to define the possible authentication types
** **
*******************************************************************************/ *******************************************************************************/
@SuppressWarnings("rawtypes")
public enum QAuthenticationType public enum QAuthenticationType
{ {
AUTH_0("auth0"), AUTH_0("auth0"),

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey; 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.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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 Map<String, QBackendMetaData> backends = new HashMap<>();
private QAuthenticationMetaData authentication = null; 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. // // 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 ** 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 ** Getter for hasBeenValidated
** **
@ -447,6 +494,17 @@ public class QInstance
/*******************************************************************************
** Setter for hasBeenValidated
**
*******************************************************************************/
public void setHasBeenValidated(QInstanceValidationKey key)
{
this.hasBeenValidated = true;
}
/******************************************************************************* /*******************************************************************************
** Getter for authentication ** 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 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.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; 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. ** 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; this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER;
} }
else if(RecordAutomationHandler.class.isAssignableFrom(javaClass))
{
this.codeUsage = QCodeUsage.RECORD_AUTOMATION_HANDLER;
}
else else
{ {
throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName())); 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 BACKEND_STEP, // a backend-step in a process
CUSTOMIZER, // a function to customize part of a QQQ table's behavior 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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; 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.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 Map<String, QFieldMetaData> fields;
private QTableBackendDetails backendDetails; private QTableBackendDetails backendDetails;
private QTableAutomationDetails automationDetails;
private Map<String, QCodeReference> customizers; 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 java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; 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.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.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; 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.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.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -162,11 +166,13 @@ class QInstanceValidatorTest
qInstance.getBackend("default").setName("notDefault"); qInstance.getBackend("default").setName("notDefault");
qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople"); qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople");
qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates"); qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates");
qInstance.getAutomationProvider(TestUtils.POLLING_AUTOMATION).setName("notPolling");
}, },
"Inconsistent naming for table", "Inconsistent naming for table",
"Inconsistent naming for backend", "Inconsistent naming for backend",
"Inconsistent naming for process", "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"); "Instance of CodeReference could not be created");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)), 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)), assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)),
"Error validating customizer type parameters"); "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 ** 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 ** failed validation with reasons that match the supplied vararg-reasons (but allow
@ -690,7 +920,8 @@ class QInstanceValidatorTest
if(!allowExtraReasons) if(!allowExtraReasons)
{ {
int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size(); 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) for(String reason : reasons)

View File

@ -23,26 +23,39 @@ package com.kingsrook.qqq.backend.core.utils;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List; 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.AddAge;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; 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.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; 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.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; 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.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; 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.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; 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.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; 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.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; 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.QFunctionOutputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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.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.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.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; 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.basic.BasicETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; 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 public class TestUtils
{ {
private static final Logger LOG = LogManager.getLogger(TestUtils.class);
public static final String DEFAULT_BACKEND_NAME = "default"; public static final String DEFAULT_BACKEND_NAME = "default";
public static final String MEMORY_BACKEND_NAME = "memory"; 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_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge"; 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_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 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_STATE = "state"; // enum-type
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-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_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(defineTablePerson());
qInstance.addTable(definePersonFileTable()); qInstance.addTable(definePersonFileTable());
qInstance.addTable(definePersonMemoryTable());
qInstance.addTable(defineTableIdAndNameOnly()); qInstance.addTable(defineTableIdAndNameOnly());
qInstance.addTable(defineTableShape()); qInstance.addTable(defineTableShape());
qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
qInstance.addPossibleValueSource(defineShapePossibleValueSource()); qInstance.addPossibleValueSource(defineShapePossibleValueSource());
qInstance.addPossibleValueSource(defineCustomPossibleValueSource()); qInstance.addPossibleValueSource(defineCustomPossibleValueSource());
@ -117,6 +145,8 @@ public class TestUtils
qInstance.addProcess(new BasicETLProcess().defineProcessMetaData()); qInstance.addProcess(new BasicETLProcess().defineProcessMetaData());
qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData()); qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData());
qInstance.addAutomationProvider(definePollingAutomationProvider());
defineApps(qInstance); defineApps(qInstance);
return (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 ** 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. ** 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 ** Define simple table with just an id and name
*******************************************************************************/ *******************************************************************************/