diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java index 5e30832b..ca9badc9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; @@ -117,12 +118,14 @@ public class DeleteAction ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if there's a query filter, but the interface doesn't support using a query filter, then do a query for the filter, to get a list of primary keys instead // + // or - anytime there are associations on the table we want primary keys, as that's what the manage associations method uses // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput()) + if(deleteInput.getQueryFilter() != null && (!deleteInterface.supportsQueryFilterInput() || CollectionUtils.nullSafeHasContents(table.getAssociations()))) { - LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes"); + LOG.info("Querying for primary keys, for table " + table.getName() + " in backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes (or the table has associations)"); List primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput); deleteInput.setPrimaryKeys(primaryKeyList); + primaryKeys = primaryKeyList; if(primaryKeyList.isEmpty()) { @@ -165,10 +168,22 @@ public class DeleteAction if(!primaryKeysToRemoveFromInput.isEmpty()) { - primaryKeys.removeAll(primaryKeysToRemoveFromInput); + if(primaryKeys == null) + { + LOG.warn("There were primary keys to remove from the input, but no primary key list (filter supplied as input?)", new LogPair("primaryKeysToRemoveFromInput", primaryKeysToRemoveFromInput)); + } + else + { + primaryKeys.removeAll(primaryKeysToRemoveFromInput); + } } } + //////////////////////////////////////////////////////////////////////////////////////////////// + // stash a copy of primary keys that didn't have errors (for use in manageAssociations below) // + //////////////////////////////////////////////////////////////////////////////////////////////// + Set primaryKeysWithoutErrors = new HashSet<>(CollectionUtils.nonNullList(primaryKeys)); + //////////////////////////////////// // have the backend do the delete // //////////////////////////////////// @@ -187,11 +202,13 @@ public class DeleteAction /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if a record had a validation warning, but then an execution error, remove it from the warning list - so it's only in one of them. // + // also, always remove from /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// for(QRecord outputRecordWithError : outputRecordsWithErrors) { Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName); recordsWithValidationWarnings.remove(pkey); + primaryKeysWithoutErrors.remove(pkey); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -211,15 +228,23 @@ public class DeleteAction //////////////////////////////////////// // delete associations, if applicable // //////////////////////////////////////// - manageAssociations(deleteInput); + manageAssociations(primaryKeysWithoutErrors, deleteInput); - /////////////////////////////////// - // do the audit // - // todo - add input.omitDmlAudit // - /////////////////////////////////// - DMLAuditInput dmlAuditInput = new DMLAuditInput().withTableActionInput(deleteInput); - oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l)); - new DMLAuditAction().execute(dmlAuditInput); + ////////////////// + // do the audit // + ////////////////// + if(deleteInput.getOmitDmlAudit()) + { + LOG.debug("Requested to omit DML audit"); + } + else + { + DMLAuditInput dmlAuditInput = new DMLAuditInput() + .withTableActionInput(deleteInput) + .withAuditContext(deleteInput.getAuditContext()); + oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l)); + new DMLAuditAction().execute(dmlAuditInput); + } ////////////////////////////////////////////////////////////// // finally, run the post-delete customizer, if there is one // @@ -340,7 +365,7 @@ public class DeleteAction /******************************************************************************* ** *******************************************************************************/ - private void manageAssociations(DeleteInput deleteInput) throws QException + private void manageAssociations(Set primaryKeysWithoutErrors, DeleteInput deleteInput) throws QException { QTableMetaData table = deleteInput.getTable(); for(Association association : CollectionUtils.nonNullList(table.getAssociations())) @@ -353,7 +378,7 @@ public class DeleteAction if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField())) { - filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys())); + filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, new ArrayList<>(primaryKeysWithoutErrors))); } else { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java index 1a4863e5..b6383d99 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java @@ -51,6 +51,17 @@ public class CountInput extends AbstractTableActionInput + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public CountInput(String tableName) + { + setTableName(tableName); + } + + + /******************************************************************************* ** Getter for filter ** @@ -152,4 +163,15 @@ public class CountInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Fluent setter for filter + *******************************************************************************/ + public CountInput withFilter(QQueryFilter filter) + { + this.filter = filter; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java index c39ed3ec..3945246e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java @@ -43,6 +43,9 @@ public class DeleteInput extends AbstractTableActionInput private QQueryFilter queryFilter; private InputSource inputSource = QInputSource.SYSTEM; + private boolean omitDmlAudit = false; + private String auditContext = null; + /******************************************************************************* @@ -211,4 +214,66 @@ public class DeleteInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for omitDmlAudit + *******************************************************************************/ + public boolean getOmitDmlAudit() + { + return (this.omitDmlAudit); + } + + + + /******************************************************************************* + ** Setter for omitDmlAudit + *******************************************************************************/ + public void setOmitDmlAudit(boolean omitDmlAudit) + { + this.omitDmlAudit = omitDmlAudit; + } + + + + /******************************************************************************* + ** Fluent setter for omitDmlAudit + *******************************************************************************/ + public DeleteInput withOmitDmlAudit(boolean omitDmlAudit) + { + this.omitDmlAudit = omitDmlAudit; + return (this); + } + + + + /******************************************************************************* + ** Getter for auditContext + *******************************************************************************/ + public String getAuditContext() + { + return (this.auditContext); + } + + + + /******************************************************************************* + ** Setter for auditContext + *******************************************************************************/ + public void setAuditContext(String auditContext) + { + this.auditContext = auditContext; + } + + + + /******************************************************************************* + ** Fluent setter for auditContext + *******************************************************************************/ + public DeleteInput withAuditContext(String auditContext) + { + this.auditContext = auditContext; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java index 8492eff3..076a3f39 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java @@ -25,11 +25,15 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.util.List; import java.util.Objects; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; 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.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; 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.QFilterOrderBy; @@ -41,6 +45,10 @@ 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.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; +import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; @@ -399,4 +407,87 @@ class DeleteActionTest extends BaseTest new InsertAction().execute(insertInput); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDeleteWithErrorsDoesntDeleteAssociations() throws QException + { + QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1))); + + QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.withCustomizer(TableCustomizers.PRE_DELETE_RECORD, new QCodeReference(OrderPreDeleteCustomizer.class)); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // insert 2 orders - one that will fail to delete, and one that will warn, but should delete. // + //////////////////////////////////////////////////////////////////////////////////////////////// + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_ORDER) + .withRecords(List.of( + new QRecord().withValue("id", OrderPreDeleteCustomizer.DELETE_ERROR_ID).withValue("storeId", 1).withValue("orderNo", "ORD123") + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1"))), + + new QRecord().withValue("id", OrderPreDeleteCustomizer.DELETE_WARN_ID).withValue("storeId", 1).withValue("orderNo", "ORD124") + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC3").withValue("quantity", 3)) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD-1").withValue("value", "YOUR-VALUE-1")) + ))); + + /////////////////////////// + // confirm insert counts // + /////////////////////////// + assertEquals(2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER)).getCount()); + assertEquals(2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_LINE_ITEM)).getCount()); + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC)).getCount()); + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER_EXTRINSIC)).getCount()); + + ///////////////////////////// + // try to delete them both // + ///////////////////////////// + new DeleteAction().execute(new DeleteInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKeys(List.of(OrderPreDeleteCustomizer.DELETE_WARN_ID, OrderPreDeleteCustomizer.DELETE_WARN_ID))); + + /////////////////////// + // count what's left // + /////////////////////// + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER)).getCount()); + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_LINE_ITEM)).getCount()); + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC)).getCount()); + assertEquals(0, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER_EXTRINSIC)).getCount()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class OrderPreDeleteCustomizer extends AbstractPreDeleteCustomizer + { + public static final Integer DELETE_ERROR_ID = 9999; + public static final Integer DELETE_WARN_ID = 9998; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List apply(List records) + { + for(QRecord record : records) + { + if(DELETE_ERROR_ID.equals(record.getValue("id"))) + { + record.addError(new BadInputStatusMessage("You may not delete this order")); + } + else if(DELETE_WARN_ID.equals(record.getValue("id"))) + { + record.addWarning(new QWarningMessage("It was bad that you deleted this order")); + } + } + + return (records); + } + } + }