CE-535 Cleanup in DeleteAction - add omitDmlAudit & auditContext; be more sure not to delete associations if errors

This commit is contained in:
2023-07-11 08:31:15 -05:00
parent e6816174c3
commit 086787a5ca
4 changed files with 216 additions and 13 deletions

View File

@ -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<Serializable> 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<Serializable> 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<Serializable> 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
{

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<QRecord> apply(List<QRecord> 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);
}
}
}