From b84406d8efe215fe52d025d244a4928c9311560e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 23 May 2025 15:27:13 -0500 Subject: [PATCH] Add support for executing table triggers beyond what the core table provides (scripts) via custom plugins (adding for workflows qbit) also move RecordAutomationHandler to an interface (RecordAutomationHandlerInterface) --- ...omTableTriggerRecordAutomationHandler.java | 38 ++++++++ .../automation/RecordAutomationHandler.java | 12 +-- .../RecordAutomationHandlerInterface.java | 40 +++++++++ ...omTableTriggerRecordAutomationHandler.java | 89 +++++++++++++++++++ .../RunRecordScriptAutomationHandler.java | 2 +- .../PollingAutomationPerTableRunner.java | 37 ++++++-- .../core/instances/QInstanceValidator.java | 4 +- ...tionUpdatingSelfAvoidInfiniteLoopTest.java | 4 +- ...leRunnerChildPostInsertCustomizerTest.java | 4 +- .../StandardScheduledExecutorTest.java | 4 +- .../qqq/backend/core/utils/TestUtils.java | 8 +- 11 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/CustomTableTriggerRecordAutomationHandler.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandlerInterface.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunCustomTableTriggerRecordAutomationHandler.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/CustomTableTriggerRecordAutomationHandler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/CustomTableTriggerRecordAutomationHandler.java new file mode 100644 index 00000000..661ee3b8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/CustomTableTriggerRecordAutomationHandler.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.automation; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; + + +/******************************************************************************* + ** interface to be implemented by one that wishes to execute custom table triggers + *******************************************************************************/ +public interface CustomTableTriggerRecordAutomationHandler extends RecordAutomationHandlerInterface +{ + /*************************************************************************** + ** + ***************************************************************************/ + boolean handlesThisInput(RecordAutomationInput recordAutomationInput) throws QException; +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java index 9358b58e..781161d9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandler.java @@ -22,19 +22,11 @@ package com.kingsrook.qqq.backend.core.actions.automation; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; - - /******************************************************************************* ** Base class for custom-codes to run as an automation action *******************************************************************************/ -public abstract class RecordAutomationHandler +@Deprecated(since = "0.26.0 - when RecordAutomationHandlerInterface was introduced") +public abstract class RecordAutomationHandler implements RecordAutomationHandlerInterface { - /******************************************************************************* - ** - *******************************************************************************/ - public abstract void execute(RecordAutomationInput recordAutomationInput) throws QException; - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandlerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandlerInterface.java new file mode 100644 index 00000000..9713934c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationHandlerInterface.java @@ -0,0 +1,40 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.automation; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; + + +/******************************************************************************* + ** Interface for custom-codes to run as an automation action + *******************************************************************************/ +public interface RecordAutomationHandlerInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void execute(RecordAutomationInput recordAutomationInput) throws QException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunCustomTableTriggerRecordAutomationHandler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunCustomTableTriggerRecordAutomationHandler.java new file mode 100644 index 00000000..94ebe620 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunCustomTableTriggerRecordAutomationHandler.java @@ -0,0 +1,89 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.automation; + + +import java.util.LinkedHashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** RecordAutomationHandler implementation that is called by automation runner + ** that doesn't know to deal with a TableTrigger record that it received. + ** + ** e.g., if an app has altered that table (e.g., workflows-qbit). + *******************************************************************************/ +public class RunCustomTableTriggerRecordAutomationHandler implements RecordAutomationHandlerInterface +{ + private static final QLogger LOG = QLogger.getLogger(RunCustomTableTriggerRecordAutomationHandler.class); + + private static Map handlers = new LinkedHashMap<>(); + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static void registerHandler(String name, QCodeReference codeReference) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if there's already a value mapped for this name, warn about it (unless it's for the same code reference) // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(handlers.containsKey(name)) + { + if(handlers.get(name).getName().equals(codeReference.getName())) + { + LOG.warn("Registering a CustomTableTriggerRecordAutomationHandler for a name that is already registered", logPair("name", name)); + } + } + + handlers.put(name, codeReference); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void execute(RecordAutomationInput recordAutomationInput) throws QException + { + for(QCodeReference codeReference : handlers.values()) + { + CustomTableTriggerRecordAutomationHandler customHandler = QCodeLoader.getAdHoc(CustomTableTriggerRecordAutomationHandler.class, codeReference); + if(customHandler.handlesThisInput(recordAutomationInput)) + { + customHandler.execute(recordAutomationInput); + return; + } + } + + throw (new QException("No custom record automation handler was found for " + recordAutomationInput)); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java index 2f77c623..c9576433 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java @@ -51,7 +51,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* ** *******************************************************************************/ -public class RunRecordScriptAutomationHandler extends RecordAutomationHandler +public class RunRecordScriptAutomationHandler implements RecordAutomationHandlerInterface { private static final QLogger LOG = QLogger.getLogger(RunRecordScriptAutomationHandler.class); 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 5351b53a..64bc8211 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 @@ -33,8 +33,9 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; -import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; +import com.kingsrook.qqq.backend.core.actions.automation.RunCustomTableTriggerRecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.automation.RunRecordScriptAutomationHandler; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; @@ -442,15 +443,33 @@ public class PollingAutomationPerTableRunner implements Runnable } } - rs.add(new TableAutomationAction() - .withName("Script:" + tableTrigger.getScriptId()) + TableAutomationAction tableAutomationAction = new TableAutomationAction() .withFilter(filter) .withTriggerEvent(triggerEvent) .withPriority(tableTrigger.getPriority()) - .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) - .withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId())) - .withIncludeRecordAssociations(true) - ); + .withIncludeRecordAssociations(true); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the table trigger has a script id on it, then we know how to run that here in qqq-backend-core // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + if(tableTrigger.getScriptId() != null) + { + rs.add(tableAutomationAction + .withName("Script:" + tableTrigger.getScriptId()) + .withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId())) + .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))); + } + else + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // but - the app may have added an extension to the TableTrigger table (e.g., workflows qbit) // + // so, defer to RunCustomRecordAutomationHandler for unrecognized triggers // + //////////////////////////////////////////////////////////////////////////////////////////////// + rs.add(tableAutomationAction + .withName("Custom Trigger:" + tableTrigger.getScriptId()) + .withValues(MapBuilder.of("tableTriggerId", tableTrigger.getId())) + .withCodeReference(new QCodeReference(RunCustomTableTriggerRecordAutomationHandler.class))); + } } catch(Exception e) { @@ -526,7 +545,7 @@ public class PollingAutomationPerTableRunner implements Runnable // note - this method - will re-query the objects, so we should have confidence that their data is fresh... // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// List matchingQRecords = getRecordsMatchingActionFilter(table, records, action); - LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action); + LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action); if(CollectionUtils.nullSafeHasContents(matchingQRecords)) { LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action); @@ -649,7 +668,7 @@ public class PollingAutomationPerTableRunner implements Runnable input.setRecordList(records); input.setAction(action); - RecordAutomationHandler recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandler.class, action.getCodeReference()); + RecordAutomationHandlerInterface recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandlerInterface.class, action.getCodeReference()); recordAutomationHandler.execute(input); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index e578d570..b03d1526 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -41,7 +41,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph; @@ -1356,7 +1356,7 @@ public class QInstanceValidator numberSet++; if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix)) { - validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandler.class); + validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandlerInterface.class); } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest.java index f8c4df67..358482c2 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest.java @@ -30,7 +30,7 @@ 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.automation.RecordAutomationHandlerInterface; 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; @@ -163,7 +163,7 @@ public class PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfinite /******************************************************************************* ** *******************************************************************************/ - public static class OrderPostInsertAndUpdateAction extends RecordAutomationHandler + public static class OrderPostInsertAndUpdateAction implements RecordAutomationHandlerInterface { /******************************************************************************* diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerChildPostInsertCustomizerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerChildPostInsertCustomizerTest.java index 1e22966c..35915254 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerChildPostInsertCustomizerTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerChildPostInsertCustomizerTest.java @@ -33,7 +33,7 @@ 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.automation.RecordAutomationHandlerInterface; 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; @@ -147,7 +147,7 @@ public class PollingAutomationPerTableRunnerChildPostInsertCustomizerTest extend /******************************************************************************* ** *******************************************************************************/ - public static class OrderPostInsertAction extends RecordAutomationHandler + public static class OrderPostInsertAction implements RecordAutomationHandlerInterface { /******************************************************************************* diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/StandardScheduledExecutorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/StandardScheduledExecutorTest.java index 84ed8ae9..dd26d531 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/StandardScheduledExecutorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/StandardScheduledExecutorTest.java @@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; 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.automation.RecordAutomationHandlerInterface; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -162,7 +162,7 @@ class StandardScheduledExecutorTest extends BaseTest /******************************************************************************* ** *******************************************************************************/ - public static class CaptureSessionIdAutomationHandler extends RecordAutomationHandler + public static class CaptureSessionIdAutomationHandler implements RecordAutomationHandlerInterface { static String sessionId; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 7b81bc23..2add4346 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -29,7 +29,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; -import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; +import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface; import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarChart; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; @@ -1070,7 +1070,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - public static class CheckAge extends RecordAutomationHandler + public static class CheckAge implements RecordAutomationHandlerInterface { public static String SUFFIX_FOR_MINORS = " (a minor)"; @@ -1119,7 +1119,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - public static class FailAutomationForSith extends RecordAutomationHandler + public static class FailAutomationForSith implements RecordAutomationHandlerInterface { /******************************************************************************* @@ -1142,7 +1142,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - public static class LogPersonUpdate extends RecordAutomationHandler + public static class LogPersonUpdate implements RecordAutomationHandlerInterface { public static List updatedIds = new ArrayList<>();