mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
CE-535 Cleanup in DeleteAction - add omitDmlAudit & auditContext; be more sure not to delete associations if errors
This commit is contained in:
@ -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.actions.interfaces.DeleteInterface;
|
||||||
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;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
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.audits.DMLAuditInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
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 //
|
// 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);
|
List<Serializable> primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
|
||||||
deleteInput.setPrimaryKeys(primaryKeyList);
|
deleteInput.setPrimaryKeys(primaryKeyList);
|
||||||
|
primaryKeys = primaryKeyList;
|
||||||
|
|
||||||
if(primaryKeyList.isEmpty())
|
if(primaryKeyList.isEmpty())
|
||||||
{
|
{
|
||||||
@ -165,10 +168,22 @@ public class DeleteAction
|
|||||||
|
|
||||||
if(!primaryKeysToRemoveFromInput.isEmpty())
|
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 //
|
// 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. //
|
// 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)
|
for(QRecord outputRecordWithError : outputRecordsWithErrors)
|
||||||
{
|
{
|
||||||
Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName);
|
Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName);
|
||||||
recordsWithValidationWarnings.remove(pkey);
|
recordsWithValidationWarnings.remove(pkey);
|
||||||
|
primaryKeysWithoutErrors.remove(pkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -211,15 +228,23 @@ public class DeleteAction
|
|||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
// delete associations, if applicable //
|
// delete associations, if applicable //
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
manageAssociations(deleteInput);
|
manageAssociations(primaryKeysWithoutErrors, deleteInput);
|
||||||
|
|
||||||
///////////////////////////////////
|
//////////////////
|
||||||
// do the audit //
|
// do the audit //
|
||||||
// todo - add input.omitDmlAudit //
|
//////////////////
|
||||||
///////////////////////////////////
|
if(deleteInput.getOmitDmlAudit())
|
||||||
DMLAuditInput dmlAuditInput = new DMLAuditInput().withTableActionInput(deleteInput);
|
{
|
||||||
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
|
LOG.debug("Requested to omit DML audit");
|
||||||
new DMLAuditAction().execute(dmlAuditInput);
|
}
|
||||||
|
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 //
|
// 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();
|
QTableMetaData table = deleteInput.getTable();
|
||||||
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
|
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()))
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -51,6 +51,17 @@ public class CountInput extends AbstractTableActionInput
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CountInput(String tableName)
|
||||||
|
{
|
||||||
|
setTableName(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for filter
|
** Getter for filter
|
||||||
**
|
**
|
||||||
@ -152,4 +163,15 @@ public class CountInput extends AbstractTableActionInput
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for filter
|
||||||
|
*******************************************************************************/
|
||||||
|
public CountInput withFilter(QQueryFilter filter)
|
||||||
|
{
|
||||||
|
this.filter = filter;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,9 @@ public class DeleteInput extends AbstractTableActionInput
|
|||||||
private QQueryFilter queryFilter;
|
private QQueryFilter queryFilter;
|
||||||
private InputSource inputSource = QInputSource.SYSTEM;
|
private InputSource inputSource = QInputSource.SYSTEM;
|
||||||
|
|
||||||
|
private boolean omitDmlAudit = false;
|
||||||
|
private String auditContext = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -211,4 +214,66 @@ public class DeleteInput extends AbstractTableActionInput
|
|||||||
return (this);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
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.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
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.DeleteInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
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.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.QCriteriaOperator;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
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.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
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.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.TestUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
@ -399,4 +407,87 @@ class DeleteActionTest extends BaseTest
|
|||||||
new InsertAction().execute(insertInput);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user