mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Merged dev into feature/CE-876-develop-missing-widget-types
This commit is contained in:
@ -78,6 +78,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
private static Set<String> loggedUnauditableTableNames = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -210,6 +211,19 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
contextSuffix.append(" ").append(input.getAuditContext());
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// look for a context value place directly into the session //
|
||||
//////////////////////////////////////////////////////////////
|
||||
QSession qSession = QContext.getQSession();
|
||||
if(qSession != null)
|
||||
{
|
||||
String sessionContext = qSession.getValue(AUDIT_CONTEXT_FIELD_NAME);
|
||||
if(StringUtils.hasContent(sessionContext))
|
||||
{
|
||||
contextSuffix.append(" ").append(sessionContext);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// note process label (and a possible context from the process's state) if present //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -233,7 +247,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
///////////////////////////////////////////////////
|
||||
// use api label & version if present in session //
|
||||
///////////////////////////////////////////////////
|
||||
QSession qSession = QContext.getQSession();
|
||||
if(qSession != null)
|
||||
{
|
||||
String apiVersion = qSession.getValue("apiVersion");
|
||||
if(apiVersion != null)
|
||||
{
|
||||
@ -244,6 +259,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
}
|
||||
contextSuffix.append(" via ").append(apiLabel).append(" Version: ").append(apiVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return (contextSuffix.toString());
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.automation;
|
||||
|
||||
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||
|
||||
|
||||
@ -55,6 +56,30 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get instance by id
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static AutomationStatus getById(Integer id)
|
||||
{
|
||||
if(id == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
for(AutomationStatus value : AutomationStatus.values())
|
||||
{
|
||||
if(Objects.equals(value.id, id))
|
||||
{
|
||||
return (value);
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
**
|
||||
|
@ -22,30 +22,40 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.automation;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.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.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
|
||||
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.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.memoization.Memoization;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -55,19 +65,37 @@ public class RecordAutomationStatusUpdater
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(RecordAutomationStatusUpdater.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// feature flag - by default, will be true - before setting records to PENDING_UPDATE_AUTOMATIONS, //
|
||||
// we will fetch them (if we didn't take them in from the caller, which, UpdateAction does if its //
|
||||
// backend supports it), to check their current automationStatus - and if they are currently PENDING //
|
||||
// or RUNNING inserts or updates, we won't update them. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static boolean allowPreUpdateFetch = new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.recordAutomationStatusUpdater.allowPreUpdateFetch", "QQQ_RECORD_AUTOMATION_STATUS_UPDATER_ALLOW_PRE_UPDATE_FETCH", true);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// feature flag - by default, we'll memoize the check for triggers - but we can turn it off. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static boolean memoizeCheckForTriggers = new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.recordAutomationStatusUpdater.memoizeCheckForTriggers", "QQQ_RECORD_AUTOMATION_STATUS_UPDATER_MEMOIZE_CHECK_FOR_TRIGGERS", true);
|
||||
|
||||
private static Memoization<Key, Boolean> areThereTableTriggersForTableMemoization = new Memoization<Key, Boolean>().withTimeout(Duration.of(60, ChronoUnit.SECONDS));
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for a list of records from a table, set their automation status - based on
|
||||
** how the table is configured.
|
||||
*******************************************************************************/
|
||||
public static boolean setAutomationStatusInRecords(QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
|
||||
public static boolean setAutomationStatusInRecords(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus, QBackendTransaction transaction, List<QRecord> oldRecordList)
|
||||
{
|
||||
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
Set<Serializable> pkeysWeMayNotUpdate = new HashSet<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// In case an automation is running, and it updates records - don't let those records be marked //
|
||||
// as PENDING_UPDATE_AUTOMATIONS... this is meant to avoid having a record's automation update //
|
||||
@ -81,12 +109,60 @@ public class RecordAutomationStatusUpdater
|
||||
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"))
|
||||
if(className.contains(RecordAutomationStatusUpdater.class.getPackageName()) && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test") && !className.contains("Test$"))
|
||||
{
|
||||
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// if table uses field-in-table status tracking, then check the old records, //
|
||||
// before we set them to pending-updates, to avoid losing other pending or //
|
||||
// running status information. We will allow moving from OK or the 2 //
|
||||
// failed statuses into pending-updates - which seems right. //
|
||||
// This is added to fix cases where an update that comes in before insert //
|
||||
// -automations have run, will cause the pending-insert status to be missed. //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
|
||||
{
|
||||
try
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(oldRecordList))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we didn't get the oldRecordList as input (though UpdateAction should usually pass it?) //
|
||||
// then check feature-flag if we're allowed to do a lookup here & now. If so, then do. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(allowPreUpdateFetch)
|
||||
{
|
||||
List<Serializable> pkeysToLookup = records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
|
||||
oldRecordList = new QueryAction().execute(new QueryInput(table.getName())
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, pkeysToLookup)))
|
||||
.withTransaction(transaction)
|
||||
).getRecords();
|
||||
}
|
||||
}
|
||||
|
||||
for(QRecord freshRecord : CollectionUtils.nonNullList(oldRecordList))
|
||||
{
|
||||
Serializable recordStatus = freshRecord.getValue(automationDetails.getStatusTracking().getFieldName());
|
||||
if(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(recordStatus)
|
||||
|| AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(recordStatus)
|
||||
|| AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId().equals(recordStatus)
|
||||
|| AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(recordStatus))
|
||||
{
|
||||
Serializable primaryKey = freshRecord.getValue(table.getPrimaryKeyField());
|
||||
LOG.debug("May not update automation status", logPair("table", table.getName()), logPair("id", primaryKey), logPair("currentStatus", recordStatus), logPair("requestedStatus", automationStatus.getId()));
|
||||
pkeysWeMayNotUpdate.add(primaryKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
LOG.error("Error checking existing automation status before setting new automation status - more records will be updated than maybe should be...", qe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -98,21 +174,17 @@ public class RecordAutomationStatusUpdater
|
||||
automationStatus = AutomationStatus.OK;
|
||||
}
|
||||
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd //
|
||||
// lose that pending status, which would be a Bad Thing™... //
|
||||
// problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if(!pkeysWeMayNotUpdate.contains(record.getValue(table.getPrimaryKeyField())))
|
||||
{
|
||||
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
|
||||
// todo - another field - for the automation timestamp??
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
@ -188,11 +260,29 @@ public class RecordAutomationStatusUpdater
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(memoizeCheckForTriggers)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// as within the lookup method, error on the side of "yes, maybe there are triggers" //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
Optional<Boolean> result = areThereTableTriggersForTableMemoization.getResult(new Key(table, triggerEvent), key -> lookupIfThereAreTriggersForTable(table, triggerEvent));
|
||||
return result.orElse(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return lookupIfThereAreTriggersForTable(table, triggerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Boolean lookupIfThereAreTriggersForTable(QTableMetaData table, TriggerEvent triggerEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
///////////////////
|
||||
// todo - cache? //
|
||||
///////////////////
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(TableTrigger.TABLE_NAME);
|
||||
countInput.setFilter(new QQueryFilter(
|
||||
@ -207,6 +297,7 @@ public class RecordAutomationStatusUpdater
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the count query failed, we're a bit safer to err on the side of "yeah, there might be automations" //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Error looking if there are triggers for table", e, logPair("tableName", table.getName()));
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
@ -217,12 +308,12 @@ public class RecordAutomationStatusUpdater
|
||||
** for a list of records, update their automation status and actually Update the
|
||||
** backend as well.
|
||||
*******************************************************************************/
|
||||
public static void setAutomationStatusInRecordsAndUpdate(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) throws QException
|
||||
public static void setAutomationStatusInRecordsAndUpdate(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus, QBackendTransaction transaction) throws QException
|
||||
{
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
|
||||
{
|
||||
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus);
|
||||
boolean didSetStatusField = setAutomationStatusInRecords(table, records, automationStatus, transaction, null);
|
||||
if(didSetStatusField)
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
@ -237,6 +328,7 @@ public class RecordAutomationStatusUpdater
|
||||
.withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField()))
|
||||
.withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList());
|
||||
updateInput.setAreAllValuesBeingUpdatedTheSame(true);
|
||||
updateInput.setTransaction(transaction);
|
||||
updateInput.setOmitDmlAudit(true);
|
||||
|
||||
new UpdateAction().execute(updateInput);
|
||||
@ -250,4 +342,8 @@ public class RecordAutomationStatusUpdater
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private record Key(QTableMetaData table, TriggerEvent triggerEvent) {}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||
@ -60,6 +61,8 @@ import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
|
||||
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.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
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;
|
||||
@ -252,12 +255,11 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
try
|
||||
{
|
||||
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession();
|
||||
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status());
|
||||
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), tableActions.status());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error running automations", e);
|
||||
LOG.warn("Error running automations", e, logPair("tableName", tableActions.tableName()), logPair("status", tableActions.status()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -271,7 +273,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
/*******************************************************************************
|
||||
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
|
||||
*******************************************************************************/
|
||||
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus) throws QException
|
||||
public void processTableInsertOrUpdate(QTableMetaData table, AutomationStatus automationStatus) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// get the actions to run against this table in this automation status //
|
||||
@ -301,7 +303,9 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
|
||||
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType))
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
|
||||
QQueryFilter filter = new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId())));
|
||||
addOrderByToQueryFilter(table, automationStatus, filter);
|
||||
queryInput.setFilter(filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -322,7 +326,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}, () ->
|
||||
{
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
applyActionsToRecords(session, table, records, actions, automationStatus);
|
||||
applyActionsToRecords(table, records, actions, automationStatus);
|
||||
return (records.size());
|
||||
}
|
||||
);
|
||||
@ -330,6 +334,38 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static void addOrderByToQueryFilter(QTableMetaData table, AutomationStatus automationStatus, QQueryFilter filter)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// look for a field in the table with either create-date or modify-date behavior, //
|
||||
// based on if doing insert or update automations //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
DynamicDefaultValueBehavior dynamicDefaultValueBehavior = automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS) ? DynamicDefaultValueBehavior.CREATE_DATE : DynamicDefaultValueBehavior.MODIFY_DATE;
|
||||
Optional<QFieldMetaData> field = table.getFields().values().stream()
|
||||
.filter(f -> dynamicDefaultValueBehavior.equals(f.getBehaviorOrDefault(QContext.getQInstance(), DynamicDefaultValueBehavior.class)))
|
||||
.findFirst();
|
||||
|
||||
if(field.isPresent())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// if a create/modify date field was found, order by it (ascending) //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
filter.addOrderBy(new QFilterOrderBy(field.get().getName()));
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////
|
||||
// else, order by the primary key //
|
||||
////////////////////////////////////
|
||||
filter.addOrderBy(new QFilterOrderBy(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get the actions to run against a table in an automation status. both from
|
||||
** metaData and tableTriggers/data.
|
||||
@ -430,7 +466,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
** table's actions against them - IF they are found to match the action's filter
|
||||
** (assuming it has one - if it doesn't, then all records match).
|
||||
*******************************************************************************/
|
||||
private void applyActionsToRecords(QSession session, QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
|
||||
private void applyActionsToRecords(QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(records))
|
||||
{
|
||||
@ -440,7 +476,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
///////////////////////////////////////////////////
|
||||
// mark the records as RUNNING their automations //
|
||||
///////////////////////////////////////////////////
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToRunningStatusMap.get(automationStatus));
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, pendingToRunningStatusMap.get(automationStatus), null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach action - run it against the records (but only if they match the action's filter, if there is one) //
|
||||
@ -458,13 +494,15 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
////////////////////////////////////////
|
||||
// update status on all these records //
|
||||
////////////////////////////////////////
|
||||
if(anyActionsFailed)
|
||||
AutomationStatus statusToUpdateTo = anyActionsFailed ? pendingToFailedStatusMap.get(automationStatus) : AutomationStatus.OK;
|
||||
try
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToFailedStatusMap.get(automationStatus));
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, statusToUpdateTo, null);
|
||||
}
|
||||
else
|
||||
catch(Exception e)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, AutomationStatus.OK);
|
||||
LOG.warn("Error updating automationStatus after running automations", logPair("tableName", table), logPair("count", records.size()), logPair("status", statusToUpdateTo));
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -494,7 +532,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
|
||||
LOG.warn("Caught exception processing automations", e, logPair("tableName", table), logPair("action", action.getName()));
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -176,11 +177,13 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
return (totalString);
|
||||
}
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return ("<a href='" + tablePath + "?filter=" + JsonUtils.toJson(filter) + "'>" + totalString + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -192,6 +195,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return;
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter));
|
||||
}
|
||||
|
||||
@ -208,6 +212,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (null);
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "?filter=" + JsonUtils.toJson(filter));
|
||||
}
|
||||
|
||||
@ -224,6 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (null);
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
}
|
||||
|
||||
@ -326,6 +332,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
|
||||
String tablePath = QContext.getQInstance().getTablePath(tableName);
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
@ -47,12 +48,14 @@ 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.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -65,6 +68,8 @@ public class RunAdHocRecordScriptAction
|
||||
private Map<Integer, ScriptRevision> scriptRevisionCacheByScriptRevisionId = new HashMap<>();
|
||||
private Map<Integer, ScriptRevision> scriptRevisionCacheByScriptId = new HashMap<>();
|
||||
|
||||
private static Memoization<Integer, Script> scriptMemoizationById = new Memoization<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -85,6 +90,12 @@ public class RunAdHocRecordScriptAction
|
||||
throw (new QException("Script revision was not found."));
|
||||
}
|
||||
|
||||
Optional<Script> script = getScript(scriptRevision);
|
||||
|
||||
QContext.getQSession().setValue(DMLAuditAction.AUDIT_CONTEXT_FIELD_NAME, script.isPresent()
|
||||
? "via Script \"%s\"".formatted(script.get().getName())
|
||||
: "via Script id " + scriptRevision.getScriptId());
|
||||
|
||||
////////////////////////////
|
||||
// figure out the records //
|
||||
////////////////////////////
|
||||
@ -124,6 +135,10 @@ public class RunAdHocRecordScriptAction
|
||||
{
|
||||
output.setException(Optional.of(e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.getQSession().removeValue(DMLAuditAction.AUDIT_CONTEXT_FIELD_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -217,4 +232,44 @@ public class RunAdHocRecordScriptAction
|
||||
throw (new QException("Code reference did not contain a scriptRevision, scriptRevisionId, or scriptId"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Optional<Script> getScript(ScriptRevision scriptRevision)
|
||||
{
|
||||
if(scriptRevision == null || scriptRevision.getScriptId() == null)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return scriptMemoizationById.getResult(scriptRevision.getScriptId(), scriptId ->
|
||||
{
|
||||
try
|
||||
{
|
||||
QRecord scriptRecord = new GetAction().executeForRecord(new GetInput(Script.TABLE_NAME).withPrimaryKey(scriptRevision.getScriptId()));
|
||||
if(scriptRecord != null)
|
||||
{
|
||||
Script script = new Script(scriptRecord);
|
||||
scriptMemoizationById.storeResult(scriptRevision.getScriptId(), script);
|
||||
return (script);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("");
|
||||
}
|
||||
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -444,7 +444,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
*******************************************************************************/
|
||||
private void setAutomationStatusField(InsertInput insertInput)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getSession(), insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS, insertInput.getTransaction(), null);
|
||||
}
|
||||
|
||||
|
||||
|
@ -113,7 +113,6 @@ public class UpdateAction
|
||||
public UpdateOutput execute(UpdateInput updateInput) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(updateInput);
|
||||
setAutomationStatusField(updateInput);
|
||||
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
|
||||
@ -130,6 +129,16 @@ public class UpdateAction
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Optional<List<QRecord>> oldRecordList = fetchOldRecords(updateInput, updateInterface);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// allow caller to specify that we don't want to trigger automations. this isn't //
|
||||
// isn't expected to be used much - by design, only for the process that is meant to //
|
||||
// heal automation status, so that it can force us into status=Pending-inserts //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!updateInput.getOmitTriggeringAutomations())
|
||||
{
|
||||
setAutomationStatusField(updateInput, oldRecordList);
|
||||
}
|
||||
|
||||
performValidations(updateInput, oldRecordList, false);
|
||||
|
||||
////////////////////////////////////
|
||||
@ -561,9 +570,9 @@ public class UpdateAction
|
||||
/*******************************************************************************
|
||||
** If the table being updated uses an automation-status field, populate it now.
|
||||
*******************************************************************************/
|
||||
private void setAutomationStatusField(UpdateInput updateInput)
|
||||
private void setAutomationStatusField(UpdateInput updateInput, Optional<List<QRecord>> oldRecordList)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getSession(), updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS);
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS, updateInput.getTransaction(), oldRecordList.orElse(null));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
|
||||
@ -346,4 +347,37 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QFilterCriteria that = (QFilterCriteria) o;
|
||||
return Objects.equals(fieldName, that.fieldName) && operator == that.operator && Objects.equals(values, that.values) && Objects.equals(otherFieldName, that.otherFieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(fieldName, operator, values, otherFieldName);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -323,6 +324,18 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for adding a single subFilter
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QQueryFilter withSubFilter(QQueryFilter subFilter)
|
||||
{
|
||||
addSubFilter(subFilter);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -467,4 +480,36 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QQueryFilter that = (QQueryFilter) o;
|
||||
return Objects.equals(criteria, that.criteria) && Objects.equals(orderBys, that.orderBys) && booleanOperator == that.booleanOperator && Objects.equals(subFilters, that.subFilters) && Objects.equals(skip, that.skip) && Objects.equals(limit, that.limit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(criteria, orderBys, booleanOperator, subFilters, skip, limit);
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private Boolean areAllValuesBeingUpdatedTheSame = null;
|
||||
|
||||
private boolean omitTriggeringAutomations = false;
|
||||
private boolean omitDmlAudit = false;
|
||||
private String auditContext = null;
|
||||
|
||||
@ -321,4 +322,35 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for omitTriggeringAutomations
|
||||
*******************************************************************************/
|
||||
public boolean getOmitTriggeringAutomations()
|
||||
{
|
||||
return (this.omitTriggeringAutomations);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for omitTriggeringAutomations
|
||||
*******************************************************************************/
|
||||
public void setOmitTriggeringAutomations(boolean omitTriggeringAutomations)
|
||||
{
|
||||
this.omitTriggeringAutomations = omitTriggeringAutomations;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for omitTriggeringAutomations
|
||||
*******************************************************************************/
|
||||
public UpdateInput withOmitTriggeringAutomations(boolean omitTriggeringAutomations)
|
||||
{
|
||||
this.omitTriggeringAutomations = omitTriggeringAutomations;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -167,6 +168,11 @@ public class QRecord implements Serializable
|
||||
ArrayList<?> cloneList = new ArrayList<>(arrayList);
|
||||
clone.put(entry.getKey(), (V) cloneList);
|
||||
}
|
||||
else if(entry.getValue() instanceof LinkedList<?> linkedList)
|
||||
{
|
||||
LinkedList<?> cloneList = new LinkedList<>(linkedList);
|
||||
clone.put(entry.getKey(), (V) cloneList);
|
||||
}
|
||||
else if(entry.getValue() instanceof LinkedHashMap<?, ?> linkedHashMap)
|
||||
{
|
||||
LinkedHashMap<?, ?> cloneMap = new LinkedHashMap<>(linkedHashMap);
|
||||
|
@ -51,6 +51,7 @@ public class HtmlWrapper implements Serializable
|
||||
public static final String STYLE_INDENT_2 = "padding-left: 2rem; ";
|
||||
public static final String STYLE_FLOAT_RIGHT = "float: right; ";
|
||||
public static final String STYLE_RED = "color: red; ";
|
||||
public static final String STYLE_YELLOW = "color: #bfb743; ";
|
||||
|
||||
|
||||
|
||||
|
@ -142,6 +142,19 @@ public class QSession implements Serializable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void removeValue(String key)
|
||||
{
|
||||
if(values != null)
|
||||
{
|
||||
values.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -22,13 +22,10 @@
|
||||
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
|
||||
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;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -45,17 +42,6 @@ public class MemoryUpdateAction extends AbstractMemoryAction implements UpdateIn
|
||||
{
|
||||
try
|
||||
{
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
Instant now = Instant.now();
|
||||
|
||||
for(QRecord record : updateInput.getRecords())
|
||||
{
|
||||
///////////////////////////////////////////
|
||||
// todo .. better (not hard-coded names) //
|
||||
///////////////////////////////////////////
|
||||
setValueIfTableHasField(record, table, "modifyDate", now, false);
|
||||
}
|
||||
|
||||
UpdateOutput updateOutput = new UpdateOutput();
|
||||
updateOutput.setRecords(MemoryRecordStore.getInstance().update(updateInput, true));
|
||||
return (updateOutput);
|
||||
|
@ -0,0 +1,298 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
|
||||
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.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.query.expressions.NowWithOffset;
|
||||
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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
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.processes.NoCodeWidgetFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MultiLevelMapHelper;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to find records with a bad automation status, and repair them.
|
||||
**
|
||||
** Bad status are defined as:
|
||||
** - failed insert or updates.
|
||||
** - running insert or updates for more than X minutes (see input field value).
|
||||
**
|
||||
** Repair in this case means resetting their status to the corresponding (e.g.,
|
||||
** insert/update) pending status.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class HealBadRecordAutomationStatusesProcessStep implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "HealBadRecordAutomationStatusesProcess";
|
||||
|
||||
private static final QLogger LOG = QLogger.getLogger(HealBadRecordAutomationStatusesProcessStep.class);
|
||||
|
||||
private static final Map<Integer, Integer> statusUpdateMap = Map.of(
|
||||
AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(),
|
||||
AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(),
|
||||
AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(),
|
||||
AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId()
|
||||
);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QProcessMetaData processMetaData = new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withStepList(List.of(
|
||||
new QFrontendStepMetaData()
|
||||
.withName("input")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
||||
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
|
||||
.withFormField(new QFieldMetaData("minutesOldLimit", QFieldType.INTEGER).withDefaultValue(60)),
|
||||
new QBackendStepMetaData()
|
||||
.withName("run")
|
||||
.withCode(new QCodeReference(getClass())),
|
||||
new QFrontendStepMetaData()
|
||||
.withName("output")
|
||||
|
||||
.withComponent(new NoCodeWidgetFrontendComponentMetaData()
|
||||
.withOutput(new WidgetHtmlLine()
|
||||
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
|
||||
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_YELLOW))
|
||||
.withVelocityTemplate("<b>Warning:</b>"))
|
||||
.withOutput(new WidgetHtmlLine()
|
||||
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
|
||||
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_INDENT_1))
|
||||
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_YELLOW))
|
||||
.withVelocityTemplate("""
|
||||
<ul>
|
||||
#foreach($string in $warnings)
|
||||
<li>$string</li>
|
||||
#end
|
||||
</ul>
|
||||
""")))
|
||||
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
||||
.withViewField(new QFieldMetaData("totalRecordsUpdated", QFieldType.INTEGER) /* todo - didn't display commas... .withDisplayFormat(DisplayFormat.COMMAS) */)
|
||||
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.RECORD_LIST))
|
||||
.withRecordListField(new QFieldMetaData("tableName", QFieldType.STRING))
|
||||
.withRecordListField(new QFieldMetaData("badStatus", QFieldType.STRING))
|
||||
.withRecordListField(new QFieldMetaData("count", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) /* todo - didn't display commas... */)
|
||||
|
||||
));
|
||||
|
||||
return (processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
int recordsUpdated = 0;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// if a table name is given, validate it, and run for just that table //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
ArrayList<String> warnings = new ArrayList<>();
|
||||
if(StringUtils.hasContent(tableName))
|
||||
{
|
||||
if(!QContext.getQInstance().getTables().containsKey(tableName))
|
||||
{
|
||||
throw (new QException("Unrecognized table name: " + tableName));
|
||||
}
|
||||
|
||||
recordsUpdated += processTable(tableName, runBackendStepInput, runBackendStepOutput, warnings);
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// else, try to run for all tables that have an automation status field //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
for(QTableMetaData table : QContext.getQInstance().getTables().values())
|
||||
{
|
||||
recordsUpdated += processTable(table.getName(), runBackendStepInput, runBackendStepOutput, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
runBackendStepOutput.addValue("totalRecordsUpdated", recordsUpdated);
|
||||
runBackendStepOutput.addValue("warnings", warnings);
|
||||
runBackendStepOutput.addValue("warningCount", warnings.size());
|
||||
|
||||
if(CollectionUtils.nullSafeIsEmpty(runBackendStepOutput.getRecords()))
|
||||
{
|
||||
runBackendStepOutput.addRecord(new QRecord()
|
||||
.withValue("tableName", "--")
|
||||
.withValue("badStatus", "--")
|
||||
.withValue("count", "0"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private int processTable(String tableName, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<String> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
Integer minutesOldLimit = Objects.requireNonNullElse(runBackendStepInput.getValueInteger("minutesOldLimit"), 60);
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// only process tables w/ automation details w/ a status tracking field //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
if(table != null && table.getAutomationDetails() != null && table.getAutomationDetails().getStatusTracking() != null && StringUtils.hasContent(table.getAutomationDetails().getStatusTracking().getFieldName()))
|
||||
{
|
||||
String automationStatusFieldName = table.getAutomationDetails().getStatusTracking().getFieldName();
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// find the modify-date field on the table //
|
||||
/////////////////////////////////////////////
|
||||
String modifyDateFieldName = null;
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
if(DynamicDefaultValueBehavior.MODIFY_DATE.equals(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)))
|
||||
{
|
||||
modifyDateFieldName = field.getName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(modifyDateFieldName == null)
|
||||
{
|
||||
warnings.add("Could not find a Modify Date field on table: " + tableName);
|
||||
LOG.info("Couldn't find a MODIFY_DATE field on table", logPair("tableName", tableName));
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// query for records either FAILED, or RUNNING w/ modify date too old //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
|
||||
.withSubFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId())))
|
||||
.withSubFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId()))
|
||||
.withCriteria(new QFilterCriteria(modifyDateFieldName, QCriteriaOperator.LESS_THAN, NowWithOffset.minus(minutesOldLimit, ChronoUnit.MINUTES))))
|
||||
);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach record found, add it to list of records to be updated - mapping status to appropriate pending status //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
Map<String, Integer> countByStatus = new HashMap<>();
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Integer badAutomationStatusId = record.getValueInteger(automationStatusFieldName);
|
||||
Integer updateStatus = statusUpdateMap.get(badAutomationStatusId);
|
||||
if(updateStatus != null)
|
||||
{
|
||||
AutomationStatus badStatus = AutomationStatus.getById(badAutomationStatusId);
|
||||
if(badStatus != null)
|
||||
{
|
||||
MultiLevelMapHelper.getOrPutAndIncrement(countByStatus, badStatus.getLabel());
|
||||
}
|
||||
|
||||
recordsToUpdate.add(new QRecord()
|
||||
.withValue(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField()))
|
||||
.withValue(automationStatusFieldName, updateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
if(!recordsToUpdate.isEmpty())
|
||||
{
|
||||
LOG.info("Healing bad record automation statuses", logPair("tableName", tableName), logPair("count", recordsToUpdate.size()));
|
||||
new UpdateAction().execute(new UpdateInput(tableName).withRecords(recordsToUpdate).withOmitTriggeringAutomations(true));
|
||||
}
|
||||
|
||||
for(Map.Entry<String, Integer> entry : countByStatus.entrySet())
|
||||
{
|
||||
runBackendStepOutput.addRecord(new QRecord()
|
||||
.withValue("tableName", tableName)
|
||||
.withValue("badStatus", entry.getKey())
|
||||
.withValue("count", entry.getValue()));
|
||||
}
|
||||
|
||||
return (recordsToUpdate.size());
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
warnings.add("Error processing table: " + tableName + ": " + ExceptionUtils.getTopAndBottomMessages(e));
|
||||
LOG.warn("Error processing table for bad automation statuses", e, logPair("tableName, name"));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunner;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
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.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to manually run table automations, for a table.
|
||||
**
|
||||
** Useful, maybe, for an e2e test. Or, if you don't want jobs to be running,
|
||||
** but want to run automations by-hand, for some reason.
|
||||
**
|
||||
** In the future, this class could take a param to only do inserts or updates.
|
||||
**
|
||||
** Also, right now, only records that are Pending automations will be run -
|
||||
** again, that could be changed, presumably (take a list of records, always run, etc...)
|
||||
*******************************************************************************/
|
||||
public class RunTableAutomationsProcessStep implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "RunTableAutomationsProcess";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QProcessMetaData processMetaData = new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withStepList(List.of(
|
||||
new QFrontendStepMetaData()
|
||||
.withName("input")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
||||
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withIsRequired(true).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
|
||||
.withFormField(new QFieldMetaData("automationProviderName", QFieldType.STRING)),
|
||||
new QBackendStepMetaData()
|
||||
.withName("run")
|
||||
.withCode(new QCodeReference(getClass())),
|
||||
new QFrontendStepMetaData()
|
||||
.withName("output")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
||||
.withViewField(new QFieldMetaData("ok", QFieldType.STRING))
|
||||
));
|
||||
|
||||
return (processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// get tableName param (since this process is not table-specific) //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
if(!StringUtils.hasContent(tableName))
|
||||
{
|
||||
throw (new QException("Missing required input value: tableName"));
|
||||
}
|
||||
|
||||
if(!QContext.getQInstance().getTables().containsKey(tableName))
|
||||
{
|
||||
throw (new QException("Unrecognized table name: " + tableName));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get the automation provider name to use - either as the only-one-in-instance, or via param //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String automationProviderName = runBackendStepInput.getValueString("automationProviderName");
|
||||
if(!StringUtils.hasContent(automationProviderName))
|
||||
{
|
||||
Map<String, QAutomationProviderMetaData> automationProviders = CollectionUtils.nonNullMap(qInstance.getAutomationProviders());
|
||||
if(automationProviders.size() == 1)
|
||||
{
|
||||
automationProviderName = automationProviders.keySet().iterator().next();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Missing required input value: automationProviderName (and there is not exactly 1 in the active instance)"));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// run automations for the requested table //
|
||||
/////////////////////////////////////////////
|
||||
List<PollingAutomationPerTableRunner.TableActionsInterface> tableActions = PollingAutomationPerTableRunner.getTableActions(qInstance, automationProviderName);
|
||||
for(PollingAutomationPerTableRunner.TableActionsInterface tableAction : tableActions)
|
||||
{
|
||||
if(tableName.equals(tableAction.tableName()))
|
||||
{
|
||||
PollingAutomationPerTableRunner pollingAutomationPerTableRunner = new PollingAutomationPerTableRunner(qInstance, automationProviderName, () -> QContext.getQSession(), tableAction);
|
||||
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
|
||||
}
|
||||
}
|
||||
|
||||
runBackendStepOutput.addValue("ok", "true");
|
||||
}
|
||||
|
||||
}
|
@ -26,7 +26,6 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.AuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.RunAdHocRecordScriptAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
@ -133,11 +132,6 @@ public class RunRecordScriptLoadStep extends AbstractLoadStep implements Process
|
||||
throw (new QException("Could not find script by id: " + scriptId));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set an "audit context" - so any DML executed during the script will include the note of what script was running. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
runBackendStepInput.addValue(DMLAuditAction.AUDIT_CONTEXT_FIELD_NAME, "via Script \"" + script.getValue("name") + "\"");
|
||||
|
||||
String tableName = script.getValueString("tableName");
|
||||
|
||||
RunAdHocRecordScriptInput input = new RunAdHocRecordScriptInput();
|
||||
|
@ -164,4 +164,23 @@ public class ExceptionUtils
|
||||
|
||||
return (StringUtils.join("; ", messages));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the messages from the top & bottoms (root) of an exception.
|
||||
**
|
||||
** If there's no root, just return the top (e.g., parameter)'s message.
|
||||
** If they are both found, put ": " between them.
|
||||
*******************************************************************************/
|
||||
public static String getTopAndBottomMessages(Exception e)
|
||||
{
|
||||
String rs = e.getMessage();
|
||||
Throwable rootException = getRootException(e);
|
||||
if(rootException != e)
|
||||
{
|
||||
rs += ": " + rootException.getMessage();
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,363 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.utils;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Class to help deduplicate redundant criteria in filters.
|
||||
**
|
||||
** Original use-case is for making more clean url links out of filters.
|
||||
**
|
||||
** Does not (at this time) look into sub-filters at all, or support any "OR"
|
||||
** filters other than the most basic (a=1 OR a=1).
|
||||
**
|
||||
** Also, other than for completely redundant criteria (e.g., a>1 and a>1) only
|
||||
* works on a limited subset of criteria operators (EQUALS, NOT_EQUALS, IN, and NOT_IN)
|
||||
*******************************************************************************/
|
||||
public class QQueryFilterDeduper
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QQueryFilterDeduper.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QQueryFilter dedupeFilter(QQueryFilter filter)
|
||||
{
|
||||
if(filter == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// track (just for logging) if we failed or if we did any good //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
List<String> log = new ArrayList<>();
|
||||
boolean fail = false;
|
||||
boolean didAnyGood = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// always create a clone to be returned. this is especially useful because, //
|
||||
// the clone's lists will be ArrayLists, which are mutable - since some of the deduping //
|
||||
// involves manipulating value lists. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter rs = filter.clone();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// general strategy is: //
|
||||
// iterate over criteria, possibly removing the one the iterator is pointing at, //
|
||||
// if we are able to somehow merge it into other criteria we've already seen. //
|
||||
// the others-we've-seen will be tracked in the criteriaByFieldName listing hash. //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<String, QFilterCriteria> criteriaByFieldName = new ListingHash<>();
|
||||
Iterator<QFilterCriteria> iterator = rs.getCriteria().iterator();
|
||||
while(iterator.hasNext())
|
||||
{
|
||||
QFilterCriteria criteria = iterator.next();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// first thing to check is, have we seen any other criteria for this field - if so - try to do some de-duping. //
|
||||
// note that, any time we do a remove, we'll need to do a continue - to avoid adding the now-removed criteria //
|
||||
// to the listing hash //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(criteriaByFieldName.containsKey(criteria.getFieldName()))
|
||||
{
|
||||
List<QFilterCriteria> others = criteriaByFieldName.get(criteria.getFieldName());
|
||||
QFilterCriteria other = others.get(0);
|
||||
|
||||
if(others.size() == 1 && other.equals(criteria))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we've only see 1 other criteria for this field so far, and this one is an exact match, then remove this one. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
log.add(String.format("Remove duplicate criteria [%s]", criteria));
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else - if there's still just 1 other, and it's an AND query - then apply some basic //
|
||||
// logic-merging operations, based on the pair of criteria operators //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(others.size() == 1 && QQueryFilter.BooleanOperator.AND.equals(filter.getBooleanOperator()))
|
||||
{
|
||||
if((NOT_EQUALS.equals(other.getOperator()) || NOT_IN.equals(other.getOperator())) && EQUALS.equals(criteria.getOperator()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if we previously saw a not-equals or not-in, and now we see an equals //
|
||||
// and the value from the EQUALS isn't in the not-in list //
|
||||
// then replace the not-equals with the equals //
|
||||
// then just discard this equals //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
if(other.getValues().contains(criteria.getValues().get(0)))
|
||||
{
|
||||
log.add("Contradicting NOT_EQUALS/NOT_IN and EQUALS");
|
||||
fail = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
other.setOperator(criteria.getOperator());
|
||||
other.setValues(criteria.getValues());
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Replace a not-equals or not-in superseded by an equals");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(EQUALS.equals(other.getOperator()) && (NOT_EQUALS.equals(criteria.getOperator()) || NOT_IN.equals(criteria.getOperator())))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// if we previously saw an equals, and now we see a not-equals or a not-in //
|
||||
// and the value from the EQUALS isn't in the not-in list //
|
||||
// then just discard this not-equals //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
if(criteria.getValues().contains(other.getValues().get(0)))
|
||||
{
|
||||
log.add("Contradicting NOT_EQUALS/NOT_IN and EQUALS");
|
||||
fail = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Remove a redundant not-equals");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(NOT_EQUALS.equals(other.getOperator()) && IN.equals(criteria.getOperator()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we previously saw a not-equals, and now we see an IN //
|
||||
// then replace the not-equals with the IN (making sure the not-equals value isn't in the in-list) //
|
||||
// then just discard this equals //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Serializable notEqualsValue = other.getValues().get(0);
|
||||
List<Serializable> inValues = new ArrayList<>(criteria.getValues());
|
||||
inValues.remove(notEqualsValue);
|
||||
if(inValues.isEmpty())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if the only in-value was the not-equal value, then... i don't know, don't try //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
log.add("Contradicting IN and NOT_EQUAL");
|
||||
fail = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// else, we can proceed by replacing the not-equals with the in //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
other.setOperator(criteria.getOperator());
|
||||
other.setValues(criteria.getValues());
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Replace superseded not-equals (removing its value from in-list)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(IN.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// if we previously saw an in, and now we see a not-equals //
|
||||
// discard the not-equals (removing its value from the in-list) //
|
||||
// then just discard this not-equals //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
Serializable notEqualsValue = criteria.getValues().get(0);
|
||||
List<Serializable> inValues = new ArrayList<>(other.getValues());
|
||||
inValues.remove(notEqualsValue);
|
||||
if(inValues.isEmpty())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if the only in-value was the not-equal value, then... i don't know, don't try //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
log.add("Contradicting IN and NOT_EQUAL");
|
||||
fail = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// else, we can proceed by replacing the not-equals with the in //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Remove redundant not-equals (removing its value from in-list)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(NOT_EQUALS.equals(other.getOperator()) && NOT_IN.equals(criteria.getOperator()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we previously saw a not-equals, and now we see a not-in //
|
||||
// we can change the not-equals to the not-in, and make sure it's value is in the list //
|
||||
// then just discard this not-in //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
Serializable originalNotEqualsValue = other.getValues().get(0);
|
||||
other.setOperator(criteria.getOperator());
|
||||
other.setValues(criteria.getValues());
|
||||
if(!other.getValues().contains(originalNotEqualsValue))
|
||||
{
|
||||
other.getValues().add(originalNotEqualsValue);
|
||||
}
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Replace superseded not-equals with not-in");
|
||||
continue;
|
||||
}
|
||||
else if(NOT_IN.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we previously saw a not-in, and now we see a not-equals //
|
||||
// we can discard this not-equals, and just make sure its value is in the not-in list //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
Serializable originalNotEqualsValue = criteria.getValues().get(0);
|
||||
if(!other.getValues().contains(originalNotEqualsValue))
|
||||
{
|
||||
other.getValues().add(originalNotEqualsValue);
|
||||
}
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Remove not-equals, absorbing into not-in");
|
||||
continue;
|
||||
}
|
||||
else if(NOT_IN.equals(other.getOperator()) && NOT_IN.equals(criteria.getOperator()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// for multiple not-ins, just merge their values (as a union) //
|
||||
////////////////////////////////////////////////////////////////
|
||||
for(Serializable value : criteria.getValues())
|
||||
{
|
||||
if(!other.getValues().contains(value))
|
||||
{
|
||||
other.getValues().add(value);
|
||||
}
|
||||
}
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Merging not-ins");
|
||||
continue;
|
||||
}
|
||||
else if(IN.equals(other.getOperator()) && IN.equals(criteria.getOperator()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// for multiple not-ins, just merge their values (as an intersection) //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
Set<Serializable> otherValues = new HashSet<>(other.getValues());
|
||||
Set<Serializable> criteriaValues = new HashSet<>(criteria.getValues());
|
||||
otherValues.retainAll(criteriaValues);
|
||||
if(otherValues.isEmpty())
|
||||
{
|
||||
log.add("Contradicting IN lists (no values)");
|
||||
fail = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
other.setValues(new ArrayList<>(otherValues));
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Merging not-ins");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(NOT_EQUALS.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we have 2 not-equals, we can merge them in a not-in //
|
||||
// we can assume their values are different, else they'd have been equals up above //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
other.setOperator(NOT_IN);
|
||||
other.setValues(new ArrayList<>(List.of(other.getValues().get(0), criteria.getValues().get(0))));
|
||||
iterator.remove();
|
||||
didAnyGood = true;
|
||||
log.add("Merge two not-equals as not-in");
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
log.add("Fail because unhandled operator pair");
|
||||
fail = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.add("Fail because > 1 other or operator: OR");
|
||||
fail = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we reach here (e.g., no continue), then assuming we didn't remove the criteria, add it to the listing hash. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
criteriaByFieldName.add(criteria.getFieldName(), criteria);
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
// log based on booleans //
|
||||
///////////////////////////
|
||||
if(fail && didAnyGood)
|
||||
{
|
||||
LOG.info("Partially unsuccessful dedupe of filter", logPair("original", filter), logPair("deduped", rs), logPair("log", log));
|
||||
}
|
||||
else if(fail)
|
||||
{
|
||||
LOG.info("Unsuccessful dedupe of filter", logPair("filter", filter), logPair("log", log));
|
||||
}
|
||||
else if(didAnyGood)
|
||||
{
|
||||
LOG.debug("Successful dedupe of filter", logPair("original", filter), logPair("deduped", rs), logPair("log", log));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.debug("No duplicates in filter, so nothing to dedupe", logPair("original", filter));
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error de-duping filter", e, logPair("filter", filter));
|
||||
return (filter.clone());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -30,6 +30,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -58,8 +59,16 @@ public class Memoization<K, V>
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the memoized Value for a given input Key.
|
||||
**
|
||||
** But note, this looks the same to the caller, whether the key just wasn't in
|
||||
** the internal map (e.g., had never been looked up), or if it was previously looked
|
||||
** up, and that returned null. In either case, the optional will be empty.
|
||||
**
|
||||
** See getMemoizedResult for where we can tell the difference (and we would
|
||||
** generally want to call that.
|
||||
*******************************************************************************/
|
||||
@Deprecated
|
||||
public Optional<V> getResult(K key)
|
||||
{
|
||||
MemoizedResult<V> result = map.get(key);
|
||||
@ -77,7 +86,57 @@ public class Memoization<K, V>
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the memoized Value for a given input Key - computing it if it wasn't previously
|
||||
** memoized (or expired).
|
||||
**
|
||||
** In here, if the optional is empty, it means the value is null (whether that
|
||||
** came form memoization, or from the lookupFunction, you don't care - the answer
|
||||
** is null).
|
||||
*******************************************************************************/
|
||||
public Optional<V> getResult(K key, UnsafeFunction<K, V, ?> lookupFunction)
|
||||
{
|
||||
MemoizedResult<V> result = map.get(key);
|
||||
if(result != null)
|
||||
{
|
||||
if(result.getTime().isAfter(Instant.now().minus(timeout)))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// ok, we have a memoized value, and it's not expired, so we can return it. //
|
||||
// of course, it might be a memoized null, so we use .ofNullable. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
return (Optional.ofNullable(result.getResult()));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - either we never memoized this key, or it's expired, so, apply the lookup function, //
|
||||
// store the result, and then return the value (in an Optional.ofNullable) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
V value = lookupFunction.apply(key);
|
||||
storeResult(key, value);
|
||||
return (Optional.ofNullable(value));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Uncaught Exception while executing a Memoization lookupFunction (to avoid this log, add a catch in the lookupFunction)", e);
|
||||
storeResult(key, null);
|
||||
return (Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get a memoized result, optionally containing a Value, for a given input Key.
|
||||
**
|
||||
** In this method (contrasted with getResult), if the returned Optional is empty,
|
||||
** it means that we haven't ever looked up or memoized the key (or it's expired).
|
||||
**
|
||||
** If the returned Optional is not empty, then it means we've memoized something
|
||||
** (and it's not expired) - so if the Value from the MemoizedResult is null,
|
||||
** then null is the proper memoized value.
|
||||
*******************************************************************************/
|
||||
public Optional<MemoizedResult<V>> getMemoizedResult(K key)
|
||||
{
|
||||
@ -86,7 +145,7 @@ public class Memoization<K, V>
|
||||
{
|
||||
if(result.getTime().isAfter(Instant.now().minus(timeout)))
|
||||
{
|
||||
return (Optional.ofNullable(result));
|
||||
return (Optional.of(result));
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,4 +240,47 @@ public class Memoization<K, V>
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timeout
|
||||
*******************************************************************************/
|
||||
public Duration getTimeout()
|
||||
{
|
||||
return (this.timeout);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for timeout
|
||||
*******************************************************************************/
|
||||
public Memoization<K, V> withTimeout(Duration timeout)
|
||||
{
|
||||
this.timeout = timeout;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxSize
|
||||
*******************************************************************************/
|
||||
public Integer getMaxSize()
|
||||
{
|
||||
return (this.maxSize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for maxSize
|
||||
*******************************************************************************/
|
||||
public Memoization<K, V> withMaxSize(Integer maxSize)
|
||||
{
|
||||
this.maxSize = maxSize;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.automation.polling;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
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.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
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.code.QCodeReference;
|
||||
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.utils.TestUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunnerTest.runAllTableActions;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test for the case where:
|
||||
** - inserting into a main table and a child table, and the child table has a
|
||||
** post-insert customizer, which mo
|
||||
*******************************************************************************/
|
||||
public class PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest extends BaseTest
|
||||
{
|
||||
private static boolean didFailInThread = false;
|
||||
|
||||
static
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we can set this property to revert to the behavior that existed before this test was written. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// System.setProperty("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "true");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void beforeEach()
|
||||
{
|
||||
didFailInThread = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
////////////////////////////////////
|
||||
// add automations to order table //
|
||||
////////////////////////////////////
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_ORDER)
|
||||
.withField(TestUtils.standardQqqAutomationStatusField())
|
||||
.withAutomationDetails(TestUtils.defineStandardAutomationDetails()
|
||||
.withAction(new TableAutomationAction()
|
||||
.withName("orderPostInsertAction")
|
||||
.withTriggerEvent(TriggerEvent.POST_INSERT)
|
||||
.withCodeReference(new QCodeReference(OrderPostInsertAndUpdateAction.class)))
|
||||
.withAction(new TableAutomationAction()
|
||||
.withName("orderPostUpdateAction")
|
||||
.withTriggerEvent(TriggerEvent.POST_UPDATE)
|
||||
.withCodeReference(new QCodeReference(OrderPostInsertAndUpdateAction.class))));
|
||||
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("orderNo", "10101").withValue("total", new BigDecimal(1))));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// make sure the order is in pending-inserts status //
|
||||
//////////////////////////////////////////////////////
|
||||
{
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
assertEquals(new BigDecimal(1), order.getValueBigDecimal("total"));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run automations - that should update the order via the automation - but leave status as OK //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
runAllTableActions(QContext.getQInstance());
|
||||
assertFalse(didFailInThread, "A failure condition happened in the automation sub-thread. Check System.out for message.");
|
||||
|
||||
{
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// now update the order, verify status moves to pending-updates //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_ORDER).withRecord(new QRecord()
|
||||
.withValue("id", 1)
|
||||
.withValue("storeId", "x")));
|
||||
|
||||
{
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
assertEquals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
|
||||
assertEquals("x", order.getValueString("storeId"));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run automations - that should update the order via the automation - but leave status as OK //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
runAllTableActions(QContext.getQInstance());
|
||||
assertFalse(didFailInThread, "A failure condition happened in the automation sub-thread. Check System.out for message.");
|
||||
|
||||
{
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
assertEquals(new BigDecimal(3), order.getValueBigDecimal("total"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class OrderPostInsertAndUpdateAction extends RecordAutomationHandler
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void execute(RecordAutomationInput recordAutomationInput) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// launch a new thread, to make sure we avoid the "stack contains automations" check in RecordAutomationStatusUpdater //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
CapturedContext capturedContext = QContext.capture();
|
||||
for(QRecord record : recordAutomationInput.getRecordList())
|
||||
{
|
||||
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
|
||||
Future<?> submit = service.submit(() ->
|
||||
{
|
||||
QContext.init(capturedContext);
|
||||
try
|
||||
{
|
||||
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_ORDER).withRecord(new QRecord()
|
||||
.withValue("id", record.getValue("id"))
|
||||
.withValue("total", record.getValueBigDecimal("total").add(new BigDecimal(1)))
|
||||
));
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// make sure that update action didn't change the order's status //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
if(Objects.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName())))
|
||||
{
|
||||
System.out.println("Failing test - expected status to not be [PENDING_UPDATE_AUTOMATIONS], but it was.");
|
||||
didFailInThread = true;
|
||||
}
|
||||
assertNotEquals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
});
|
||||
|
||||
while(!submit.isDone())
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.automation.polling;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
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.customizers.AbstractPostInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
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.utils.TestUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunnerTest.runAllTableActions;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test for the case where:
|
||||
** - inserting into a main table and a child table, and the child table has a
|
||||
** post-insert customizer, which mo
|
||||
*******************************************************************************/
|
||||
public class PollingAutomationPerTableRunnerChildPostInsertCustomizerTest extends BaseTest
|
||||
{
|
||||
|
||||
static
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we can set this property to revert to the behavior that existed before this test was written. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// System.setProperty("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "true");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
////////////////////////////////////
|
||||
// add automations to order table //
|
||||
////////////////////////////////////
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_ORDER)
|
||||
.withField(TestUtils.standardQqqAutomationStatusField())
|
||||
.withAutomationDetails(TestUtils.defineStandardAutomationDetails()
|
||||
.withAction(new TableAutomationAction()
|
||||
.withName("orderPostInsertAction")
|
||||
.withTriggerEvent(TriggerEvent.POST_INSERT)
|
||||
.withCodeReference(new QCodeReference(OrderPostInsertAction.class))
|
||||
));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// add a post-insert customizer to line-ite table (child of order table) //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_LINE_ITEM)
|
||||
.withCustomizer(TableCustomizers.POST_INSERT_RECORD, new QCodeReference(LineItemPostInsertCustomizer.class));
|
||||
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(new QRecord()
|
||||
.withValue("orderNo", "10101")
|
||||
.withAssociatedRecord("orderLine", new QRecord()
|
||||
.withValue("sku", "ABC")
|
||||
.withValue("quantity", 1))));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure the order is in pending-inserts status (at one time, a bug meant that it wouldn't have been...) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
assertEquals(new BigDecimal(1), order.getValueBigDecimal("total"));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run automations - that should... insert a second line item, but should leave the order in //
|
||||
// automation-status = OK, to avoid perpetual re-running //
|
||||
// the line-item post-inserter should run a second time, making the order's total = 2 //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
runAllTableActions(QContext.getQInstance());
|
||||
|
||||
{
|
||||
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
|
||||
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
|
||||
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class OrderPostInsertAction extends RecordAutomationHandler
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void execute(RecordAutomationInput recordAutomationInput) throws QException
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// add a new line item to the orders //
|
||||
///////////////////////////////////////
|
||||
List<QRecord> lineItemsToInsert = new ArrayList<>();
|
||||
for(QRecord record : recordAutomationInput.getRecordList())
|
||||
{
|
||||
lineItemsToInsert.add(new QRecord()
|
||||
.withValue("orderId", record.getValue("id"))
|
||||
.withValue("sku", UUID.randomUUID())
|
||||
.withValue("quantity", 1)
|
||||
);
|
||||
}
|
||||
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_LINE_ITEM).withRecords(lineItemsToInsert));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class LineItemPostInsertCustomizer extends AbstractPostInsertCustomizer
|
||||
{
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||
{
|
||||
//////////////////////////////////
|
||||
// count line items by order id //
|
||||
//////////////////////////////////
|
||||
Set<Serializable> orderIds = records.stream().map(r -> r.getValue("orderId")).collect(Collectors.toSet());
|
||||
|
||||
GroupBy groupByOrderId = new GroupBy(QFieldType.STRING, "orderId");
|
||||
Aggregate countId = new Aggregate("id", AggregateOperator.COUNT);
|
||||
|
||||
AggregateInput aggregateInput = new AggregateInput();
|
||||
aggregateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
aggregateInput.setFilter(new QQueryFilter(new QFilterCriteria("orderId", QCriteriaOperator.IN, orderIds)));
|
||||
aggregateInput.withGroupBy(groupByOrderId);
|
||||
aggregateInput.withAggregate(countId);
|
||||
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
|
||||
Map<Integer, Integer> countByOrderId = new HashMap<>();
|
||||
for(AggregateResult result : aggregateOutput.getResults())
|
||||
{
|
||||
countByOrderId.put(ValueUtils.getValueAsInteger(result.getGroupByValue(groupByOrderId)), ValueUtils.getValueAsInteger(result.getAggregateValue(countId)));
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// update the order total fields //
|
||||
// s/b in bulk, but, meh //
|
||||
///////////////////////////////////
|
||||
for(Integer orderId : countByOrderId.keySet())
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
updateInput.setRecords(List.of(new QRecord()
|
||||
.withValue("id", orderId)
|
||||
.withValue("total", new BigDecimal(countByOrderId.get(orderId)))));
|
||||
new UpdateAction().execute(updateInput);
|
||||
}
|
||||
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -46,6 +46,7 @@ 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.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
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.processes.QProcessMetaData;
|
||||
@ -56,14 +57,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAut
|
||||
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.backend.implementations.memory.MemoryRecordStore;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcessTest;
|
||||
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.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@ -77,18 +75,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
class PollingAutomationPerTableRunnerTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void beforeAndAfterEach()
|
||||
{
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test a cycle that does an insert, some automations, then and an update, and more automations.
|
||||
*******************************************************************************/
|
||||
@ -200,7 +186,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void runAllTableActions(QInstance qInstance) throws QException
|
||||
static void runAllTableActions(QInstance qInstance) throws QException
|
||||
{
|
||||
List<PollingAutomationPerTableRunner.TableActionsInterface> tableActions = PollingAutomationPerTableRunner.getTableActions(qInstance, TestUtils.POLLING_AUTOMATION);
|
||||
for(PollingAutomationPerTableRunner.TableActionsInterface tableAction : tableActions)
|
||||
@ -210,7 +196,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), QContext.getQSession(), tableAction.status());
|
||||
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
|
||||
}
|
||||
}
|
||||
|
||||
@ -512,7 +498,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), QContext.getQSession(), tableAction.status());
|
||||
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
|
||||
}
|
||||
}).hasMessage(PollingAutomationPerTableRunnerThatShouldSimulateServerShutdownMidRun.EXCEPTION_MESSAGE);
|
||||
|
||||
@ -593,4 +579,70 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
|
||||
new PollingAutomationPerTableRunner.ShardedTableActions(null, null, null, null, null).noopToFakeTestCoverage();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAddOrderByToQueryFilter()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// make a table we'll test with. just put a primary-key id on it first //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = new QTableMetaData()
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter);
|
||||
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
|
||||
}
|
||||
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter);
|
||||
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// add createDate & modifyDate fields, but not with dynamic-default-behaviors //
|
||||
// so should still sort by id //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
QFieldMetaData createDate = new QFieldMetaData("createDate", QFieldType.DATE_TIME);
|
||||
QFieldMetaData modifyDate = new QFieldMetaData("modifyDate", QFieldType.DATE_TIME);
|
||||
table.addField(createDate);
|
||||
table.addField(modifyDate);
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter);
|
||||
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
|
||||
}
|
||||
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter);
|
||||
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// add dynamic default value behaviors, confirm create/modify date fields are used //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
createDate.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE);
|
||||
modifyDate.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
|
||||
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter);
|
||||
assertEquals("createDate", filter.getOrderBys().get(0).getFieldName());
|
||||
}
|
||||
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter);
|
||||
assertEquals("modifyDate", filter.getOrderBys().get(0).getFieldName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.dashboard;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
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.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for AbstractHTMLWidgetRenderer
|
||||
*******************************************************************************/
|
||||
class AbstractHTMLWidgetRendererTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
String link = AbstractHTMLWidgetRenderer.getCountLink(null, TestUtils.TABLE_NAME_PERSON, new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)), 2
|
||||
);
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// assert that filter de-duplication is occurring //
|
||||
////////////////////////////////////////////////////
|
||||
assertThat(link).doesNotMatch(".*EQUALS.*EQUALS.*");
|
||||
}
|
||||
|
||||
}
|
@ -26,6 +26,7 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
@ -175,7 +176,7 @@ class QRecordTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testListAsValue()
|
||||
void testArrayListAsValue()
|
||||
{
|
||||
ArrayList<Integer> originalArrayList = new ArrayList<>(List.of(1, 2, 3));
|
||||
QRecord recordWithArrayListValue = new QRecord().withValue("myList", originalArrayList);
|
||||
@ -196,6 +197,31 @@ class QRecordTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testLinkedListAsValue()
|
||||
{
|
||||
LinkedList<Integer> originalLinkedList = new LinkedList<>(List.of(1, 2, 3));
|
||||
QRecord recordWithLinkedListValue = new QRecord().withValue("myList", originalLinkedList);
|
||||
QRecord cloneWithLinkedListValue = new QRecord(recordWithLinkedListValue);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the clone list and original list should be equals (have contents that are equals), but not be the same (reference) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of(1, 2, 3), cloneWithLinkedListValue.getValue("myList"));
|
||||
assertNotSame(originalLinkedList, cloneWithLinkedListValue.getValue("myList"));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure a change to the original list doesn't change the cloned list (as it was cloned deeply) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
originalLinkedList.add(4);
|
||||
assertNotEquals(originalLinkedList, cloneWithLinkedListValue.getValue("myList"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
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 HealBadRecordAutomationStatusesProcessStep
|
||||
*******************************************************************************/
|
||||
class HealBadRecordAutomationStatusesProcessStepTest extends BaseTest
|
||||
{
|
||||
private static String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoFailedUpdates() throws QException
|
||||
{
|
||||
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(new QRecord(), new QRecord())));
|
||||
List<QRecord> records = queryAllRecords();
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.FAILED_UPDATE_AUTOMATIONS, null);
|
||||
|
||||
assertThat(queryAllRecords()).allMatch(r -> AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
|
||||
RunBackendStepOutput output = runProcessStep();
|
||||
|
||||
assertEquals(2, output.getValueInteger("totalRecordsUpdated"));
|
||||
assertThat(queryAllRecords()).allMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOneFailedUpdateOneFailedInsert() throws QException
|
||||
{
|
||||
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(new QRecord(), new QRecord())));
|
||||
List<QRecord> records = queryAllRecords();
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records.subList(0, 1), AutomationStatus.FAILED_UPDATE_AUTOMATIONS, null);
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records.subList(1, 2), AutomationStatus.FAILED_INSERT_AUTOMATIONS, null);
|
||||
|
||||
assertThat(queryAllRecords())
|
||||
.anyMatch(r -> AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
|
||||
.anyMatch(r -> AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
|
||||
RunBackendStepOutput output = runProcessStep();
|
||||
|
||||
assertEquals(2, output.getValueInteger("totalRecordsUpdated"));
|
||||
assertThat(queryAllRecords())
|
||||
.anyMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
|
||||
.anyMatch(r -> AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOldRunning() throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////
|
||||
// temporarily remove the modify-date behavior //
|
||||
/////////////////////////////////////////////////
|
||||
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.NONE);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// insert 2 records, one with an old modifyDate, one with 6 minutes ago //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
|
||||
new QRecord().withValue("firstName", "Darin").withValue("modifyDate", Instant.parse("2023-01-01T12:00:00Z")),
|
||||
new QRecord().withValue("firstName", "Tim").withValue("modifyDate", Instant.now().minus(6, ChronoUnit.MINUTES))
|
||||
)));
|
||||
List<QRecord> records = queryAllRecords();
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// put those records both in status: running-updates //
|
||||
///////////////////////////////////////////////////////
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS, null);
|
||||
|
||||
assertThat(queryAllRecords())
|
||||
.allMatch(r -> AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
|
||||
/////////////////////////////////////
|
||||
// restore the modifyDate behavior //
|
||||
/////////////////////////////////////
|
||||
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
|
||||
|
||||
/////////////////////////
|
||||
// run code under test //
|
||||
/////////////////////////
|
||||
RunBackendStepOutput output = runProcessStep();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// assert we updated 1 (the old one) to pending-updates, the other left as running-updates //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
|
||||
assertThat(queryAllRecords())
|
||||
.anyMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
|
||||
.anyMatch(r -> AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
|
||||
/////////////////////////////////
|
||||
// re-run, with 3-minute limit //
|
||||
/////////////////////////////////
|
||||
output = runProcessStep(new RunBackendStepInput().withValues(Map.of("minutesOldLimit", 3)));
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// assert that one updated too, and all are now pending-update //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
|
||||
assertThat(queryAllRecords())
|
||||
.allMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer getAutomationStatus(QRecord r)
|
||||
{
|
||||
return r.getValueInteger(TestUtils.standardQqqAutomationStatusField().getName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> queryAllRecords() throws QException
|
||||
{
|
||||
return new QueryAction().execute(new QueryInput(tableName)).getRecords();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static RunBackendStepOutput runProcessStep() throws QException
|
||||
{
|
||||
RunBackendStepInput input = new RunBackendStepInput();
|
||||
return runProcessStep(input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static RunBackendStepOutput runProcessStep(RunBackendStepInput input) throws QException
|
||||
{
|
||||
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||
new HealBadRecordAutomationStatusesProcessStep().run(input, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for RunTableAutomationsProcessStep
|
||||
*******************************************************************************/
|
||||
class RunTableAutomationsProcessStepTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws Exception
|
||||
{
|
||||
UnsafeSupplier<Integer, ?> getAutomationStatus = () -> new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withPrimaryKey(1)).getValueInteger("qqqAutomationStatus");
|
||||
|
||||
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
|
||||
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), getAutomationStatus.get());
|
||||
|
||||
RunBackendStepInput input = new RunBackendStepInput();
|
||||
input.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||
new RunTableAutomationsProcessStep().run(input, output);
|
||||
assertEquals("true", output.getValue("ok"));
|
||||
|
||||
assertEquals(AutomationStatus.OK.getId(), getAutomationStatus.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testThrowsWithoutTableName() throws QException
|
||||
{
|
||||
RunBackendStepInput input = new RunBackendStepInput();
|
||||
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||
assertThatThrownBy(() -> new RunTableAutomationsProcessStep().run(input, output))
|
||||
.hasMessageContaining("Missing required input value: tableName");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testThrowsWithInvalidTableName() throws QException
|
||||
{
|
||||
RunBackendStepInput input = new RunBackendStepInput();
|
||||
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||
input.addValue("tableName", "asdf");
|
||||
assertThatThrownBy(() -> new RunTableAutomationsProcessStep().run(input, output))
|
||||
.hasMessageContaining("Unrecognized table name: asdf");
|
||||
}
|
||||
|
||||
}
|
@ -116,6 +116,19 @@ class ExceptionUtilsTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testGetTopAndBottomMessages()
|
||||
{
|
||||
assertEquals("foo", ExceptionUtils.getTopAndBottomMessages(new Exception("foo")));
|
||||
assertEquals("foo: bar", ExceptionUtils.getTopAndBottomMessages(new Exception("foo", new Exception("bar"))));
|
||||
assertEquals("foo: baz", ExceptionUtils.getTopAndBottomMessages(new Exception("foo", new Exception("bar", new Exception("baz")))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test exception class - lets you set the cause, easier to create a loop.
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,355 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.utils;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.GREATER_THAN;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter.BooleanOperator.OR;
|
||||
import static com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper.dedupeFilter;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for QQueryFilterDeduper
|
||||
*******************************************************************************/
|
||||
class QQueryFilterDeduperTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDegenerateCases()
|
||||
{
|
||||
assertNull(dedupeFilter(null));
|
||||
|
||||
QQueryFilter empty = new QQueryFilter();
|
||||
assertEquals(empty, dedupeFilter(empty));
|
||||
assertNotSame(empty, dedupeFilter(empty)); // method always clones, so, just assert that.
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSimpleFiltersWithNoChanges()
|
||||
{
|
||||
QQueryFilter oneCriteria = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1));
|
||||
assertEquals(oneCriteria, dedupeFilter(oneCriteria));
|
||||
assertNotSame(oneCriteria, dedupeFilter(oneCriteria));
|
||||
|
||||
QQueryFilter twoCriteriaDifferentFields = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("b", GREATER_THAN, 2));
|
||||
assertEquals(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields));
|
||||
assertNotSame(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOrs()
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// we've only written the simplest cases with ORs... //
|
||||
///////////////////////////////////////////////////////
|
||||
assertEquals(new QQueryFilter().withBooleanOperator(OR).withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withBooleanOperator(OR)
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// just not built at this time - obviously, could become an IN list //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter notSupportedOrTwoEquals = new QQueryFilter()
|
||||
.withBooleanOperator(OR)
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 2));
|
||||
assertEquals(notSupportedOrTwoEquals, dedupeFilter(notSupportedOrTwoEquals));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// I think the logic would be, that the EQUALS 1 would be removed (is redundant) //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter notSupportedOrEqualsNotEquals = new QQueryFilter()
|
||||
.withBooleanOperator(OR)
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2));
|
||||
assertEquals(notSupportedOrEqualsNotEquals, dedupeFilter(notSupportedOrEqualsNotEquals));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testMoreOperators()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// only simplest case (of criteria being .equals()) is supported... //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", GREATER_THAN, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", GREATER_THAN, 1))
|
||||
.withCriteria(new QFilterCriteria("a", GREATER_THAN, 1))
|
||||
));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// in theory, we could do more, but we just haven't yet (e.g, this could be > 5) //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter tooComplex = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", GREATER_THAN, 1))
|
||||
.withCriteria(new QFilterCriteria("f", GREATER_THAN, 5));
|
||||
assertEquals(tooComplex, dedupeFilter(tooComplex));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAllEquals()
|
||||
{
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("c", EQUALS, 3)),
|
||||
dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("c", EQUALS, 3))
|
||||
.withCriteria(new QFilterCriteria("c", EQUALS, 3))
|
||||
.withCriteria(new QFilterCriteria("c", EQUALS, 3))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEqualsAndNotEqualsAndNotIn()
|
||||
{
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
|
||||
));
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// this is a contradiction, so we choose not to dedupe it //
|
||||
////////////////////////////////////////////////////////////
|
||||
QQueryFilter contradiction1 = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1));
|
||||
assertEquals(contradiction1, dedupeFilter(contradiction1));
|
||||
|
||||
QQueryFilter contradiction2 = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 0, 1));
|
||||
assertEquals(contradiction2, dedupeFilter(contradiction2));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this case can collapse the two not-equals, but then fails to merge the equals with them, because they are a contradiction! //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 2)),
|
||||
dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
|
||||
.withCriteria(new QFilterCriteria("f", EQUALS, 2))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNotEqualsAndNotIn()
|
||||
{
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
|
||||
));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ideally, maybe, this would have the values ordered 1,2,3, but, is equivalent enough //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInAndNotEquals()
|
||||
{
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 2, 3))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", IN, 2, 3))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
|
||||
));
|
||||
|
||||
QQueryFilter contradiction1 = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1));
|
||||
assertEquals(contradiction1, dedupeFilter(contradiction1));
|
||||
|
||||
QQueryFilter contradiction2 = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1))
|
||||
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1));
|
||||
assertEquals(contradiction2, dedupeFilter(contradiction2));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testMultipleInLists()
|
||||
{
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1, 2))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 2, 3))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3, 4)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6))
|
||||
));
|
||||
|
||||
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3)), dedupeFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1, 3, 5, 7))
|
||||
));
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// contradicting in-lists - we give up and refuse to simplify it //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QQueryFilter contradiction = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("f", IN, 1, 2))
|
||||
.withCriteria(new QFilterCriteria("f", IN, 3, 4));
|
||||
assertEquals(contradiction, dedupeFilter(contradiction));
|
||||
}
|
||||
|
||||
}
|
@ -720,7 +720,7 @@ public class TestUtils
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QTableAutomationDetails defineStandardAutomationDetails()
|
||||
public static QTableAutomationDetails defineStandardAutomationDetails()
|
||||
{
|
||||
return (new QTableAutomationDetails()
|
||||
.withProviderName(POLLING_AUTOMATION)
|
||||
|
@ -32,11 +32,14 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@ -122,6 +125,58 @@ class MemoizationTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testLookupFunction()
|
||||
{
|
||||
AtomicInteger lookupFunctionCallCounter = new AtomicInteger(0);
|
||||
|
||||
Memoization<String, Integer> memoization = new Memoization<>();
|
||||
|
||||
UnsafeFunction<String, Integer, Exception> lookupFunction = numberString ->
|
||||
{
|
||||
lookupFunctionCallCounter.getAndIncrement();
|
||||
|
||||
if(numberString.equals("null"))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return Integer.parseInt(numberString);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get "1" twice - should return 1 each time, and call the lookup function exactly once //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertThat(memoization.getResult("1", lookupFunction)).isPresent().contains(1);
|
||||
assertEquals(1, lookupFunctionCallCounter.get());
|
||||
|
||||
assertThat(memoization.getResult("1", lookupFunction)).isPresent().contains(1);
|
||||
assertEquals(1, lookupFunctionCallCounter.get());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now get "null" twice - should return null each time, and call the lookup function exactly once more //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertThat(memoization.getResult("null", lookupFunction)).isEmpty();
|
||||
assertEquals(2, lookupFunctionCallCounter.get());
|
||||
|
||||
assertThat(memoization.getResult("null", lookupFunction)).isEmpty();
|
||||
assertEquals(2, lookupFunctionCallCounter.get());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now make a call that throws twice - again, should return null each time, and only do one more loookup call //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertThat(memoization.getResult(null, lookupFunction)).isEmpty();
|
||||
assertEquals(3, lookupFunctionCallCounter.get());
|
||||
|
||||
assertThat(memoization.getResult(null, lookupFunction)).isEmpty();
|
||||
assertEquals(3, lookupFunctionCallCounter.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -992,6 +992,8 @@ public class ApiImplementation
|
||||
runProcessInput.setProcessUUID(processUUID);
|
||||
// todo i don't think runProcessInput.setAsyncJobCallback();
|
||||
|
||||
PermissionsHelper.checkProcessPermissionThrowing(runProcessInput, processName);
|
||||
|
||||
//////////////////////
|
||||
// map input values //
|
||||
//////////////////////
|
||||
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.api.javalin;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.api.BaseTest;
|
||||
import com.kingsrook.qqq.api.TestUtils;
|
||||
import com.kingsrook.qqq.api.actions.ApiImplementation;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
|
||||
import io.javalin.apibuilder.EndpointGroup;
|
||||
import kong.unirest.Unirest;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.api.javalin.QJavalinApiHandlerTest.assertErrorResponse;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test permissions within QJavalinApiHandler
|
||||
*******************************************************************************/
|
||||
class QJavalinApiHandlerPermissionsTest extends BaseTest
|
||||
{
|
||||
private static final int PORT = 6263;
|
||||
protected static final String BASE_URL = "http://localhost:" + PORT;
|
||||
|
||||
private static final String VERSION = "2023.Q1";
|
||||
|
||||
protected static QJavalinImplementation qJavalinImplementation;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeAll
|
||||
static void beforeAll() throws QInstanceValidationException
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// turn on permissions on all tables & processes //
|
||||
///////////////////////////////////////////////////
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
table.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS));
|
||||
}
|
||||
|
||||
for(QProcessMetaData process : qInstance.getProcesses().values())
|
||||
{
|
||||
process.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION));
|
||||
}
|
||||
|
||||
qJavalinImplementation = new QJavalinImplementation(qInstance);
|
||||
qJavalinImplementation.startJavalinServer(PORT);
|
||||
EndpointGroup routes = new QJavalinApiHandler(qInstance).getRoutes();
|
||||
qJavalinImplementation.getJavalinService().routes(routes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void beforeAndAfterEach()
|
||||
{
|
||||
ApiImplementation.clearCaches();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Before the class (all) runs, start a javalin server.
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterAll
|
||||
static void afterAll()
|
||||
{
|
||||
qJavalinImplementation.stopJavalinServer();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test403s()
|
||||
{
|
||||
////////////////////////////
|
||||
// tables - single & bulk //
|
||||
////////////////////////////
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.get(BASE_URL + "/api/" + VERSION + "/order/query").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.get(BASE_URL + "/api/" + VERSION + "/order/1").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.post(BASE_URL + "/api/" + VERSION + "/person").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/1").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/1").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk").asString());
|
||||
|
||||
///////////////
|
||||
// processes //
|
||||
///////////////
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString());
|
||||
assertErrorResponse(HttpStatus.FORBIDDEN_403, "You do not have permission", Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString());
|
||||
}
|
||||
|
||||
}
|
@ -1548,7 +1548,7 @@ class QJavalinApiHandlerTest extends BaseTest
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void assertErrorResponse(Integer expectedStatusCode, String expectedErrorMessage, HttpResponse<String> response)
|
||||
static void assertErrorResponse(Integer expectedStatusCode, String expectedErrorMessage, HttpResponse<String> response)
|
||||
{
|
||||
if(expectedStatusCode != null)
|
||||
{
|
||||
|
Reference in New Issue
Block a user