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)