diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java index d5deff86..28ba150d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java @@ -78,6 +78,7 @@ public class DMLAuditAction extends AbstractQActionFunction loggedUnauditableTableNames = new HashSet<>(); + /******************************************************************************* ** *******************************************************************************/ @@ -210,6 +211,19 @@ public class DMLAuditAction extends AbstractQActionFunction + /******************************************************************************* + ** 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 ** @@ -106,10 +131,10 @@ public enum AutomationStatus implements PossibleValueEnum public String getInsertOrUpdate() { return switch(this) - { - case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert"; - case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update"; - case OK -> ""; - }; + { + case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert"; + case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update"; + case OK -> ""; + }; } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java index ce942c25..beef50e5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java @@ -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 areThereTableTriggersForTableMemoization = new Memoization().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 records, AutomationStatus automationStatus) + public static boolean setAutomationStatusInRecords(QTableMetaData table, List records, AutomationStatus automationStatus, QBackendTransaction transaction, List oldRecordList) { if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records)) { return (false); } + QTableAutomationDetails automationDetails = table.getAutomationDetails(); + Set 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 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,19 +174,15 @@ 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... // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId()); - // todo - another field - for the automation timestamp?? + if(!pkeysWeMayNotUpdate.contains(record.getValue(table.getPrimaryKeyField()))) + { + record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId()); + // todo - another field - for the automation timestamp?? + } } } @@ -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 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 records, AutomationStatus automationStatus) throws QException + public static void setAutomationStatusInRecordsAndUpdate(QTableMetaData table, List 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) {} + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 88602fc4..2113facc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -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 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 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 records, List actions, AutomationStatus automationStatus) throws QException + private void applyActionsToRecords(QTableMetaData table, List records, List 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); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index 3705968c..26481e78 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -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 ("" + totalString + ""); } + /******************************************************************************* ** *******************************************************************************/ @@ -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)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java index adad50a6..fafd48e8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java @@ -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 scriptRevisionCacheByScriptRevisionId = new HashMap<>(); private Map scriptRevisionCacheByScriptId = new HashMap<>(); + private static Memoization scriptMemoizationById = new Memoization<>(); + /******************************************************************************* @@ -85,6 +90,12 @@ public class RunAdHocRecordScriptAction throw (new QException("Script revision was not found.")); } + Optional