diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java new file mode 100644 index 00000000..ca0b1158 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java @@ -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 +{ + 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()); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java new file mode 100644 index 00000000..74d76fa6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java @@ -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; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java new file mode 100644 index 00000000..8728f4a6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java @@ -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 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 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()))); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationExecutor.java new file mode 100644 index 00000000..205a16e3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationExecutor.java @@ -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 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 sessionSupplier) + { + this.sessionSupplier = sessionSupplier; + } + + + + /******************************************************************************* + ** Getter for runningState + ** + *******************************************************************************/ + public RunningState getRunningState() + { + return runningState; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum RunningState + { + STOPPED, + RUNNING, + STOPPING, + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationRunner.java new file mode 100644 index 00000000..46d69208 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationRunner.java @@ -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 sessionSupplier; + + private List managedTables = new ArrayList<>(); + + private Map> tableInsertActions = new HashMap<>(); + private Map> tableUpdateActions = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public PollingAutomationRunner(QInstance instance, String providerName, Supplier 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 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 actions, List 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 records, AutomationStatus automationStatus) throws QException + { + RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, automationStatus); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java index f1774928..eec3cab5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java @@ -24,12 +24,14 @@ package com.kingsrook.qqq.backend.core.actions.customizers; import java.util.Optional; import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -97,6 +99,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)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java index 54ebcfb2..f7dc6869 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java @@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; @@ -48,6 +50,9 @@ public class InsertAction *******************************************************************************/ public InsertOutput execute(InsertInput insertInput) throws QException { + ActionHelper.validateSession(insertInput); + setAutomationStatusField(insertInput); + QBackendModuleInterface qModule = getBackendModuleInterface(insertInput); // todo pre-customization - just get to modify the request? InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput); @@ -57,6 +62,16 @@ public class InsertAction + /******************************************************************************* + ** If the table being inserted into uses an automation-status field, populate it now. + *******************************************************************************/ + private void setAutomationStatusField(InsertInput insertInput) + { + RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java index 5b8e3e03..4f925411 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; @@ -42,6 +44,7 @@ public class UpdateAction public UpdateOutput execute(UpdateInput updateInput) throws QException { ActionHelper.validateSession(updateInput); + setAutomationStatusField(updateInput); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend()); @@ -50,4 +53,15 @@ public class UpdateAction // todo post-customization - can do whatever w/ the result if you want return updateResult; } + + + + /******************************************************************************* + ** If the table being updated uses an automation-status field, populate it now. + *******************************************************************************/ + private void setAutomationStatusField(UpdateInput updateInput) + { + RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 4b7c4c2b..3d5a5839 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; @@ -39,10 +40,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; @@ -65,6 +70,8 @@ public class QInstanceValidator private boolean printWarnings = false; + private List errors = new ArrayList<>(); + /******************************************************************************* @@ -96,14 +103,14 @@ public class QInstanceValidator ////////////////////////////////////////////////////////////////////////// // do the validation checks - a good qInstance has all conditions TRUE! // ////////////////////////////////////////////////////////////////////////// - List errors = new ArrayList<>(); try { - validateBackends(qInstance, errors); - validateTables(qInstance, errors); - validateProcesses(qInstance, errors); - validateApps(qInstance, errors); - validatePossibleValueSources(qInstance, errors); + validateBackends(qInstance); + validateAutomationProviders(qInstance); + validateTables(qInstance); + validateProcesses(qInstance); + validateApps(qInstance); + validatePossibleValueSources(qInstance); } catch(Exception e) { @@ -123,13 +130,13 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateBackends(QInstance qInstance, List errors) + private void validateBackends(QInstance qInstance) { - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getBackends()), "At least 1 backend must be defined.")) + if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getBackends()), "At least 1 backend must be defined.")) { qInstance.getBackends().forEach((backendName, backend) -> { - assertCondition(errors, Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + "."); + assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + "."); }); } } @@ -139,42 +146,59 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateTables(QInstance qInstance, List errors) + private void validateAutomationProviders(QInstance qInstance) { - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getTables()), + if(qInstance.getAutomationProviders() != null) + { + qInstance.getAutomationProviders().forEach((name, automationProvider) -> + { + assertCondition(Objects.equals(name, automationProvider.getName()), "Inconsistent naming for automationProvider: " + name + "/" + automationProvider.getName() + "."); + assertCondition(automationProvider.getType() != null, "Missing type for automationProvider: " + name); + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTables(QInstance qInstance) + { + if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getTables()), "At least 1 table must be defined.")) { qInstance.getTables().forEach((tableName, table) -> { - assertCondition(errors, Objects.equals(tableName, table.getName()), "Inconsistent naming for table: " + tableName + "/" + table.getName() + "."); + assertCondition(Objects.equals(tableName, table.getName()), "Inconsistent naming for table: " + tableName + "/" + table.getName() + "."); - validateAppChildHasValidParentAppName(qInstance, errors, table); + validateAppChildHasValidParentAppName(qInstance, table); //////////////////////////////////////// // validate the backend for the table // //////////////////////////////////////// - if(assertCondition(errors, StringUtils.hasContent(table.getBackendName()), + if(assertCondition(StringUtils.hasContent(table.getBackendName()), "Missing backend name for table " + tableName + ".")) { if(CollectionUtils.nullSafeHasContents(qInstance.getBackends())) { - assertCondition(errors, qInstance.getBackendForTable(tableName) != null, "Unrecognized backend " + table.getBackendName() + " for table " + tableName + "."); + assertCondition(qInstance.getBackendForTable(tableName) != null, "Unrecognized backend " + table.getBackendName() + " for table " + tableName + "."); } } ////////////////////////////////// // validate fields in the table // ////////////////////////////////// - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(table.getFields()), "At least 1 field must be defined in table " + tableName + ".")) + if(assertCondition(CollectionUtils.nullSafeHasContents(table.getFields()), "At least 1 field must be defined in table " + tableName + ".")) { table.getFields().forEach((fieldName, field) -> { - assertCondition(errors, Objects.equals(fieldName, field.getName()), + assertCondition(Objects.equals(fieldName, field.getName()), "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); if(field.getPossibleValueSourceName() != null) { - assertCondition(errors, qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, + assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); } }); @@ -189,10 +213,10 @@ public class QInstanceValidator { for(QFieldSection section : table.getSections()) { - validateSection(errors, table, section, fieldNamesInSections); + validateSection(table, section, fieldNamesInSections); if(section.getTier().equals(Tier.T1)) { - assertCondition(errors, tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1"); + assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1"); tier1Section = section; } } @@ -202,7 +226,7 @@ public class QInstanceValidator { for(String fieldName : table.getFields().keySet()) { - assertCondition(errors, fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections."); + assertCondition(fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections."); } } @@ -213,7 +237,7 @@ public class QInstanceValidator { for(String recordLabelField : table.getRecordLabelFields()) { - assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); + assertCondition(table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); } } @@ -221,9 +245,17 @@ public class QInstanceValidator { for(Map.Entry 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 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 usedNames = new HashSet<>(); + if(automationDetails.getActions() != null) + { + automationDetails.getActions().forEach(action -> + { + assertCondition(StringUtils.hasContent(action.getName()), prefix + "has an action missing a name"); + assertCondition(!usedNames.contains(action.getName()), prefix + "has more than one action named " + action.getName()); + usedNames.add(action.getName()); + + String actionPrefix = prefix + "action " + action.getName() + " "; + assertCondition(action.getTriggerEvent() != null, actionPrefix + "is missing a triggerEvent"); + + ///////////////////////////////////////////////////// + // validate the code or process used by the action // + ///////////////////////////////////////////////////// + int numberSet = 0; + if(action.getCodeReference() != null) + { + numberSet++; + if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix)) + { + validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandler.class); + } + } + + if(action.getProcessName() != null) + { + numberSet++; + QProcessMetaData process = qInstance.getProcess(action.getProcessName()); + if(assertCondition(process != null, actionPrefix + "has an unrecognized processName: " + action.getProcessName())) + { + if(process.getTableName() != null) + { + assertCondition(tableName.equals(process.getTableName()), actionPrefix + " references a process from a different table"); + } + } + } + + assertCondition(numberSet != 0, actionPrefix + "is missing both a codeReference and a processName"); + assertCondition(!(numberSet > 1), actionPrefix + "has both a codeReference and a processName (which is not allowed)"); + + /////////////////////////////////////////// + // validate the filter (if there is one) // + /////////////////////////////////////////// + if(action.getFilter() != null && action.getFilter().getCriteria() != null) + { + action.getFilter().getCriteria().forEach((criteria) -> + { + if(assertCondition(StringUtils.hasContent(criteria.getFieldName()), actionPrefix + "has a filter criteria without a field name")) + { + assertNoException(() -> table.getField(criteria.getFieldName()), actionPrefix + "has a filter criteria referencing an unrecognized field: " + criteria.getFieldName()); + } + + assertCondition(criteria.getOperator() != null, actionPrefix + "has a filter criteria without an operator"); + + // todo - validate cardinality of values... + }); + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableCustomizer(String tableName, String customizerName, QCodeReference codeReference) { String prefix = "Table " + tableName + ", customizer " + customizerName + ": "; - if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + if(!preAssertionsForCodeReference(codeReference, prefix)) { return; } @@ -245,18 +378,18 @@ public class QInstanceValidator ////////////////////////////////////////////////////////////////////////////// // make sure (at this time) that it's a java type, then do some java checks // ////////////////////////////////////////////////////////////////////////////// - if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + if(assertCondition(codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) { /////////////////////////////////////// // make sure the class can be loaded // /////////////////////////////////////// - Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + Class customizerClass = getClassForCodeReference(codeReference, prefix); if(customizerClass != null) { ////////////////////////////////////////////////// // make sure the customizer can be instantiated // ////////////////////////////////////////////////// - Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass); TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName); if(tableCustomizer == null) @@ -273,7 +406,7 @@ public class QInstanceValidator //////////////////////////////////////////////////////////////////////// if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null) { - Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance); + Object castedObject = getCastedObject(prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance); Consumer validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction(); if(castedObject != null && validationFunction != null) @@ -305,7 +438,7 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private T getCastedObject(List errors, String prefix, Class expectedType, Object customizerInstance) + private T getCastedObject(String prefix, Class expectedType, Object customizerInstance) { T castedObject = null; try @@ -314,7 +447,7 @@ public class QInstanceValidator } catch(ClassCastException e) { - errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType); + errors.add(prefix + "CodeReference is not of the expected type: " + expectedType); } return castedObject; } @@ -324,7 +457,7 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private Object getInstanceOfCodeReference(List errors, String prefix, Class customizerClass) + private Object getInstanceOfCodeReference(String prefix, Class customizerClass) { Object customizerInstance = null; try @@ -343,18 +476,18 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateSection(List errors, QTableMetaData table, QFieldSection section, Set fieldNamesInSections) + private void validateSection(QTableMetaData table, QFieldSection section, Set fieldNamesInSections) { - assertCondition(errors, StringUtils.hasContent(section.getName()), "Missing a name for field section in table " + table.getName() + "."); - assertCondition(errors, StringUtils.hasContent(section.getLabel()), "Missing a label for field section in table " + table.getLabel() + "."); - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(section.getFieldNames()), "Table " + table.getName() + " section " + section.getName() + " does not have any fields.")) + assertCondition(StringUtils.hasContent(section.getName()), "Missing a name for field section in table " + table.getName() + "."); + assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for field section in table " + table.getLabel() + "."); + if(assertCondition(CollectionUtils.nullSafeHasContents(section.getFieldNames()), "Table " + table.getName() + " section " + section.getName() + " does not have any fields.")) { if(table.getFields() != null) { for(String fieldName : section.getFieldNames()) { - assertCondition(errors, table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table."); - assertCondition(errors, !fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections."); + assertCondition(table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table."); + assertCondition(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections."); fieldNamesInSections.add(fieldName); } @@ -367,33 +500,33 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateProcesses(QInstance qInstance, List errors) + private void validateProcesses(QInstance qInstance) { if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses())) { qInstance.getProcesses().forEach((processName, process) -> { - assertCondition(errors, Objects.equals(processName, process.getName()), "Inconsistent naming for process: " + processName + "/" + process.getName() + "."); + assertCondition(Objects.equals(processName, process.getName()), "Inconsistent naming for process: " + processName + "/" + process.getName() + "."); - validateAppChildHasValidParentAppName(qInstance, errors, process); + validateAppChildHasValidParentAppName(qInstance, process); ///////////////////////////////////////////// // validate the table name for the process // ///////////////////////////////////////////// if(process.getTableName() != null) { - assertCondition(errors, qInstance.getTable(process.getTableName()) != null, "Unrecognized table " + process.getTableName() + " for process " + processName + "."); + assertCondition(qInstance.getTable(process.getTableName()) != null, "Unrecognized table " + process.getTableName() + " for process " + processName + "."); } /////////////////////////////////// // validate steps in the process // /////////////////////////////////// - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + ".")) + if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + ".")) { int index = 0; for(QStepMetaData step : process.getStepList()) { - assertCondition(errors, StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName); + assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName); index++; } } @@ -406,26 +539,26 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateApps(QInstance qInstance, List errors) + private void validateApps(QInstance qInstance) { if(CollectionUtils.nullSafeHasContents(qInstance.getApps())) { qInstance.getApps().forEach((appName, app) -> { - assertCondition(errors, Objects.equals(appName, app.getName()), "Inconsistent naming for app: " + appName + "/" + app.getName() + "."); + assertCondition(Objects.equals(appName, app.getName()), "Inconsistent naming for app: " + appName + "/" + app.getName() + "."); - validateAppChildHasValidParentAppName(qInstance, errors, app); + validateAppChildHasValidParentAppName(qInstance, app); Set appsVisited = new HashSet<>(); - visitAppCheckingForCycles(app, appsVisited, errors); + visitAppCheckingForCycles(app, appsVisited); if(app.getChildren() != null) { Set childNames = new HashSet<>(); for(QAppChildMetaData child : app.getChildren()) { - assertCondition(errors, Objects.equals(appName, child.getParentAppName()), "Child " + child.getName() + " of app " + appName + " does not have its parent app properly set."); - assertCondition(errors, !childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName()); + assertCondition(Objects.equals(appName, child.getParentAppName()), "Child " + child.getName() + " of app " + appName + " does not have its parent app properly set."); + assertCondition(!childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName()); childNames.add(child.getName()); } } @@ -438,14 +571,14 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validatePossibleValueSources(QInstance qInstance, List errors) + private void validatePossibleValueSources(QInstance qInstance) { if(CollectionUtils.nullSafeHasContents(qInstance.getPossibleValueSources())) { qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> { - assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); - if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) + assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); + if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) { //////////////////////////////////////////////////////////////////////////////////////////////// // assert about fields that should and should not be set, based on possible value source type // @@ -455,30 +588,30 @@ public class QInstanceValidator { case ENUM -> { - assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); - assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); - assertCondition(errors, CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); + assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); } case TABLE -> { - assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); - assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); - if(assertCondition(errors, StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) + if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) { - assertCondition(errors, qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); + assertCondition(qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); } } case CUSTOM -> { - assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); - assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); - if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) + if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) { - assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); - validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference()); + assertCondition(QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); + validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class); } } default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); @@ -493,11 +626,9 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateCustomPossibleValueSourceCode(List errors, String pvsName, QCodeReference codeReference) + private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class expectedClass) { - String prefix = "PossibleValueSource " + pvsName + " custom code reference: "; - - if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + if(!preAssertionsForCodeReference(codeReference, prefix)) { return; } @@ -505,25 +636,25 @@ public class QInstanceValidator ////////////////////////////////////////////////////////////////////////////// // make sure (at this time) that it's a java type, then do some java checks // ////////////////////////////////////////////////////////////////////////////// - if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + if(assertCondition(codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA code references are supported at this time.")) { /////////////////////////////////////// // make sure the class can be loaded // /////////////////////////////////////// - Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + Class customizerClass = getClassForCodeReference(codeReference, prefix); if(customizerClass != null) { ////////////////////////////////////////////////// // make sure the customizer can be instantiated // ////////////////////////////////////////////////// - Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass); //////////////////////////////////////////////////////////////////////// // make sure the customizer instance can be cast to the expected type // //////////////////////////////////////////////////////////////////////// if(customizerInstance != null) { - getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance); + getCastedObject(prefix, expectedClass, customizerInstance); } } } @@ -534,7 +665,7 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private Class getClassForCodeReference(List errors, QCodeReference codeReference, String prefix) + private Class getClassForCodeReference(QCodeReference codeReference, String prefix) { Class customizerClass = null; try @@ -553,15 +684,15 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private boolean preAssertionsForCodeReference(List errors, QCodeReference codeReference, String prefix) + private boolean preAssertionsForCodeReference(QCodeReference codeReference, String prefix) { boolean okay = true; - if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name")) + if(!assertCondition(StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name")) { okay = false; } - if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type")) + if(!assertCondition(codeReference.getCodeType() != null, prefix + " is missing a code type")) { okay = false; } @@ -575,9 +706,9 @@ public class QInstanceValidator ** Check if an app's child list can recursively be traversed without finding a ** duplicate, which would indicate a cycle (e.g., an error) *******************************************************************************/ - private void visitAppCheckingForCycles(QAppMetaData app, Set appsVisited, List errors) + private void visitAppCheckingForCycles(QAppMetaData app, Set appsVisited) { - if(assertCondition(errors, !appsVisited.contains(app.getName()), "Circular app reference detected, involving " + app.getName())) + if(assertCondition(!appsVisited.contains(app.getName()), "Circular app reference detected, involving " + app.getName())) { appsVisited.add(app.getName()); if(app.getChildren() != null) @@ -586,7 +717,7 @@ public class QInstanceValidator { if(child instanceof QAppMetaData childApp) { - visitAppCheckingForCycles(childApp, appsVisited, errors); + visitAppCheckingForCycles(childApp, appsVisited); } } } @@ -598,11 +729,11 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateAppChildHasValidParentAppName(QInstance qInstance, List errors, QAppChildMetaData appChild) + private void validateAppChildHasValidParentAppName(QInstance qInstance, QAppChildMetaData appChild) { if(appChild.getParentAppName() != null) { - assertCondition(errors, qInstance.getApp(appChild.getParentAppName()) != null, "Unrecognized parent app " + appChild.getParentAppName() + " for " + appChild.getName() + "."); + assertCondition(qInstance.getApp(appChild.getParentAppName()) != null, "Unrecognized parent app " + appChild.getParentAppName() + " for " + appChild.getName() + "."); } } @@ -613,7 +744,7 @@ public class QInstanceValidator ** But if it's false, add the provided message to the list of errors (and return false, ** e.g., in case you need to stop evaluating rules to avoid exceptions). *******************************************************************************/ - private boolean assertCondition(List errors, boolean condition, String message) + private boolean assertCondition(boolean condition, String message) { if(!condition) { @@ -625,6 +756,41 @@ public class QInstanceValidator + /******************************************************************************* + ** For the given lambda, if it doesn't throw an exception, then we're all good (and return true). + ** But if it throws, add the provided message to the list of errors (and return false, + ** e.g., in case you need to stop evaluating rules to avoid exceptions). + *******************************************************************************/ + private boolean assertNoException(UnsafeLambda unsafeLambda, String message) + { + try + { + unsafeLambda.run(); + return (true); + } + catch(Exception e) + { + errors.add(message); + return (false); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @FunctionalInterface + interface UnsafeLambda + { + /******************************************************************************* + ** + *******************************************************************************/ + void run() throws Exception; + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/RecordAutomationInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/RecordAutomationInput.java new file mode 100644 index 00000000..d3cd1c4d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/RecordAutomationInput.java @@ -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 recordList; + private List 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 getRecordList() + { + return recordList; + } + + + + /******************************************************************************* + ** Setter for recordList + ** + *******************************************************************************/ + public void setRecordList(List recordList) + { + this.recordList = recordList; + } + + + + /******************************************************************************* + ** Fluent setter for recordList + ** + *******************************************************************************/ + public RecordAutomationInput withRecordList(List recordList) + { + this.recordList = recordList; + return (this); + } + + + + /******************************************************************************* + ** Getter for primaryKeyList + ** + *******************************************************************************/ + public List getPrimaryKeyList() + { + return primaryKeyList; + } + + + + /******************************************************************************* + ** Setter for primaryKeyList + ** + *******************************************************************************/ + public void setPrimaryKeyList(List primaryKeyList) + { + this.primaryKeyList = primaryKeyList; + } + + + + /******************************************************************************* + ** Fluent setter for primaryKeyList + ** + *******************************************************************************/ + public RecordAutomationInput withPrimaryKeyList(List primaryKeyList) + { + this.primaryKeyList = primaryKeyList; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java index afc24aaa..0550ee66 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java @@ -26,7 +26,6 @@ package com.kingsrook.qqq.backend.core.model.metadata; ** Enum to define the possible authentication types ** *******************************************************************************/ -@SuppressWarnings("rawtypes") public enum QAuthenticationType { AUTH_0("auth0"), diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 3712d077..95faf73c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey; +import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; @@ -49,7 +50,8 @@ public class QInstance @JsonIgnore private Map backends = new HashMap<>(); - private QAuthenticationMetaData authentication = null; + private QAuthenticationMetaData authentication = null; + private Map automationProviders = new HashMap<>(); //////////////////////////////////////////////////////////////////////////////////////////// // Important to use LinkedHashmap here, to preserve the order in which entries are added. // @@ -103,17 +105,6 @@ public class QInstance - /******************************************************************************* - ** Setter for hasBeenValidated - ** - *******************************************************************************/ - public void setHasBeenValidated(QInstanceValidationKey key) - { - this.hasBeenValidated = true; - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -148,6 +139,28 @@ public class QInstance + /******************************************************************************* + ** Getter for backends + ** + *******************************************************************************/ + public Map getBackends() + { + return backends; + } + + + + /******************************************************************************* + ** Setter for backends + ** + *******************************************************************************/ + public void setBackends(Map backends) + { + this.backends = backends; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -187,6 +200,28 @@ public class QInstance + /******************************************************************************* + ** Getter for tables + ** + *******************************************************************************/ + public Map getTables() + { + return tables; + } + + + + /******************************************************************************* + ** Setter for tables + ** + *******************************************************************************/ + public void setTables(Map tables) + { + this.tables = tables; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -221,6 +256,28 @@ public class QInstance + /******************************************************************************* + ** Getter for possibleValueSources + ** + *******************************************************************************/ + public Map getPossibleValueSources() + { + return possibleValueSources; + } + + + + /******************************************************************************* + ** Setter for possibleValueSources + ** + *******************************************************************************/ + public void setPossibleValueSources(Map possibleValueSources) + { + this.possibleValueSources = possibleValueSources; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -271,6 +328,28 @@ public class QInstance + /******************************************************************************* + ** Getter for processes + ** + *******************************************************************************/ + public Map getProcesses() + { + return processes; + } + + + + /******************************************************************************* + ** Setter for processes + ** + *******************************************************************************/ + public void setProcesses(Map processes) + { + this.processes = processes; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -305,94 +384,6 @@ public class QInstance - /******************************************************************************* - ** Getter for backends - ** - *******************************************************************************/ - public Map getBackends() - { - return backends; - } - - - - /******************************************************************************* - ** Setter for backends - ** - *******************************************************************************/ - public void setBackends(Map backends) - { - this.backends = backends; - } - - - - /******************************************************************************* - ** Getter for tables - ** - *******************************************************************************/ - public Map getTables() - { - return tables; - } - - - - /******************************************************************************* - ** Setter for tables - ** - *******************************************************************************/ - public void setTables(Map tables) - { - this.tables = tables; - } - - - - /******************************************************************************* - ** Getter for possibleValueSources - ** - *******************************************************************************/ - public Map getPossibleValueSources() - { - return possibleValueSources; - } - - - - /******************************************************************************* - ** Setter for possibleValueSources - ** - *******************************************************************************/ - public void setPossibleValueSources(Map possibleValueSources) - { - this.possibleValueSources = possibleValueSources; - } - - - - /******************************************************************************* - ** Getter for processes - ** - *******************************************************************************/ - public Map getProcesses() - { - return processes; - } - - - - /******************************************************************************* - ** Setter for processes - ** - *******************************************************************************/ - public void setProcesses(Map processes) - { - this.processes = processes; - } - - - /******************************************************************************* ** Getter for apps ** @@ -415,6 +406,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 getAutomationProviders() + { + return automationProviders; + } + + + + /******************************************************************************* + ** Setter for automationProviders + ** + *******************************************************************************/ + public void setAutomationProviders(Map automationProviders) + { + this.automationProviders = automationProviders; + } + + + /******************************************************************************* ** Getter for hasBeenValidated ** @@ -426,6 +473,17 @@ public class QInstance + /******************************************************************************* + ** Setter for hasBeenValidated + ** + *******************************************************************************/ + public void setHasBeenValidated(QInstanceValidationKey key) + { + this.hasBeenValidated = true; + } + + + /******************************************************************************* ** Getter for authentication ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/PollingAutomationProviderMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/PollingAutomationProviderMetaData.java new file mode 100644 index 00000000..e5bb0283 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/PollingAutomationProviderMetaData.java @@ -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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/QAutomationProviderMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/QAutomationProviderMetaData.java new file mode 100644 index 00000000..ae8c8f00 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/QAutomationProviderMetaData.java @@ -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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/QAutomationProviderType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/QAutomationProviderType.java new file mode 100644 index 00000000..9a78e1fe --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/automation/QAutomationProviderType.java @@ -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 . + */ + +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; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index 5a82f004..40cbc3a2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; @@ -59,6 +60,17 @@ public class QCodeReference + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return "QCodeReference{name='" + name + "'}"; + } + + + /******************************************************************************* ** Constructor that just takes a java class, and infers the other fields. *******************************************************************************/ @@ -75,6 +87,10 @@ public class QCodeReference { this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER; } + else if(RecordAutomationHandler.class.isAssignableFrom(javaClass)) + { + this.codeUsage = QCodeUsage.RECORD_AUTOMATION_HANDLER; + } else { throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName())); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java index 925c1f55..7fd8fbd1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java @@ -30,5 +30,6 @@ public enum QCodeUsage { BACKEND_STEP, // a backend-step in a process CUSTOMIZER, // a function to customize part of a QQQ table's behavior - POSSIBLE_VALUE_PROVIDER // code that drives a custom possibleValueSource + POSSIBLE_VALUE_PROVIDER, // code that drives a custom possibleValueSource + RECORD_AUTOMATION_HANDLER // code that executes record automations } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 0d1cbcef..318a12bb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; /******************************************************************************* @@ -63,7 +64,8 @@ public class QTableMetaData implements QAppChildMetaData, Serializable private Map fields; - private QTableBackendDetails backendDetails; + private QTableBackendDetails backendDetails; + private QTableAutomationDetails automationDetails; private Map 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); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java new file mode 100644 index 00000000..7d504465 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java @@ -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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTrackingType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTrackingType.java new file mode 100644 index 00000000..6d67f046 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTrackingType.java @@ -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) +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java new file mode 100644 index 00000000..fadfc06d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java @@ -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 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 getActions() + { + return actions; + } + + + + /******************************************************************************* + ** Setter for actions + ** + *******************************************************************************/ + public void setActions(List actions) + { + this.actions = actions; + } + + + + /******************************************************************************* + ** Fluent setter for actions + ** + *******************************************************************************/ + public QTableAutomationDetails withActions(List 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); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java new file mode 100644 index 00000000..295e4bdc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java @@ -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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TriggerEvent.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TriggerEvent.java new file mode 100644 index 00000000..4b64ebe2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TriggerEvent.java @@ -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 +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationExecutorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationExecutorTest.java new file mode 100644 index 00000000..cfda8981 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationExecutorTest.java @@ -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 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 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(); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index ee2464a9..b2b3d95e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -30,6 +30,9 @@ import java.util.function.Consumer; import java.util.function.Function; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; @@ -43,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; @@ -162,11 +166,13 @@ class QInstanceValidatorTest qInstance.getBackend("default").setName("notDefault"); qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople"); qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates"); + qInstance.getAutomationProvider(TestUtils.POLLING_AUTOMATION).setName("notPolling"); }, "Inconsistent naming for table", "Inconsistent naming for backend", "Inconsistent naming for process", - "Inconsistent naming for possibleValueSource" + "Inconsistent naming for possibleValueSource", + "Inconsistent naming for automationProvider" ); } @@ -296,7 +302,7 @@ class QInstanceValidatorTest "Instance of CodeReference could not be created"); assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)), - "CodeReference could not be casted"); + "CodeReference is not of the expected type"); assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)), "Error validating customizer type parameters"); @@ -649,6 +655,230 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAutomationProviderType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getAutomationProvider(TestUtils.POLLING_AUTOMATION).setType(null), + "Missing type for automationProvider"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationProviderName() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setProviderName(null), + "is missing a providerName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setProviderName(""), + "is missing a providerName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setProviderName("notARealProvider"), + "unrecognized providerName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationStatusTracking() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setStatusTracking(null), + "do not have statusTracking"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationStatusTrackingType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getStatusTracking().setType(null), + "statusTracking is missing a type"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationStatusTrackingFieldName() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getStatusTracking().setFieldName(null), + "missing its fieldName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getStatusTracking().setFieldName(""), + "missing its fieldName"); + + ////////////////////////////////////////////////// + // todo - make sure it's a field in the table?? // + ////////////////////////////////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationActionsNames() + { + assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setName(null), + "action missing a name"); + + assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setName(""), + "action missing a name"); + + assertValidationFailureReasons((qInstance) -> + { + List actions = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getActions(); + actions.add(actions.get(0)); + }, + "more than one action named"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationActionTriggerEvent() + { + assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setTriggerEvent(null), + "missing a triggerEvent"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationActionCodeReference() + { + assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setCodeReference(new QCodeReference()), + "missing a code reference name", "missing a code type"); + + assertValidationFailureReasons((qInstance) -> getAction0(qInstance).setCodeReference(new QCodeReference(TestUtils.CustomPossibleValueSource.class)), + "is not of the expected type"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationActionProcessName() + { + assertValidationFailureReasons((qInstance) -> + { + TableAutomationAction action = getAction0(qInstance); + action.setCodeReference(null); + action.setProcessName("notAProcess"); + }, + "unrecognized processName"); + + assertValidationSuccess((qInstance) -> + { + qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + TableAutomationAction action = getAction0(qInstance); + action.setCodeReference(null); + action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE); + }); + + assertValidationFailureReasons((qInstance) -> + { + TableAutomationAction action = getAction0(qInstance); + action.setCodeReference(null); + action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE); + }, + "different table"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationActionCodeReferenceAndProcessName() + { + assertValidationFailureReasons((qInstance) -> + { + TableAutomationAction action = getAction0(qInstance); + action.setCodeReference(null); + action.setProcessName(null); + }, + "missing both"); + + assertValidationFailureReasons((qInstance) -> + { + qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + TableAutomationAction action = getAction0(qInstance); + action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE); + }, + "has both"); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableAutomationActionFilter() + { + assertValidationFailureReasons((qInstance) -> + { + TableAutomationAction action = getAction0(qInstance); + action.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria()) + ); + }, + "without a field name", "without an operator"); + + assertValidationFailureReasons((qInstance) -> + { + TableAutomationAction action = getAction0(qInstance); + action.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("notAField", QCriteriaOperator.EQUALS, Collections.emptyList())) + ); + }, + "unrecognized field"); + + assertValidationSuccess((qInstance) -> + { + TableAutomationAction action = getAction0(qInstance); + action.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(1701))) + ); + }); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private TableAutomationAction getAction0(QInstance qInstance) + { + return qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().getActions().get(0); + } + + + /******************************************************************************* ** Run a little setup code on a qInstance; then validate it, and assert that it ** failed validation with reasons that match the supplied vararg-reasons (but allow @@ -690,7 +920,8 @@ class QInstanceValidatorTest if(!allowExtraReasons) { int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size(); - assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + "\nActual reasons: " + e.getReasons()); + assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + + "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", e.getReasons()) : "--")); } for(String reason : reasons) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 9058bc4a..5c358fbe 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -23,26 +23,39 @@ package com.kingsrook.qqq.backend.core.utils; import java.io.Serializable; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.automation.PollingAutomationProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; @@ -52,7 +65,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; +import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; @@ -61,6 +79,8 @@ import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockB import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -69,6 +89,8 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend *******************************************************************************/ public class TestUtils { + private static final Logger LOG = LogManager.getLogger(TestUtils.class); + public static final String DEFAULT_BACKEND_NAME = "default"; public static final String MEMORY_BACKEND_NAME = "memory"; @@ -83,11 +105,15 @@ public class TestUtils public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge"; public static final String TABLE_NAME_PERSON_FILE = "personFile"; + public static final String TABLE_NAME_PERSON_MEMORY = "personMemory"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; - public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type - public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type - public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type + public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type + public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type + public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type + public static final String POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS = "automationStatus"; + + public static final String POLLING_AUTOMATION = "polling"; @@ -104,9 +130,11 @@ public class TestUtils qInstance.addTable(defineTablePerson()); qInstance.addTable(definePersonFileTable()); + qInstance.addTable(definePersonMemoryTable()); qInstance.addTable(defineTableIdAndNameOnly()); qInstance.addTable(defineTableShape()); + qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); qInstance.addPossibleValueSource(defineShapePossibleValueSource()); qInstance.addPossibleValueSource(defineCustomPossibleValueSource()); @@ -117,6 +145,8 @@ public class TestUtils qInstance.addProcess(new BasicETLProcess().defineProcessMetaData()); qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData()); + qInstance.addAutomationProvider(definePollingAutomationProvider()); + defineApps(qInstance); return (qInstance); @@ -124,6 +154,18 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QAutomationProviderMetaData definePollingAutomationProvider() + { + return (new PollingAutomationProviderMetaData() + .withName(POLLING_AUTOMATION) + ); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -148,6 +190,21 @@ public class TestUtils + /******************************************************************************* + ** Define the "automationStatus" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineAutomationStatusPossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS) + .withType(QPossibleValueSourceType.ENUM) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY) + .withValuesFromEnum(AutomationStatus.values()); + } + + + /******************************************************************************* ** Define the "states" possible value source used in standard tests ** @@ -252,6 +309,30 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static QFieldMetaData standardQqqAutomationStatusField() + { + return (new QFieldMetaData("qqqAutomationStatus", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableAutomationDetails defineStandardAutomationDetails() + { + return (new QTableAutomationDetails() + .withProviderName(POLLING_AUTOMATION) + .withStatusTracking(new AutomationStatusTracking() + .withType(AutomationStatusTrackingType.FIELD_IN_TABLE) + .withFieldName("qqqAutomationStatus"))); + } + + + /******************************************************************************* ** Define the 'shape' table used in standard tests. *******************************************************************************/ @@ -289,6 +370,101 @@ public class TestUtils + /******************************************************************************* + ** Define a 3nd version of the 'person' table, backed by the in-memory backend + *******************************************************************************/ + public static QTableMetaData definePersonMemoryTable() + { + return (new QTableMetaData() + .withName(TABLE_NAME_PERSON_MEMORY) + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withFields(TestUtils.defineTablePerson().getFields())) + + .withField(standardQqqAutomationStatusField()) + .withAutomationDetails(defineStandardAutomationDetails() + .withAction(new TableAutomationAction() + .withName("checkAgeOnInsert") + .withTriggerEvent(TriggerEvent.POST_INSERT) + .withCodeReference(new QCodeReference(CheckAge.class)) + ) + .withAction(new TableAutomationAction() + .withName("logOnUpdatePerFilter") + .withTriggerEvent(TriggerEvent.POST_UPDATE) + .withFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Darin")))) + .withCodeReference(new QCodeReference(LogPersonUpdate.class)) + ) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CheckAge extends RecordAutomationHandler + { + public static String SUFFIX_FOR_MINORS = " (a minor)"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void execute(RecordAutomationInput recordAutomationInput) throws QException + { + LocalDate limitDate = LocalDate.now().minusYears(18); + List 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 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 *******************************************************************************/