mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Completed first round implementation of {pre,post}{insert,delete} actions
This commit is contained in:
@ -28,7 +28,23 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Abstract class that a table can specify an implementation of, to provide
|
||||||
|
** custom actions after a delete takes place.
|
||||||
**
|
**
|
||||||
|
** General implementation would be, to iterate over the records (ones which didn't
|
||||||
|
** have a delete error), and look at their values:
|
||||||
|
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
|
||||||
|
** - possibly throwing an exception - though doing so won't stop the delete, and instead
|
||||||
|
** will just set a warning on all of the deleted records...
|
||||||
|
** - doing "whatever else" you may want to do.
|
||||||
|
** - returning the list of records (can be the input list) that you want to go back
|
||||||
|
** to the caller - this is how errors and warnings are propagated .
|
||||||
|
**
|
||||||
|
** Note that the full deleteInput is available as a field in this class.
|
||||||
|
**
|
||||||
|
** A future enhancement here may be to take (as fields in this class) the list of
|
||||||
|
** records that the delete action marked in error - the user might want to do
|
||||||
|
** something special with them (idk, try some other way to delete them?)
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract class AbstractPostDeleteCustomizer
|
public abstract class AbstractPostDeleteCustomizer
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,18 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Abstract class that a table can specify an implementation of, to provide
|
||||||
|
** custom actions after an insert takes place.
|
||||||
**
|
**
|
||||||
|
** General implementation would be, to iterate over the records (the outputs of
|
||||||
|
** the insert action), and look at their values:
|
||||||
|
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||||
|
** - possibly throwing an exception - though doing so won't stop the update, and instead
|
||||||
|
** will just set a warning on all of the updated records...
|
||||||
|
** - doing "whatever else" you may want to do.
|
||||||
|
** - returning the list of records (can be the input list) that you want to go back to the caller.
|
||||||
|
**
|
||||||
|
** Note that the full insertInput is available as a field in this class.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract class AbstractPostInsertCustomizer
|
public abstract class AbstractPostInsertCustomizer
|
||||||
{
|
{
|
||||||
|
@ -26,12 +26,27 @@ import java.io.Serializable;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Abstract class that a table can specify an implementation of, to provide
|
||||||
|
** custom actions after an update takes place.
|
||||||
**
|
**
|
||||||
|
** General implementation would be, to iterate over the records (the outputs of
|
||||||
|
** the update action), and look at their values:
|
||||||
|
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
|
||||||
|
** - possibly throwing an exception - though doing so won't stop the update, and instead
|
||||||
|
** will just set a warning on all of the updated records...
|
||||||
|
** - doing "whatever else" you may want to do.
|
||||||
|
** - returning the list of records (can be the input list) that you want to go back to the caller.
|
||||||
|
**
|
||||||
|
** Note that the full updateInput is available as a field in this class, and the
|
||||||
|
** "old records" (e.g., with values freshly fetched from the backend) will be
|
||||||
|
** available (if the backend supports it) - both as a list (`getOldRecordList`)
|
||||||
|
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract class AbstractPostUpdateCustomizer
|
public abstract class AbstractPostUpdateCustomizer
|
||||||
{
|
{
|
||||||
@ -45,7 +60,7 @@ public abstract class AbstractPostUpdateCustomizer
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract List<QRecord> apply(List<QRecord> records);
|
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,7 +28,24 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Abstract class that a table can specify an implementation of, to provide
|
||||||
|
** custom actions before a delete takes place.
|
||||||
**
|
**
|
||||||
|
** General implementation would be, to iterate over the records (which the DeleteAction
|
||||||
|
** would look up based on the inputs to the delete action), and look at their values:
|
||||||
|
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||||
|
** - possibly throwing an exception - if you really don't want the delete operation to continue.
|
||||||
|
** - doing "whatever else" you may want to do.
|
||||||
|
** - returning the list of records (can be the input list) - this is how errors
|
||||||
|
** and warnings are propagated to the DeleteAction. Note that any records with
|
||||||
|
** an error will NOT proceed to the backend's delete interface - but those with
|
||||||
|
** warnings will.
|
||||||
|
**
|
||||||
|
** Note that the full deleteInput is available as a field in this class.
|
||||||
|
**
|
||||||
|
** A future enhancement here may be to take (as fields in this class) the list of
|
||||||
|
** records that the delete action marked in error - the user might want to do
|
||||||
|
** something special with them (idk, try some other way to delete them?)
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract class AbstractPreDeleteCustomizer
|
public abstract class AbstractPreDeleteCustomizer
|
||||||
{
|
{
|
||||||
@ -62,4 +79,5 @@ public abstract class AbstractPreDeleteCustomizer
|
|||||||
{
|
{
|
||||||
this.deleteInput = deleteInput;
|
this.deleteInput = deleteInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,18 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Abstract class that a table can specify an implementation of, to provide
|
||||||
|
** custom actions before an insert takes place.
|
||||||
**
|
**
|
||||||
|
** General implementation would be, to iterate over the records (the inputs to
|
||||||
|
** the insert action), and look at their values:
|
||||||
|
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||||
|
** - possibly manipulating values (`setValue`)
|
||||||
|
** - possibly throwing an exception - if you really don't want the insert operation to continue.
|
||||||
|
** - doing "whatever else" you may want to do.
|
||||||
|
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
|
||||||
|
**
|
||||||
|
** Note that the full insertInput is available as a field in this class.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract class AbstractPreInsertCustomizer
|
public abstract class AbstractPreInsertCustomizer
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,21 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Abstract class that a table can specify an implementation of, to provide
|
||||||
|
** custom actions before an update takes place.
|
||||||
**
|
**
|
||||||
|
** General implementation would be, to iterate over the records (the inputs to
|
||||||
|
** the update action), and look at their values:
|
||||||
|
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||||
|
** - possibly manipulating values (`setValue`)
|
||||||
|
** - possibly throwing an exception - if you really don't want the update operation to continue.
|
||||||
|
** - doing "whatever else" you may want to do.
|
||||||
|
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
|
||||||
|
**
|
||||||
|
** Note that the full updateInput is available as a field in this class, and the
|
||||||
|
** "old records" (e.g., with values freshly fetched from the backend) will be
|
||||||
|
** available (if the backend supports it) - both as a list (`getOldRecordList`)
|
||||||
|
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public abstract class AbstractPreUpdateCustomizer
|
public abstract class AbstractPreUpdateCustomizer
|
||||||
{
|
{
|
||||||
|
@ -30,8 +30,10 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostDeleteCustomizer;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
|
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||||
@ -48,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||||
@ -79,16 +80,23 @@ public class DeleteAction
|
|||||||
ActionHelper.validateSession(deleteInput);
|
ActionHelper.validateSession(deleteInput);
|
||||||
|
|
||||||
QTableMetaData table = deleteInput.getTable();
|
QTableMetaData table = deleteInput.getTable();
|
||||||
|
String primaryKeyField = table.getPrimaryKeyField();
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
|
||||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
|
|
||||||
DeleteInterface deleteInterface = qModule.getDeleteInterface();
|
|
||||||
|
|
||||||
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
||||||
{
|
{
|
||||||
throw (new QException("A delete request may not contain both a list of primary keys and a query filter."));
|
throw (new QException("A delete request may not contain both a list of primary keys and a query filter."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// load the backend module and its delete interface //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
|
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
|
||||||
|
DeleteInterface deleteInterface = qModule.getDeleteInterface();
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
|
if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
|
||||||
{
|
{
|
||||||
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 backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes");
|
||||||
@ -105,16 +113,62 @@ public class DeleteAction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<QRecord> recordListForAudit = deleteInterface.supportsPreFetchQuery() ? getRecordListForAuditIfNeeded(deleteInput) : new ArrayList<>();
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
List<QRecord> recordsWithValidationErrors = deleteInterface.supportsPreFetchQuery() ? validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit) : new ArrayList<>();
|
// fetch the old list of records (if the backend supports it), for audits, //
|
||||||
|
// for "not-found detection", and for the pre-action to use (if there is one) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Optional<List<QRecord>> oldRecordList = fetchOldRecords(deleteInput, deleteInterface);
|
||||||
|
|
||||||
Optional<AbstractPreDeleteCustomizer> preDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPreDeleteCustomizer.class, table, TableCustomizers.PRE_DELETE_RECORD.getRole());
|
List<QRecord> recordsWithValidationErrors = new ArrayList<>();
|
||||||
if(preDeleteCustomizer.isPresent())
|
List<QRecord> recordsWithValidationWarnings = new ArrayList<>();
|
||||||
|
if(oldRecordList.isPresent())
|
||||||
{
|
{
|
||||||
preDeleteCustomizer.get().setDeleteInput(deleteInput);
|
recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
|
||||||
preDeleteCustomizer.get().apply(null); // todo monday
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// after all validations, run the pre-delete customizer, if there is one //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
Optional<AbstractPreDeleteCustomizer> preDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPreDeleteCustomizer.class, table, TableCustomizers.PRE_DELETE_RECORD.getRole());
|
||||||
|
if(preDeleteCustomizer.isPresent() && oldRecordList.isPresent())
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// make list of records that are still good - to pass into the customizer //
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
List<QRecord> recordsForCustomizer = makeListOfRecordsNotInErrorList(primaryKeyField, oldRecordList.get(), recordsWithValidationErrors);
|
||||||
|
|
||||||
|
preDeleteCustomizer.get().setDeleteInput(deleteInput);
|
||||||
|
List<QRecord> customizerResult = preDeleteCustomizer.get().apply(recordsForCustomizer);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// check if any records got errors in the customizer //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
Set<Serializable> primaryKeysToRemoveFromInput = new HashSet<>();
|
||||||
|
for(QRecord record : customizerResult)
|
||||||
|
{
|
||||||
|
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||||
|
{
|
||||||
|
recordsWithValidationErrors.add(record);
|
||||||
|
primaryKeysToRemoveFromInput.add(record.getValue(primaryKeyField));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
recordsWithValidationWarnings.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// do one mass removal of any bad keys from the input key list //
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
if(!primaryKeysToRemoveFromInput.isEmpty())
|
||||||
|
{
|
||||||
|
deleteInput.getPrimaryKeys().removeAll(primaryKeysToRemoveFromInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// have the backend do the delete //
|
||||||
|
////////////////////////////////////
|
||||||
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
|
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -126,18 +180,94 @@ public class DeleteAction
|
|||||||
deleteOutput.setRecordsWithErrors(new ArrayList<>());
|
deleteOutput.setRecordsWithErrors(new ArrayList<>());
|
||||||
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
|
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
outputRecordsWithErrors.addAll(recordsWithValidationErrors);
|
outputRecordsWithErrors.addAll(recordsWithValidationErrors);
|
||||||
|
|
||||||
|
List<QRecord> outputRecordsWithWarnings = deleteOutput.getRecordsWithWarnings();
|
||||||
|
if(outputRecordsWithWarnings == null)
|
||||||
|
{
|
||||||
|
deleteOutput.setRecordsWithWarnings(new ArrayList<>());
|
||||||
|
outputRecordsWithWarnings = deleteOutput.getRecordsWithWarnings();
|
||||||
|
}
|
||||||
|
outputRecordsWithWarnings.addAll(recordsWithValidationWarnings);
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// delete associations, if applicable //
|
||||||
|
////////////////////////////////////////
|
||||||
manageAssociations(deleteInput);
|
manageAssociations(deleteInput);
|
||||||
|
|
||||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
|
///////////////////////////////////
|
||||||
|
// do the audit //
|
||||||
|
// todo - add input.omitDmlAudit //
|
||||||
|
///////////////////////////////////
|
||||||
|
DMLAuditInput dmlAuditInput = new DMLAuditInput().withTableActionInput(deleteInput);
|
||||||
|
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
|
||||||
|
new DMLAuditAction().execute(dmlAuditInput);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// finally, run the pre-delete customizer, if there is one //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
Optional<AbstractPostDeleteCustomizer> postDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPostDeleteCustomizer.class, table, TableCustomizers.POST_DELETE_RECORD.getRole());
|
||||||
|
if(postDeleteCustomizer.isPresent() && oldRecordList.isPresent())
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// make list of records that are still good - to pass into the customizer //
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
List<QRecord> recordsForCustomizer = makeListOfRecordsNotInErrorList(primaryKeyField, oldRecordList.get(), outputRecordsWithErrors);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
postDeleteCustomizer.get().setDeleteInput(deleteInput);
|
||||||
|
List<QRecord> customizerResult = postDeleteCustomizer.get().apply(recordsForCustomizer);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// check if any records got errors in the customizer //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
for(QRecord record : customizerResult)
|
||||||
|
{
|
||||||
|
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||||
|
{
|
||||||
|
outputRecordsWithErrors.add(record);
|
||||||
|
}
|
||||||
|
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
|
||||||
|
{
|
||||||
|
outputRecordsWithWarnings.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
for(QRecord record : recordsForCustomizer)
|
||||||
|
{
|
||||||
|
record.addWarning("An error occurred after the delete: " + e.getMessage());
|
||||||
|
outputRecordsWithWarnings.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return deleteOutput;
|
return deleteOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static List<QRecord> makeListOfRecordsNotInErrorList(String primaryKeyField, List<QRecord> oldRecordList, List<QRecord> outputRecordsWithErrors)
|
||||||
|
{
|
||||||
|
Map<Serializable, QRecord> recordsWithErrorsMap = outputRecordsWithErrors.stream().collect(Collectors.toMap(r -> r.getValue(primaryKeyField), r -> r));
|
||||||
|
List<QRecord> recordsForCustomizer = new ArrayList<>();
|
||||||
|
for(QRecord record : oldRecordList)
|
||||||
|
{
|
||||||
|
if(!recordsWithErrorsMap.containsKey(record.getValue(primaryKeyField)))
|
||||||
|
{
|
||||||
|
recordsForCustomizer.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recordsForCustomizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -183,12 +313,9 @@ public class DeleteAction
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static List<QRecord> getRecordListForAuditIfNeeded(DeleteInput deleteInput) throws QException
|
private static Optional<List<QRecord>> fetchOldRecords(DeleteInput deleteInput, DeleteInterface deleteInterface) throws QException
|
||||||
{
|
{
|
||||||
List<QRecord> recordListForAudit = null;
|
if(deleteInterface.supportsPreFetchQuery())
|
||||||
|
|
||||||
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(deleteInput);
|
|
||||||
if(AuditLevel.RECORD.equals(auditLevel) || AuditLevel.FIELD.equals(auditLevel))
|
|
||||||
{
|
{
|
||||||
List<Serializable> primaryKeyList = deleteInput.getPrimaryKeys();
|
List<Serializable> primaryKeyList = deleteInput.getPrimaryKeys();
|
||||||
if(CollectionUtils.nullSafeIsEmpty(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
if(CollectionUtils.nullSafeIsEmpty(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
||||||
@ -198,19 +325,16 @@ public class DeleteAction
|
|||||||
|
|
||||||
if(CollectionUtils.nullSafeHasContents(primaryKeyList))
|
if(CollectionUtils.nullSafeHasContents(primaryKeyList))
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// always fetch the records - we'll use them anyway for checking not-exist below //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
QueryInput queryInput = new QueryInput();
|
QueryInput queryInput = new QueryInput();
|
||||||
queryInput.setTransaction(deleteInput.getTransaction());
|
queryInput.setTransaction(deleteInput.getTransaction());
|
||||||
queryInput.setTableName(deleteInput.getTableName());
|
queryInput.setTableName(deleteInput.getTableName());
|
||||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(deleteInput.getTable().getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeyList)));
|
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(deleteInput.getTable().getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeyList)));
|
||||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
recordListForAudit = queryOutput.getRecords();
|
return (Optional.of(queryOutput.getRecords()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (recordListForAudit);
|
return (Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCust
|
|||||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
|
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
|
||||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||||
@ -89,13 +90,23 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
|||||||
|
|
||||||
setAutomationStatusField(insertInput);
|
setAutomationStatusField(insertInput);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// load the backend module and its insert interface //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
|
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
|
||||||
|
InsertInterface insertInterface = qModule.getInsertInterface();
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// run standard validators //
|
||||||
|
/////////////////////////////
|
||||||
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
|
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
|
||||||
setErrorsIfUniqueKeyErrors(insertInput, table);
|
setErrorsIfUniqueKeyErrors(insertInput, table);
|
||||||
validateRequiredFields(insertInput);
|
validateRequiredFields(insertInput);
|
||||||
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
|
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// after all validations, run the pre-insert customizer, if there is one //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
Optional<AbstractPreInsertCustomizer> preInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPreInsertCustomizer.class, table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
Optional<AbstractPreInsertCustomizer> preInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPreInsertCustomizer.class, table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||||
if(preInsertCustomizer.isPresent())
|
if(preInsertCustomizer.isPresent())
|
||||||
{
|
{
|
||||||
@ -103,15 +114,28 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
|||||||
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
|
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
|
||||||
}
|
}
|
||||||
|
|
||||||
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
|
////////////////////////////////////
|
||||||
|
// have the backend do the insert //
|
||||||
|
////////////////////////////////////
|
||||||
|
InsertOutput insertOutput = insertInterface.execute(insertInput);
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// log if there were errors //
|
||||||
|
//////////////////////////////
|
||||||
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
|
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
|
||||||
if(CollectionUtils.nullSafeHasContents(errors))
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
{
|
{
|
||||||
LOG.warn("Errors in insertAction", logPair("tableName", table.getName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
|
LOG.warn("Errors in insertAction", logPair("tableName", table.getName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
// insert any associations in the input records //
|
||||||
|
//////////////////////////////////////////////////
|
||||||
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
|
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
|
||||||
|
|
||||||
|
//////////////////
|
||||||
|
// do the audit //
|
||||||
|
//////////////////
|
||||||
if(insertInput.getOmitDmlAudit())
|
if(insertInput.getOmitDmlAudit())
|
||||||
{
|
{
|
||||||
LOG.debug("Requested to omit DML audit");
|
LOG.debug("Requested to omit DML audit");
|
||||||
@ -121,12 +145,25 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
|||||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
|
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// finally, run the pre-insert customizer, if there is one //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
|
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
|
||||||
if(postInsertCustomizer.isPresent())
|
if(postInsertCustomizer.isPresent())
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
postInsertCustomizer.get().setInsertInput(insertInput);
|
postInsertCustomizer.get().setInsertInput(insertInput);
|
||||||
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
|
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
|
||||||
}
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
for(QRecord record : insertOutput.getRecords())
|
||||||
|
{
|
||||||
|
record.addWarning("An error occurred after the insert: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return insertOutput;
|
return insertOutput;
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||||
@ -92,64 +91,130 @@ public class UpdateAction
|
|||||||
|
|
||||||
QTableMetaData table = updateInput.getTable();
|
QTableMetaData table = updateInput.getTable();
|
||||||
|
|
||||||
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords());
|
//////////////////////////////////////////////////////
|
||||||
|
// load the backend module and its update interface //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
|
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
|
||||||
UpdateInterface updateInterface = qModule.getUpdateInterface();
|
UpdateInterface updateInterface = qModule.getUpdateInterface();
|
||||||
|
|
||||||
List<QRecord> oldRecordList = updateInterface.supportsPreFetchQuery() ? getOldRecordListForAuditIfNeeded(updateInput) : new ArrayList<>();
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// fetch the old list of records (if the backend supports it), for audits, //
|
||||||
|
// for "not-found detection", and for the pre-action to use (if there is one) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Optional<List<QRecord>> oldRecordList = fetchOldRecords(updateInput, updateInterface);
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// run standard validators //
|
||||||
|
/////////////////////////////
|
||||||
|
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords());
|
||||||
validatePrimaryKeysAreGiven(updateInput);
|
validatePrimaryKeysAreGiven(updateInput);
|
||||||
|
|
||||||
if(updateInterface.supportsPreFetchQuery())
|
if(oldRecordList.isPresent())
|
||||||
{
|
{
|
||||||
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList);
|
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
validateRequiredFields(updateInput);
|
validateRequiredFields(updateInput);
|
||||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// after all validations, run the pre-update customizer, if there is one //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
Optional<AbstractPreUpdateCustomizer> preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
Optional<AbstractPreUpdateCustomizer> preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||||
if(preUpdateCustomizer.isPresent())
|
if(preUpdateCustomizer.isPresent())
|
||||||
{
|
{
|
||||||
preUpdateCustomizer.get().setUpdateInput(updateInput);
|
preUpdateCustomizer.get().setUpdateInput(updateInput);
|
||||||
preUpdateCustomizer.get().setOldRecordList(oldRecordList);
|
oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l));
|
||||||
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
|
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// have the backend do the update //
|
||||||
|
////////////////////////////////////
|
||||||
UpdateOutput updateOutput = updateInterface.execute(updateInput);
|
UpdateOutput updateOutput = updateInterface.execute(updateInput);
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// log if there were errors //
|
||||||
|
//////////////////////////////
|
||||||
List<String> errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
|
List<String> errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
|
||||||
if(CollectionUtils.nullSafeHasContents(errors))
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
{
|
{
|
||||||
LOG.warn("Errors in updateAction", logPair("tableName", updateInput.getTableName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
|
LOG.warn("Errors in updateAction", logPair("tableName", updateInput.getTableName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// update (inserting and deleting as needed) any associations in the input records //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
manageAssociations(updateInput);
|
manageAssociations(updateInput);
|
||||||
|
|
||||||
|
//////////////////
|
||||||
|
// do the audit //
|
||||||
|
//////////////////
|
||||||
if(updateInput.getOmitDmlAudit())
|
if(updateInput.getOmitDmlAudit())
|
||||||
{
|
{
|
||||||
LOG.debug("Requested to omit DML audit");
|
LOG.debug("Requested to omit DML audit");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withRecordList(updateOutput.getRecords()).withOldRecordList(oldRecordList));
|
DMLAuditInput dmlAuditInput = new DMLAuditInput()
|
||||||
|
.withTableActionInput(updateInput)
|
||||||
|
.withRecordList(updateOutput.getRecords());
|
||||||
|
oldRecordList.ifPresent(l -> dmlAuditInput.setOldRecordList(l));
|
||||||
|
new DMLAuditAction().execute(dmlAuditInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// finally, run the pre-update customizer, if there is one //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
Optional<AbstractPostUpdateCustomizer> postUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPostUpdateCustomizer.class, table, TableCustomizers.POST_UPDATE_RECORD.getRole());
|
Optional<AbstractPostUpdateCustomizer> postUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPostUpdateCustomizer.class, table, TableCustomizers.POST_UPDATE_RECORD.getRole());
|
||||||
if(postUpdateCustomizer.isPresent())
|
if(postUpdateCustomizer.isPresent())
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
postUpdateCustomizer.get().setUpdateInput(updateInput);
|
postUpdateCustomizer.get().setUpdateInput(updateInput);
|
||||||
postUpdateCustomizer.get().setOldRecordList(oldRecordList);
|
oldRecordList.ifPresent(l -> postUpdateCustomizer.get().setOldRecordList(l));
|
||||||
updateOutput.setRecords(postUpdateCustomizer.get().apply(updateOutput.getRecords()));
|
updateOutput.setRecords(postUpdateCustomizer.get().apply(updateOutput.getRecords()));
|
||||||
}
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
for(QRecord record : updateOutput.getRecords())
|
||||||
|
{
|
||||||
|
record.addWarning("An error occurred after the update: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return updateOutput;
|
return updateOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private Optional<List<QRecord>> fetchOldRecords(UpdateInput updateInput, UpdateInterface updateInterface) throws QException
|
||||||
|
{
|
||||||
|
if(updateInterface.supportsPreFetchQuery())
|
||||||
|
{
|
||||||
|
String primaryKeyField = updateInput.getTable().getPrimaryKeyField();
|
||||||
|
List<Serializable> pkeysBeingUpdated = CollectionUtils.nonNullList(updateInput.getRecords()).stream().map(r -> r.getValue(primaryKeyField)).toList();
|
||||||
|
|
||||||
|
QueryInput queryInput = new QueryInput();
|
||||||
|
queryInput.setTransaction(updateInput.getTransaction());
|
||||||
|
queryInput.setTableName(updateInput.getTableName());
|
||||||
|
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, pkeysBeingUpdated)));
|
||||||
|
// todo - need a limit? what if too many??
|
||||||
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
|
|
||||||
|
return (Optional.of(queryOutput.getRecords()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -383,45 +448,6 @@ public class UpdateAction
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static List<QRecord> getOldRecordListForAuditIfNeeded(UpdateInput updateInput)
|
|
||||||
{
|
|
||||||
if(updateInput.getOmitDmlAudit())
|
|
||||||
{
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(updateInput);
|
|
||||||
List<QRecord> oldRecordList = null;
|
|
||||||
if(AuditLevel.FIELD.equals(auditLevel))
|
|
||||||
{
|
|
||||||
String primaryKeyField = updateInput.getTable().getPrimaryKeyField();
|
|
||||||
List<Serializable> pkeysBeingUpdated = CollectionUtils.nonNullList(updateInput.getRecords()).stream().map(r -> r.getValue(primaryKeyField)).toList();
|
|
||||||
|
|
||||||
QueryInput queryInput = new QueryInput();
|
|
||||||
queryInput.setTransaction(updateInput.getTransaction());
|
|
||||||
queryInput.setTableName(updateInput.getTableName());
|
|
||||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, pkeysBeingUpdated)));
|
|
||||||
// todo - need a limit? what if too many??
|
|
||||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
|
||||||
|
|
||||||
oldRecordList = queryOutput.getRecords();
|
|
||||||
}
|
|
||||||
return oldRecordList;
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
LOG.warn("Error getting old record list for audit", e, logPair("table", updateInput.getTableName()));
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** If the table being updated uses an automation-status field, populate it now.
|
** If the table being updated uses an automation-status field, populate it now.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -37,6 +37,7 @@ public class DeleteOutput extends AbstractActionOutput implements Serializable
|
|||||||
{
|
{
|
||||||
private int deletedRecordCount = 0;
|
private int deletedRecordCount = 0;
|
||||||
private List<QRecord> recordsWithErrors;
|
private List<QRecord> recordsWithErrors;
|
||||||
|
private List<QRecord> recordsWithWarnings;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ public class DeleteOutput extends AbstractActionOutput implements Serializable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -94,6 +96,7 @@ public class DeleteOutput extends AbstractActionOutput implements Serializable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -101,4 +104,50 @@ public class DeleteOutput extends AbstractActionOutput implements Serializable
|
|||||||
{
|
{
|
||||||
deletedRecordCount += i;
|
deletedRecordCount += i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for recordsWithWarnings
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<QRecord> getRecordsWithWarnings()
|
||||||
|
{
|
||||||
|
return (this.recordsWithWarnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for recordsWithWarnings
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setRecordsWithWarnings(List<QRecord> recordsWithWarnings)
|
||||||
|
{
|
||||||
|
this.recordsWithWarnings = recordsWithWarnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for recordsWithWarnings
|
||||||
|
*******************************************************************************/
|
||||||
|
public DeleteOutput withRecordsWithWarnings(List<QRecord> recordsWithWarnings)
|
||||||
|
{
|
||||||
|
this.recordsWithWarnings = recordsWithWarnings;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void addRecordWithWarning(QRecord recordWithWarning)
|
||||||
|
{
|
||||||
|
if(this.recordsWithWarnings == null)
|
||||||
|
{
|
||||||
|
this.recordsWithWarnings = new ArrayList<>();
|
||||||
|
}
|
||||||
|
this.recordsWithWarnings.add(recordWithWarning);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||||
|
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.delete.DeleteInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||||
|
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.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostDeleteCustomizer
|
||||||
|
*******************************************************************************/
|
||||||
|
class AbstractPostDeleteCustomizerTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test() throws QException
|
||||||
|
{
|
||||||
|
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
table.withCustomizer(TableCustomizers.POST_DELETE_RECORD.getRole(), new QCodeReference(AbstractPostDeleteCustomizerTest.PostDelete.class));
|
||||||
|
|
||||||
|
TestUtils.insertRecords(table, List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("firstName", "Homer"),
|
||||||
|
new QRecord().withValue("id", 2).withValue("firstName", "Marge"),
|
||||||
|
new QRecord().withValue("id", 3).withValue("firstName", "Bart")
|
||||||
|
));
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// try a delete that the post-customizer should reject //
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
DeleteInput deleteInput = new DeleteInput();
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1, 2));
|
||||||
|
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
assertEquals(0, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithWarnings().size());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithWarnings().get(0).getValue("id"));
|
||||||
|
assertEquals(2, deleteOutput.getDeletedRecordCount());
|
||||||
|
assertEquals("You shouldn't have deleted Homer...", deleteOutput.getRecordsWithWarnings().get(0).getWarnings().get(0));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(1);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertNull(getOutput.getRecord());
|
||||||
|
|
||||||
|
getInput.setPrimaryKey(2);
|
||||||
|
getOutput = new GetAction().execute(getInput);
|
||||||
|
assertNull(getOutput.getRecord());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static class PostDelete extends AbstractPostDeleteCustomizer
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public List<QRecord> apply(List<QRecord> records)
|
||||||
|
{
|
||||||
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
if(record.getValue("firstName").equals("Homer"))
|
||||||
|
{
|
||||||
|
record.addWarning("You shouldn't have deleted Homer...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (records);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
|
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.get.GetInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||||
|
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.update.UpdateInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||||
|
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.code.QCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for AbstractPreUpdateCustomizer
|
||||||
|
*******************************************************************************/
|
||||||
|
class AbstractPostUpdateCustomizerTest extends BaseTest
|
||||||
|
{
|
||||||
|
private static final String NAME_CHANGES_TABLE = "nameChanges";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = QContext.getQInstance();
|
||||||
|
|
||||||
|
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
table.withCustomizer(TableCustomizers.POST_UPDATE_RECORD.getRole(), new QCodeReference(PostUpdate.class));
|
||||||
|
|
||||||
|
qInstance.addTable(new QTableMetaData()
|
||||||
|
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
|
||||||
|
.withName(NAME_CHANGES_TABLE)
|
||||||
|
.withPrimaryKeyField("id")
|
||||||
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||||
|
.withField(new QFieldMetaData("personId", QFieldType.INTEGER))
|
||||||
|
.withField(new QFieldMetaData("message", QFieldType.STRING)));
|
||||||
|
|
||||||
|
TestUtils.insertRecords(table, List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("firstName", "Homer"),
|
||||||
|
new QRecord().withValue("id", 2).withValue("firstName", "Marge"),
|
||||||
|
new QRecord().withValue("id", 3).withValue("firstName", "Bart")
|
||||||
|
));
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// try an update where the post-update customizer will insert another record //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("firstName", "Homer J.")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertTrue(CollectionUtils.nullSafeIsEmpty(updateOutput.getRecords().get(0).getErrors()));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(NAME_CHANGES_TABLE);
|
||||||
|
getInput.setPrimaryKey(1);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals(1, getOutput.getRecord().getValueInteger("personId"));
|
||||||
|
assertEquals("Changed first name from [Homer] to [Homer J.]", getOutput.getRecord().getValueString("message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// try an update where the post-update customizer will issue a warning (though will have updated the record too) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("firstName", "Warning")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertTrue(CollectionUtils.nullSafeIsEmpty(updateOutput.getRecords().get(0).getErrors()));
|
||||||
|
assertTrue(updateOutput.getRecords().get(0).getWarnings().stream().anyMatch(s -> s.contains("updated to a warning value")));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(1);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals("Warning", getOutput.getRecord().getValueString("firstName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// try an update where the post-update customizer will throw an error (resulting in an updated record with a warning) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("firstName", "throw")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertTrue(CollectionUtils.nullSafeIsEmpty(updateOutput.getRecords().get(0).getErrors()));
|
||||||
|
assertTrue(updateOutput.getRecords().get(0).getWarnings().stream().anyMatch(s -> s.contains("Forced Exception")));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(1);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals("throw", getOutput.getRecord().getValueString("firstName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static class PostUpdate extends AbstractPostUpdateCustomizer
|
||||||
|
{
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||||
|
{
|
||||||
|
List<QRecord> nameChangeRecordsToInsert = new ArrayList<>();
|
||||||
|
|
||||||
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
boolean recordHadError = CollectionUtils.nullSafeHasContents(record.getErrors());
|
||||||
|
boolean inputRecordHadFirstName = record.getValues().containsKey("firstName");
|
||||||
|
boolean inputRecordHadLastName = record.getValues().containsKey("lastName");
|
||||||
|
|
||||||
|
if(recordHadError)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(inputRecordHadFirstName)
|
||||||
|
{
|
||||||
|
QRecord oldRecord = getOldRecordMap().get(record.getValue("id"));
|
||||||
|
if(oldRecord != null && oldRecord.getValue("firstName") != null)
|
||||||
|
{
|
||||||
|
nameChangeRecordsToInsert.add(new QRecord()
|
||||||
|
.withValue("personId", record.getValue("id"))
|
||||||
|
.withValue("message", "Changed first name from [" + oldRecord.getValueString("firstName") + "] to [" + record.getValueString("firstName") + "]")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if("warning".equalsIgnoreCase(record.getValueString("firstName")))
|
||||||
|
{
|
||||||
|
record.addWarning("Record was updated to a warning value");
|
||||||
|
}
|
||||||
|
|
||||||
|
if("throw".equalsIgnoreCase(record.getValueString("firstName")))
|
||||||
|
{
|
||||||
|
throw (new QException("Forced Exception"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(inputRecordHadLastName)
|
||||||
|
{
|
||||||
|
QRecord oldRecord = getOldRecordMap().get(record.getValue("id"));
|
||||||
|
if(oldRecord != null && oldRecord.getValue("lastName") != null)
|
||||||
|
{
|
||||||
|
nameChangeRecordsToInsert.add(new QRecord()
|
||||||
|
.withValue("personId", record.getValue("id"))
|
||||||
|
.withValue("message", "Changed last name from [" + oldRecord.getValueString("lastName") + "] to [" + record.getValueString("lastName") + "]")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(CollectionUtils.nullSafeHasContents(nameChangeRecordsToInsert))
|
||||||
|
{
|
||||||
|
InsertInput insertInput = new InsertInput();
|
||||||
|
insertInput.setTableName(NAME_CHANGES_TABLE);
|
||||||
|
insertInput.setRecords(nameChangeRecordsToInsert);
|
||||||
|
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||||
|
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.delete.DeleteInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||||
|
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.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for AbstractPreDeleteCustomizer
|
||||||
|
*******************************************************************************/
|
||||||
|
class AbstractPreDeleteCustomizerTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test() throws QException
|
||||||
|
{
|
||||||
|
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
table.withCustomizer(TableCustomizers.PRE_DELETE_RECORD.getRole(), new QCodeReference(PreDelete.class));
|
||||||
|
|
||||||
|
TestUtils.insertRecords(table, List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("firstName", "Homer"),
|
||||||
|
new QRecord().withValue("id", 2).withValue("firstName", "Marge"),
|
||||||
|
new QRecord().withValue("id", 3).withValue("firstName", "Bart")
|
||||||
|
));
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// try a delete that the pre-customizer should reject //
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
DeleteInput deleteInput = new DeleteInput();
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1, 2));
|
||||||
|
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertEquals(0, deleteOutput.getRecordsWithWarnings().size());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().get(0).getValue("id"));
|
||||||
|
assertEquals(1, deleteOutput.getDeletedRecordCount());
|
||||||
|
assertEquals("You may not delete a Homer.", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(1);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals("Homer", getOutput.getRecord().getValueString("firstName"));
|
||||||
|
|
||||||
|
getInput.setPrimaryKey(2);
|
||||||
|
getOutput = new GetAction().execute(getInput);
|
||||||
|
assertNull(getOutput.getRecord());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static class PreDelete extends AbstractPreDeleteCustomizer
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public List<QRecord> apply(List<QRecord> records)
|
||||||
|
{
|
||||||
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
if(record.getValue("firstName").equals("Homer"))
|
||||||
|
{
|
||||||
|
record.addError("You may not delete a Homer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (records);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -24,12 +24,21 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||||
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.model.actions.tables.get.GetInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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.code.QCodeReference;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -42,10 +51,74 @@ class AbstractPreUpdateCustomizerTest extends BaseTest
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Test
|
@Test
|
||||||
void test()
|
void test() throws QException
|
||||||
{
|
{
|
||||||
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD.getRole(), new QCodeReference(PreUpdate.class));
|
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD.getRole(), new QCodeReference(PreUpdate.class));
|
||||||
|
|
||||||
|
TestUtils.insertRecords(table, List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("firstName", "Homer"),
|
||||||
|
new QRecord().withValue("id", 2).withValue("firstName", "Marge"),
|
||||||
|
new QRecord().withValue("id", 3).withValue("firstName", "Bart")
|
||||||
|
));
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
// try an update that the pre-customizer should reject //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("firstName", "--")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(s -> s.contains("must contain at least one letter")));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(1);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals("Homer", getOutput.getRecord().getValueString("firstName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
// try an update that gets its data changed //
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 2).withValue("firstName", "Ms.")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertTrue(updateOutput.getRecords().get(0).getErrors().isEmpty());
|
||||||
|
assertEquals("Ms.", updateOutput.getRecords().get(0).getValueString("firstName"));
|
||||||
|
assertEquals("Simpson", updateOutput.getRecords().get(0).getValueString("lastName"));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(2);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals("Ms.", getOutput.getRecord().getValueString("firstName"));
|
||||||
|
assertEquals("Simpson", getOutput.getRecord().getValueString("lastName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// try an update that uses data from the previous version of the record //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 3).withValue("lastName", "Simpson")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertTrue(updateOutput.getRecords().get(0).getErrors().isEmpty());
|
||||||
|
assertEquals("BART", updateOutput.getRecords().get(0).getValueString("firstName"));
|
||||||
|
assertEquals("Simpson", updateOutput.getRecords().get(0).getValueString("lastName"));
|
||||||
|
|
||||||
|
GetInput getInput = new GetInput();
|
||||||
|
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
getInput.setPrimaryKey(3);
|
||||||
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
|
assertEquals("BART", getOutput.getRecord().getValueString("firstName"));
|
||||||
|
assertEquals("Simpson", getOutput.getRecord().getValueString("lastName"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +126,7 @@ class AbstractPreUpdateCustomizerTest extends BaseTest
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static class PreUpdate extends AbstractPreUpdateCustomizer
|
public static class PreUpdate extends AbstractPreUpdateCustomizer
|
||||||
{
|
{
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
@ -61,7 +134,44 @@ class AbstractPreUpdateCustomizerTest extends BaseTest
|
|||||||
@Override
|
@Override
|
||||||
public List<QRecord> apply(List<QRecord> records)
|
public List<QRecord> apply(List<QRecord> records)
|
||||||
{
|
{
|
||||||
return null;
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
boolean inputRecordHadFirstName = record.getValues().containsKey("firstName");
|
||||||
|
boolean inputRecordHadLastName = record.getValues().containsKey("lastName");
|
||||||
|
|
||||||
|
if(inputRecordHadFirstName)
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// if updating first name, give an error if it has no letters //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
if(!record.getValueString("firstName").matches(".*\\w.*"))
|
||||||
|
{
|
||||||
|
record.addError("First name must contain at least one letter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
// if setting firstname to Ms., update last name to Simpson //
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
if(record.getValueString("firstName").equals("Ms."))
|
||||||
|
{
|
||||||
|
record.setValue("lastName", "Simpson");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// if updating the person's last name, set their first name to all caps //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
if(inputRecordHadLastName)
|
||||||
|
{
|
||||||
|
QRecord oldRecord = getOldRecordMap().get(record.getValue("id"));
|
||||||
|
if(oldRecord != null && oldRecord.getValue("firstName") != null)
|
||||||
|
{
|
||||||
|
record.setValue("firstName", oldRecord.getValueString("firstName").toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (records);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +276,7 @@ public class TestUtils
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static void insertRecords(QInstance qInstance, QTableMetaData table, List<QRecord> records) throws QException
|
public static void insertRecords(QTableMetaData table, List<QRecord> records) throws QException
|
||||||
{
|
{
|
||||||
InsertInput insertInput = new InsertInput();
|
InsertInput insertInput = new InsertInput();
|
||||||
insertInput.setTableName(table.getName());
|
insertInput.setTableName(table.getName());
|
||||||
@ -286,6 +286,17 @@ public class TestUtils
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Deprecated
|
||||||
|
public static void insertRecords(QInstance qInstance, QTableMetaData table, List<QRecord> records) throws QException
|
||||||
|
{
|
||||||
|
insertRecords(table, records);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
Reference in New Issue
Block a user