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)
This commit is contained in:
2025-05-23 15:27:13 -05:00
parent ca33b28f7a
commit b84406d8ef
11 changed files with 210 additions and 32 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -22,19 +22,11 @@
package com.kingsrook.qqq.backend.core.actions.automation; 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 ** 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;
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, QCodeReference> 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));
}
}

View File

@ -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); private static final QLogger LOG = QLogger.getLogger(RunRecordScriptAutomationHandler.class);

View File

@ -33,8 +33,9 @@ import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; 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.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.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.automation.RunRecordScriptAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
@ -442,15 +443,33 @@ public class PollingAutomationPerTableRunner implements Runnable
} }
} }
rs.add(new TableAutomationAction() TableAutomationAction tableAutomationAction = new TableAutomationAction()
.withName("Script:" + tableTrigger.getScriptId())
.withFilter(filter) .withFilter(filter)
.withTriggerEvent(triggerEvent) .withTriggerEvent(triggerEvent)
.withPriority(tableTrigger.getPriority()) .withPriority(tableTrigger.getPriority())
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) .withIncludeRecordAssociations(true);
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
.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) 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... // // note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action); List<QRecord> 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)) if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{ {
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action); LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
@ -649,7 +668,7 @@ public class PollingAutomationPerTableRunner implements Runnable
input.setRecordList(records); input.setRecordList(records);
input.setAction(action); input.setAction(action);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandler.class, action.getCodeReference()); RecordAutomationHandlerInterface recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandlerInterface.class, action.getCodeReference());
recordAutomationHandler.execute(input); recordAutomationHandler.execute(input);
} }
} }

View File

@ -41,7 +41,7 @@ import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph; import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
@ -1356,7 +1356,7 @@ public class QInstanceValidator
numberSet++; numberSet++;
if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix)) if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix))
{ {
validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandler.class); validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandlerInterface.class);
} }
} }

View File

@ -30,7 +30,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; 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.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; 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
{ {
/******************************************************************************* /*******************************************************************************

View File

@ -33,7 +33,7 @@ import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; 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.AbstractPostInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction; 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
{ {
/******************************************************************************* /*******************************************************************************

View File

@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Supplier; import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; 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.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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; static String sessionId;

View File

@ -29,7 +29,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; 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.dashboard.PersonsByCreateDateBarChart;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; 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)"; 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<Integer> updatedIds = new ArrayList<>(); public static List<Integer> updatedIds = new ArrayList<>();