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
new file mode 100644
index 00000000..32abde3f
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerChildPostInsertCustomizerTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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 .
+ */
+
+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
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @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 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 apply(List records) throws QException
+ {
+ //////////////////////////////////
+ // count line items by order id //
+ //////////////////////////////////
+ Set 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 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);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java
index 6ce3868c..f6b83e22 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java
@@ -56,14 +56,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 +74,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 +185,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
- private void runAllTableActions(QInstance qInstance) throws QException
+ static void runAllTableActions(QInstance qInstance) throws QException
{
List tableActions = PollingAutomationPerTableRunner.getTableActions(qInstance, TestUtils.POLLING_AUTOMATION);
for(PollingAutomationPerTableRunner.TableActionsInterface tableAction : tableActions)
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 a34e5a4f..9656672e 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
@@ -720,7 +720,7 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
- private static QTableAutomationDetails defineStandardAutomationDetails()
+ public static QTableAutomationDetails defineStandardAutomationDetails()
{
return (new QTableAutomationDetails()
.withProviderName(POLLING_AUTOMATION)