Completed first round implementation of {pre,post}{insert,delete} actions

This commit is contained in:
2023-05-08 15:06:28 -05:00
parent 6aef4d92e8
commit 265847e01a
15 changed files with 978 additions and 84 deletions

View File

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

View File

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

View File

@ -26,12 +26,27 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
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.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
{
@ -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;

View File

@ -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
{
@ -62,4 +79,5 @@ public abstract class AbstractPreDeleteCustomizer
{
this.deleteInput = deleteInput;
}
}

View File

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

View File

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

View File

@ -30,8 +30,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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.QCodeLoader;
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.QueryOutput;
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.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
@ -78,17 +79,24 @@ public class DeleteAction
{
ActionHelper.validateSession(deleteInput);
QTableMetaData table = deleteInput.getTable();
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
DeleteInterface deleteInterface = qModule.getDeleteInterface();
QTableMetaData table = deleteInput.getTable();
String primaryKeyField = table.getPrimaryKeyField();
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."));
}
//////////////////////////////////////////////////////
// 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())
{
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());
if(preDeleteCustomizer.isPresent())
List<QRecord> recordsWithValidationErrors = new ArrayList<>();
List<QRecord> recordsWithValidationWarnings = new ArrayList<>();
if(oldRecordList.isPresent())
{
preDeleteCustomizer.get().setDeleteInput(deleteInput);
preDeleteCustomizer.get().apply(null); // todo monday
recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
}
///////////////////////////////////////////////////////////////////////////
// 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);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -126,18 +180,94 @@ public class DeleteAction
deleteOutput.setRecordsWithErrors(new ArrayList<>());
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
}
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);
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;
}
/*******************************************************************************
**
*******************************************************************************/
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;
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(deleteInput);
if(AuditLevel.RECORD.equals(auditLevel) || AuditLevel.FIELD.equals(auditLevel))
if(deleteInterface.supportsPreFetchQuery())
{
List<Serializable> primaryKeyList = deleteInput.getPrimaryKeys();
if(CollectionUtils.nullSafeIsEmpty(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
@ -198,19 +325,16 @@ public class DeleteAction
if(CollectionUtils.nullSafeHasContents(primaryKeyList))
{
////////////////////////////////////////////////////////////////////////////////////
// always fetch the records - we'll use them anyway for checking not-exist below //
////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(deleteInput.getTableName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(deleteInput.getTable().getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeyList)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
recordListForAudit = queryOutput.getRecords();
return (Optional.of(queryOutput.getRecords()));
}
}
return (recordListForAudit);
return (Optional.empty());
}

View File

@ -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.QCodeLoader;
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.ValidateRecordSecurityLockHelper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
@ -89,13 +90,23 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
setAutomationStatusField(insertInput);
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
//////////////////////////////////////////////////////
// load the backend module and its insert interface //
//////////////////////////////////////////////////////
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
InsertInterface insertInterface = qModule.getInsertInterface();
/////////////////////////////
// run standard validators //
/////////////////////////////
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
setErrorsIfUniqueKeyErrors(insertInput, table);
validateRequiredFields(insertInput);
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());
if(preInsertCustomizer.isPresent())
{
@ -103,15 +114,28 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
}
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
////////////////////////////////////
// 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();
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)));
}
//////////////////////////////////////////////////
// insert any associations in the input records //
//////////////////////////////////////////////////
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
//////////////////
// do the audit //
//////////////////
if(insertInput.getOmitDmlAudit())
{
LOG.debug("Requested to omit DML audit");
@ -121,11 +145,24 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
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());
if(postInsertCustomizer.isPresent())
{
postInsertCustomizer.get().setInsertInput(insertInput);
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
try
{
postInsertCustomizer.get().setInsertInput(insertInput);
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;

View File

@ -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.UpdateOutput;
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.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
@ -92,57 +91,98 @@ public class UpdateAction
QTableMetaData table = updateInput.getTable();
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords());
//////////////////////////////////////////////////////
// load the backend module and its update interface //
//////////////////////////////////////////////////////
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
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);
if(updateInterface.supportsPreFetchQuery())
if(oldRecordList.isPresent())
{
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList);
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
}
validateRequiredFields(updateInput);
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());
if(preUpdateCustomizer.isPresent())
{
preUpdateCustomizer.get().setUpdateInput(updateInput);
preUpdateCustomizer.get().setOldRecordList(oldRecordList);
oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l));
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
}
////////////////////////////////////
// have the backend do the update //
////////////////////////////////////
UpdateOutput updateOutput = updateInterface.execute(updateInput);
//////////////////////////////
// log if there were errors //
//////////////////////////////
List<String> errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
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)));
}
/////////////////////////////////////////////////////////////////////////////////////
// update (inserting and deleting as needed) any associations in the input records //
/////////////////////////////////////////////////////////////////////////////////////
manageAssociations(updateInput);
//////////////////
// do the audit //
//////////////////
if(updateInput.getOmitDmlAudit())
{
LOG.debug("Requested to omit DML audit");
}
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());
if(postUpdateCustomizer.isPresent())
{
postUpdateCustomizer.get().setUpdateInput(updateInput);
postUpdateCustomizer.get().setOldRecordList(oldRecordList);
updateOutput.setRecords(postUpdateCustomizer.get().apply(updateOutput.getRecords()));
try
{
postUpdateCustomizer.get().setUpdateInput(updateInput);
oldRecordList.ifPresent(l -> postUpdateCustomizer.get().setOldRecordList(l));
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;
@ -150,6 +190,31 @@ public class UpdateAction
/*******************************************************************************
**
*******************************************************************************/
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.
*******************************************************************************/

View File

@ -37,6 +37,7 @@ public class DeleteOutput extends AbstractActionOutput implements Serializable
{
private int deletedRecordCount = 0;
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;
}
/*******************************************************************************
** 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);
}
}

View File

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

View File

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

View File

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

View File

@ -24,12 +24,21 @@ 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.GetAction;
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.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.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.assertTrue;
/*******************************************************************************
@ -42,10 +51,74 @@ class AbstractPreUpdateCustomizerTest extends BaseTest
**
*******************************************************************************/
@Test
void test()
void test() throws QException
{
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
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
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);
}
}

View File

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