Merged dev into feature/CE-938-order-release-automation

This commit is contained in:
2024-06-04 19:55:25 -05:00
65 changed files with 806 additions and 167 deletions

View File

@ -127,7 +127,6 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public String getInsertOrUpdate()
{
return switch(this)

View File

@ -50,10 +50,7 @@ public interface RecordCustomizerUtilityInterface
/*******************************************************************************
** Container for an old value and a new value.
*******************************************************************************/
@SuppressWarnings("checkstyle:MethodName")
record Change(Serializable oldValue, Serializable newValue)
{
}
record Change(Serializable oldValue, Serializable newValue) {}
/*******************************************************************************

View File

@ -256,7 +256,6 @@ public enum DateTimeGroupBy
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public Instant roundDown(Instant instant, ZoneId zoneId)
{
ZonedDateTime zoned = instant.atZone(zoneId);

View File

@ -500,7 +500,6 @@ public class PermissionsHelper
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType)
{
if(rules == null || rules.getLevel() == null)
@ -515,10 +514,10 @@ public class PermissionsHelper
if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType))
{
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
default -> PrivatePermissionSubType.HAS_ACCESS;
};
{
case NOT_PROTECTED -> null;
default -> PrivatePermissionSubType.HAS_ACCESS;
};
}
else
{
@ -527,30 +526,30 @@ public class PermissionsHelper
// permission sub-type to what we expect to be set for the table //
////////////////////////////////////////////////////////////////////////////////////////////////////////
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
case READ_WRITE_PERMISSIONS ->
{
case NOT_PROTECTED -> null;
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
case READ_WRITE_PERMISSIONS ->
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
{
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
{
yield (originalPermissionSubType);
}
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.WRITE);
}
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.READ);
}
else
{
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
}
yield (originalPermissionSubType);
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
};
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.WRITE);
}
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.READ);
}
else
{
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
}
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
};
}
}

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
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.metadata.code.QCodeReference;
@ -93,7 +94,7 @@ public class RunBackendStepAction
//////////////////////////////////////////////////////////////////////////////////////
// ensure input data is set as needed - use callback object to get anything missing //
//////////////////////////////////////////////////////////////////////////////////////
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData);
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData, process);
ensureInputFieldsAreInRequest(runBackendStepInput, backendStepMetaData);
////////////////////////////////////////////////////////////////////
@ -178,7 +179,7 @@ public class RunBackendStepAction
** check if this step uses a record list - and if so, if we need to get one
** via the callback
*******************************************************************************/
private void ensureRecordsAreInRequest(RunBackendStepInput runBackendStepInput, QBackendStepMetaData step) throws QException
private void ensureRecordsAreInRequest(RunBackendStepInput runBackendStepInput, QBackendStepMetaData step, QProcessMetaData process) throws QException
{
QFunctionInputMetaData inputMetaData = step.getInputMetaData();
if(inputMetaData != null && inputMetaData.getRecordListMetaData() != null)
@ -201,9 +202,44 @@ public class RunBackendStepAction
queryInput.setFilter(callback.getQueryFilter());
//////////////////////////////////////////////////////////////////////////////////////////
// if process has a max-no of records, set a limit on the process of that number plus 1 //
// (the plus 1 being so we can see "oh, you selected more than that many; error!" //
//////////////////////////////////////////////////////////////////////////////////////////
if(process.getMaxInputRecords() != null)
{
if(callback.getQueryFilter() == null)
{
queryInput.setFilter(new QQueryFilter());
}
queryInput.getFilter().setLimit(process.getMaxInputRecords() + 1);
}
QueryOutput queryOutput = new QueryAction().execute(queryInput);
runBackendStepInput.setRecords(queryOutput.getRecords());
// todo - handle 0 results found?
////////////////////////////////////////////////////////////////////////////////
// if process defines a max, and more than the max were found, throw an error //
////////////////////////////////////////////////////////////////////////////////
if(process.getMaxInputRecords() != null)
{
if(queryOutput.getRecords().size() > process.getMaxInputRecords())
{
throw (new QUserFacingException("Too many records were selected for this process. At most, only " + process.getMaxInputRecords() + " can be selected."));
}
}
/////////////////////////////////////////////////////////////////////////////////
// if process defines a min, and fewer than the min were found, throw an error //
/////////////////////////////////////////////////////////////////////////////////
if(process.getMinInputRecords() != null)
{
if(queryOutput.getRecords().size() < process.getMinInputRecords())
{
throw (new QUserFacingException("Too few records were selected for this process. At least " + process.getMinInputRecords() + " must be selected."));
}
}
}
}
}

View File

@ -457,7 +457,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
if(finalTransformStep != null)
{
finalTransformStepInput.setRecords(records);
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
finalTransformStep.runOnePage(finalTransformStepInput, finalTransformStepOutput);
records = finalTransformStepOutput.getRecords();
}

View File

@ -77,7 +77,6 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
{
QCodeReference codeReference = input.getCodeReference();

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -335,6 +336,9 @@ public class UpdateAction
QTableMetaData table = updateInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
/////////////////////////////////////////////////////////////
// todo - evolve to use lock tree (e.g., from multi-locks) //
/////////////////////////////////////////////////////////////
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
@ -395,7 +399,7 @@ public class UpdateAction
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));

View File

@ -101,7 +101,7 @@ public class ValidateRecordSecurityLockHelper
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>());
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
/////////////////////////////////
// propagate errors to records //
@ -141,7 +141,7 @@ public class ValidateRecordSecurityLockHelper
** BUT - WRITE locks - in their case, we read the record no matter what, and in
** here we need to verify we have a key that allows us to WRITE the record.
*******************************************************************************/
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition) throws QException
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
@ -152,7 +152,7 @@ public class ValidateRecordSecurityLockHelper
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
treePosition.remove(treePosition.size() - 1);
i++;
}
@ -192,7 +192,7 @@ public class ValidateRecordSecurityLockHelper
}
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
@ -337,7 +337,7 @@ public class ValidateRecordSecurityLockHelper
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
@ -370,14 +370,14 @@ public class ValidateRecordSecurityLockHelper
{
String primaryKeyField = table.getPrimaryKeyField();
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
Integer madeUpPrimaryKey = -1;
Integer madeUpPrimaryKey = Integer.MIN_VALUE / 2;
for(QRecord record : records)
{
if(record.getValue(primaryKeyField) == null)
{
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
record.setValue(primaryKeyField, madeUpPrimaryKey);
madeUpPrimaryKey--;
madeUpPrimaryKey++;
}
}
return madeUpPrimaryKeys;
@ -390,7 +390,6 @@ public class ValidateRecordSecurityLockHelper
** MultiRecordSecurityLock, with only the appropriate lock-scopes being included
** (e.g., read-locks for selects, write-locks for insert/update/delete).
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
static MultiRecordSecurityLock getRecordSecurityLocks(QTableMetaData table, Action action)
{
List<RecordSecurityLock> allLocksOnTable = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
@ -445,9 +444,9 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
{
if(recordSecurityValue == null)
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
{
/////////////////////////////////////////////////////////////////
// handle null values - error if the NullValueBehavior is DENY //

View File

@ -118,6 +118,7 @@ public class QPossibleValueTranslator
}
/*******************************************************************************
** Constructor
**
@ -421,7 +422,6 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
private String doFormatPossibleValue(String formatString, List<String> valueFields, Object id, String label)
{
List<Object> values = new ArrayList<>();

View File

@ -49,7 +49,6 @@ public interface DisplayFormat
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
static String getExcelFormat(String javaDisplayFormat)
{
if(javaDisplayFormat == null)
@ -58,21 +57,21 @@ public interface DisplayFormat
}
return switch(javaDisplayFormat)
{
case DisplayFormat.DEFAULT -> null;
case DisplayFormat.COMMAS -> "#,##0";
case DisplayFormat.DECIMAL1 -> "0.0";
case DisplayFormat.DECIMAL2 -> "0.00";
case DisplayFormat.DECIMAL3 -> "0.000";
case DisplayFormat.DECIMAL1_COMMAS -> "#,##0.0";
case DisplayFormat.DECIMAL2_COMMAS -> "#,##0.00";
case DisplayFormat.DECIMAL3_COMMAS -> "#,##0.000";
case DisplayFormat.CURRENCY -> "$#,##0.00";
case DisplayFormat.PERCENT -> "0%";
case DisplayFormat.PERCENT_POINT1 -> "0.0%";
case DisplayFormat.PERCENT_POINT2 -> "0.00%";
default -> null;
};
{
case DisplayFormat.DEFAULT -> null;
case DisplayFormat.COMMAS -> "#,##0";
case DisplayFormat.DECIMAL1 -> "0.0";
case DisplayFormat.DECIMAL2 -> "0.00";
case DisplayFormat.DECIMAL3 -> "0.000";
case DisplayFormat.DECIMAL1_COMMAS -> "#,##0.0";
case DisplayFormat.DECIMAL2_COMMAS -> "#,##0.00";
case DisplayFormat.DECIMAL3_COMMAS -> "#,##0.000";
case DisplayFormat.CURRENCY -> "$#,##0.00";
case DisplayFormat.PERCENT -> "0%";
case DisplayFormat.PERCENT_POINT1 -> "0.0%";
case DisplayFormat.PERCENT_POINT2 -> "0.00%";
default -> null;
};
}
}

View File

@ -44,14 +44,13 @@ public enum JoinType
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public JoinType flip()
{
return switch(this)
{
case ONE_TO_MANY -> MANY_TO_ONE;
case MANY_TO_ONE -> ONE_TO_MANY;
case MANY_TO_MANY, ONE_TO_ONE -> this;
};
{
case ONE_TO_MANY -> MANY_TO_ONE;
case MANY_TO_ONE -> ONE_TO_MANY;
case MANY_TO_MANY, ONE_TO_ONE -> this;
};
}
}

View File

@ -39,18 +39,21 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
*******************************************************************************/
public class QStepMetaDataDeserializer extends JsonDeserializer<QStepMetaData>
{
/***************************************************************************
**
***************************************************************************/
@Override
@SuppressWarnings("checkstyle:Indentation")
public QStepMetaData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
{
TreeNode treeNode = jsonParser.readValueAsTree();
String stepType = DeserializerUtils.readTextValue(treeNode, "stepType");
Class<? extends QStepMetaData> targetClass = switch(stepType)
{
case "backend" -> QBackendStepMetaData.class;
case "frontend" -> QFrontendStepMetaData.class;
default -> throw new IllegalArgumentException("Unsupported StepType " + stepType + " for deserialization");
};
{
case "backend" -> QBackendStepMetaData.class;
case "frontend" -> QFrontendStepMetaData.class;
default -> throw new IllegalArgumentException("Unsupported StepType " + stepType + " for deserialization");
};
return (DeserializerUtils.reflectivelyDeserialize(targetClass, treeNode));
}

View File

@ -150,10 +150,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is how we allow the actions within this class to work without themselves having a logged-in user. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
private static QSession chickenAndEggSession = new QSession()
{
};
private static QSession chickenAndEggSession = null;
@ -163,14 +160,29 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
*******************************************************************************/
private QSession getChickenAndEggSession()
{
for(String typeName : QContext.getQInstance().getSecurityKeyTypes().keySet())
if(chickenAndEggSession == null)
{
QSecurityKeyType keyType = QContext.getQInstance().getSecurityKeyType(typeName);
if(StringUtils.hasContent(keyType.getAllAccessKeyName()))
////////////////////////////////////////////////////////////////////////////////
// if the static field is null, then let's make a new session; //
// prime it with all all-access keys; and then set it in the static field. //
// and, if 2 threads get in here at the same time, no real harm will be done, //
// other than creating the session twice, and whoever loses the race, that'll //
// be the one that stays in the field //
////////////////////////////////////////////////////////////////////////////////
QSession newChickenAndEggSession = new QSession();
for(String typeName : QContext.getQInstance().getSecurityKeyTypes().keySet())
{
chickenAndEggSession = chickenAndEggSession.withSecurityKeyValue(keyType.getAllAccessKeyName(), true);
QSecurityKeyType keyType = QContext.getQInstance().getSecurityKeyType(typeName);
if(StringUtils.hasContent(keyType.getAllAccessKeyName()))
{
newChickenAndEggSession.withSecurityKeyValue(keyType.getAllAccessKeyName(), true);
}
}
chickenAndEggSession = newChickenAndEggSession;
}
return (chickenAndEggSession);
}

View File

@ -769,7 +769,6 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
private static Serializable computeAggregate(List<QRecord> records, Aggregate aggregate, QTableMetaData table)
{
String fieldName = aggregate.getFieldName();

View File

@ -38,7 +38,6 @@ public class MockCountAction implements CountInterface
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:MagicNumber")
public CountOutput execute(CountInput countInput) throws QException
{
try

View File

@ -95,10 +95,8 @@ public class MockQueryAction implements QueryInterface
** Get a mock value to use, based on its type.
**
*******************************************************************************/
@SuppressWarnings("checkstyle:MagicNumber")
public static Serializable getMockValue(QTableMetaData table, String field)
{
// @formatter:off // IJ can't do new-style switch correctly yet...
return switch(table.getField(field).getType())
{
case STRING -> UUID.randomUUID().toString();
@ -112,7 +110,6 @@ public class MockQueryAction implements QueryInterface
case PASSWORD -> "abc***234";
default -> throw new IllegalStateException("Unexpected value: " + table.getField(field).getType());
};
// @formatter:on
}
}

View File

@ -134,7 +134,6 @@ public class BackendQueryFilterUtils
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public static boolean doesCriteriaMatch(QFilterCriteria criterion, String fieldName, Serializable value)
{
ListIterator<Serializable> valueListIterator = criterion.getValues().listIterator();

View File

@ -112,12 +112,12 @@ public class BulkDeleteLoadStep extends LoadViaDeleteStep implements ProcessSumm
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
////////////////////////////
// have base class delete //
////////////////////////////
super.run(runBackendStepInput, runBackendStepOutput);
super.runOnePage(runBackendStepInput, runBackendStepOutput);
QTableMetaData table = runBackendStepInput.getInstance().getTable(runBackendStepInput.getTableName());
String primaryKeyFieldName = table.getPrimaryKeyField();

View File

@ -78,7 +78,7 @@ public class BulkDeleteTransformStep extends AbstractTransformStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
QTableMetaData table = runBackendStepInput.getTable();
String primaryKeyField = table.getPrimaryKeyField();

View File

@ -114,12 +114,12 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
////////////////////////////
// have base class update //
////////////////////////////
super.run(runBackendStepInput, runBackendStepOutput);
super.runOnePage(runBackendStepInput, runBackendStepOutput);
////////////////////////////////////////////////////////
// roll up results based on output from update action //

View File

@ -103,7 +103,7 @@ public class BulkEditTransformStep extends AbstractTransformStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// on the validate step, we haven't read the full file, so we don't know how many rows there are - thus //

View File

@ -118,7 +118,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
int rowsInThisPage = runBackendStepInput.getRecords().size();
QTableMetaData table = runBackendStepInput.getInstance().getTable(runBackendStepInput.getTableName());

View File

@ -61,7 +61,7 @@ public class LoadViaDeleteStep extends AbstractLoadStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
QTableMetaData table = runBackendStepInput.getTable();

View File

@ -73,7 +73,7 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
evaluateRecords(runBackendStepInput);
insertAndUpdateRecords(runBackendStepInput, runBackendStepOutput);

View File

@ -60,7 +60,7 @@ public class LoadViaInsertStep extends AbstractLoadStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setInputSource(getInputSource());

View File

@ -62,7 +62,7 @@ public class LoadViaUpdateStep extends AbstractLoadStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
UpdateInput updateInput = new UpdateInput();
updateInput.setInputSource(getInputSource());

View File

@ -40,7 +40,7 @@ public class NoopLoadStep extends AbstractLoadStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
///////////
// noop. //

View File

@ -67,7 +67,7 @@ public class NoopTransformStep extends AbstractTransformStep
*
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
////////////////////////////////
// return if no input records //

View File

@ -58,7 +58,6 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
**
*******************************************************************************/
@Override
@SuppressWarnings("checkstyle:indentation")
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
Optional<QBackendTransaction> transaction = Optional.empty();
@ -276,7 +275,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
/////////////////////////////////////////////////////
// pass the records through the transform function //
/////////////////////////////////////////////////////
transformStep.run(streamedBackendStepInput, streamedBackendStepOutput);
transformStep.runOnePage(streamedBackendStepInput, streamedBackendStepOutput);
List<AuditInput> auditInputListFromTransform = streamedBackendStepOutput.getAuditInputList();
//////////////////////////////////////////////////////////////////////
@ -294,7 +293,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, streamedBackendStepOutput.getRecords());
streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput);
loadStep.run(streamedBackendStepInput, streamedBackendStepOutput);
loadStep.runOnePage(streamedBackendStepInput, streamedBackendStepOutput);
List<AuditInput> auditInputListFromLoad = streamedBackendStepOutput.getAuditInputList();
//////////////////////////////////////////////////////////////////////

View File

@ -214,7 +214,7 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
/////////////////////////////////////////////////////
// pass the records through the transform function //
/////////////////////////////////////////////////////
transformStep.run(streamedBackendStepInput, streamedBackendStepOutput);
transformStep.runOnePage(streamedBackendStepInput, streamedBackendStepOutput);
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //

View File

@ -178,7 +178,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
/////////////////////////////////////////////////////
// pass the records through the transform function //
/////////////////////////////////////////////////////
transformStep.run(streamedBackendStepInput, streamedBackendStepOutput);
transformStep.runOnePage(streamedBackendStepInput, streamedBackendStepOutput);
//////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state //

View File

@ -118,7 +118,7 @@ public class GarbageCollectorTransformStep extends AbstractTransformStep
*
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
////////////////////////////////
// return if no input records //

View File

@ -203,7 +203,7 @@ public abstract class AbstractMergeDuplicatesTransformStep extends AbstractTrans
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{

View File

@ -61,9 +61,9 @@ public class MergeDuplicatesLoadStep extends LoadViaInsertOrUpdateStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
super.run(runBackendStepInput, runBackendStepOutput);
super.runOnePage(runBackendStepInput, runBackendStepOutput);
ListingHash<String, Serializable> otherTableIdsToDelete = (ListingHash<String, Serializable>) runBackendStepInput.getValue("otherTableIdsToDelete");
ListingHash<String, QQueryFilter> otherTableFiltersToDelete = (ListingHash<String, QQueryFilter>) runBackendStepInput.getValue("otherTableFiltersToDelete");

View File

@ -113,7 +113,7 @@ public class RunRecordScriptLoadStep extends AbstractLoadStep implements Process
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
runBackendStepInput.getAsyncJobCallback().updateStatus("Running script");

View File

@ -88,9 +88,9 @@ public class RunRecordScriptTransformStep extends NoopTransformStep
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
super.run(runBackendStepInput, runBackendStepOutput);
super.runOnePage(runBackendStepInput, runBackendStepOutput);
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_VALIDATION_SUMMARY, doGetProcessSummary(runBackendStepOutput, false));
}

View File

@ -190,6 +190,8 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
this(sourceTable, sourceTableKeyField, destinationTable, destinationTableForeignKey, true, true);
}
/*******************************************************************************
** artificial method, here to make jacoco see that this class is indeed
** included in test coverage...
@ -207,7 +209,7 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{

View File

@ -78,7 +78,7 @@ public class PauseQuartzJobsProcess extends AbstractLoadStep implements MetaData
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
try
{

View File

@ -78,7 +78,7 @@ public class ResumeQuartzJobsProcess extends AbstractLoadStep implements MetaDat
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
try
{

View File

@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IS_NOT_BLANK;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
@ -311,6 +312,28 @@ public class QQueryFilterDeduper
log.add("Merge two not-equals as not-in");
continue;
}
else if(IN.equals(other.getOperator()) && IS_NOT_BLANK.equals(criteria.getOperator()))
{
//////////////////////////////////////////////////////////////////////////
// for an IN and IS_NOT_BLANK, remove the IS_NOT_BLANK - it's redundant //
//////////////////////////////////////////////////////////////////////////
iterator.remove();
didAnyGood = true;
log.add("Removing redundant is-not-blank");
continue;
}
else if(IS_NOT_BLANK.equals(other.getOperator()) && IN.equals(criteria.getOperator()))
{
//////////////////////////////////////////////////////////////////////////
// for an IN and IS_NOT_BLANK, remove the IS_NOT_BLANK - it's redundant //
//////////////////////////////////////////////////////////////////////////
other.setOperator(IN);
other.setValues(new ArrayList<>(criteria.getValues()));
iterator.remove();
didAnyGood = true;
log.add("Removing redundant is-not-blank");
continue;
}
else
{
log.add("Fail because unhandled operator pair");

View File

@ -793,7 +793,6 @@ public class ValueUtils
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public static Serializable getValueAsFieldType(QFieldType type, Object value)
{
return switch(type)

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.dashboard.widgets;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for Aggregate2DTableWidgetRenderer
*******************************************************************************/
class Aggregate2DTableWidgetRendererTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
new QRecord().withValue("lastName", "Simpson").withValue("homeStateId", 50),
new QRecord().withValue("lastName", "Simpson").withValue("homeStateId", 50),
new QRecord().withValue("lastName", "Simpson").withValue("homeStateId", 50),
new QRecord().withValue("lastName", "Simpson").withValue("homeStateId", 49),
new QRecord().withValue("lastName", "Flanders").withValue("homeStateId", 49),
new QRecord().withValue("lastName", "Flanders").withValue("homeStateId", 49),
new QRecord().withValue("lastName", "Burns").withValue("homeStateId", 50)
)));
RenderWidgetInput input = new RenderWidgetInput();
input.setWidgetMetaData(new QWidgetMetaData()
.withDefaultValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY)
.withDefaultValue("valueField", "id")
.withDefaultValue("rowField", "lastName")
.withDefaultValue("columnField", "homeStateId")
.withDefaultValue("orderBys", "row")
);
RenderWidgetOutput output = new Aggregate2DTableWidgetRenderer().render(input);
TableData tableData = (TableData) output.getWidgetData();
System.out.println(tableData.getRows());
TableDataAssert.assertThat(tableData)
.hasRowWithColumnContaining("_row", "Simpson", row ->
row.hasColumnContaining("50", "3")
.hasColumnContaining("49", "1")
.hasColumnContaining("_total", "4"))
.hasRowWithColumnContaining("_row", "Flanders", row ->
row.hasColumnContaining("50", "0")
.hasColumnContaining("49", "2")
.hasColumnContaining("_total", "2"))
.hasRowWithColumnContaining("_row", "Burns", row ->
row.hasColumnContaining("50", "1")
.hasColumnContaining("49", "0")
.hasColumnContaining("_total", "1"))
.hasRowWithColumnContaining("_row", "Total", row ->
row.hasColumnContaining("50", "4")
.hasColumnContaining("49", "3")
.hasColumnContaining("_total", "7"));
List<String> rowLabels = tableData.getRows().stream().map(r -> r.get("_row").toString()).toList();
assertEquals(List.of("Burns", "Flanders", "Simpson", "Total"), rowLabels);
}
}

View File

@ -0,0 +1,215 @@
/*
* Copyright © 2022-2023. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
/*******************************************************************************
** AssertJ assert class for widget TableData
*******************************************************************************/
public class TableDataAssert extends AbstractAssert<TableDataAssert, TableData>
{
/*******************************************************************************
**
*******************************************************************************/
protected TableDataAssert(TableData actual, Class<?> selfType)
{
super(actual, selfType);
}
/*******************************************************************************
**
*******************************************************************************/
public static TableDataAssert assertThat(RenderWidgetOutput widgetOutput)
{
Assertions.assertThat(widgetOutput).isNotNull();
QWidgetData widgetData = widgetOutput.getWidgetData();
Assertions.assertThat(widgetData).isNotNull();
Assertions.assertThat(widgetData).isInstanceOf(TableData.class);
return (new TableDataAssert((TableData) widgetData, TableDataAssert.class));
}
/*******************************************************************************
**
*******************************************************************************/
public static TableDataAssert assertThat(TableData actual)
{
return (new TableDataAssert(actual, TableDataAssert.class));
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasSize(int expectedSize)
{
Assertions.assertThat(actual.getRows()).hasSize(expectedSize);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasSizeAtLeast(int sizeAtLeast)
{
Assertions.assertThat(actual.getRows()).hasSizeGreaterThanOrEqualTo(sizeAtLeast);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert doesNotHaveRowWithColumnContaining(String columnName, String containingValue)
{
for(Map<String, Object> row : actual.getRows())
{
if(row.containsKey(columnName))
{
String value = String.valueOf(row.get(columnName));
if(value != null && value.contains(containingValue))
{
failWithMessage("Failed because a row was found with a value in the [" + columnName + "] column containing [" + containingValue + "]"
+ (containingValue.equals(value) ? "" : " (full value: [" + value + "])."));
}
}
}
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasRowWithColumnContaining(String columnName, String containingValue)
{
hasRowWithColumnContaining(columnName, containingValue, (row) ->
{
});
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasRowWithColumnContaining(String columnName, String containingValue, Consumer<TableDataRowAssert> rowAsserter)
{
return hasRowWithColumnPredicate(columnName, value -> value != null && value.contains(containingValue), "containing [" + containingValue + "]", rowAsserter);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasRowWithColumnMatching(String columnName, String matchingValue)
{
hasRowWithColumnMatching(columnName, matchingValue, (row) ->
{
});
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasRowWithColumnMatching(String columnName, String matchingValue, Consumer<TableDataRowAssert> rowAsserter)
{
return hasRowWithColumnPredicate(columnName, value -> value != null && value.matches(matchingValue), "matching [" + matchingValue + "]", rowAsserter);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasRowWithColumnEqualTo(String columnName, String equalToValue)
{
hasRowWithColumnEqualTo(columnName, equalToValue, (row) ->
{
});
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataAssert hasRowWithColumnEqualTo(String columnName, String equalToValue, Consumer<TableDataRowAssert> rowAsserter)
{
return hasRowWithColumnPredicate(columnName, value -> Objects.equals(value, equalToValue), "equalTo [" + equalToValue + "]", rowAsserter);
}
/*******************************************************************************
**
*******************************************************************************/
private TableDataAssert hasRowWithColumnPredicate(String columnName, Predicate<String> predicate, String predicateDescription, Consumer<TableDataRowAssert> rowAsserter)
{
List<String> foundValuesInColumn = new ArrayList<>();
for(Map<String, Object> row : actual.getRows())
{
if(row.containsKey(columnName))
{
String value = String.valueOf(row.get(columnName));
foundValuesInColumn.add(value);
if(predicate.test(value))
{
TableDataRowAssert tableDataRowAssert = TableDataRowAssert.assertThat(row);
rowAsserter.accept(tableDataRowAssert);
return (this);
}
}
}
if(actual.getRows().isEmpty())
{
failWithMessage("Failed because there are no rows in the table.");
}
else if(foundValuesInColumn.isEmpty())
{
failWithMessage("Failed to find any rows with a column named: [" + columnName + "]");
}
else
{
failWithMessage("Failed to find a row with column [" + columnName + "] " + predicateDescription
+ ".\nFound values were:\n" + StringUtils.join("\n", foundValuesInColumn));
}
return (null);
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright © 2022-2023. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
** AssertJ assert class for a row of data from a widget TableData
*******************************************************************************/
public class TableDataRowAssert extends AbstractAssert<TableDataRowAssert, Map<String, Object>>
{
/*******************************************************************************
**
*******************************************************************************/
protected TableDataRowAssert(Map<String, Object> actual, Class<?> selfType)
{
super(actual, selfType);
}
/*******************************************************************************
**
*******************************************************************************/
public static TableDataRowAssert assertThat(Map<String, Object> actual)
{
return (new TableDataRowAssert(actual, TableDataRowAssert.class));
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataRowAssert hasColumnContaining(String columnName, String containingValue)
{
String value = String.valueOf(actual.get(columnName));
Assertions.assertThat(value)
.withFailMessage("Expected column [" + columnName + "] in row [" + actual + "] to contain [" + containingValue + "], but it didn't")
.contains(containingValue);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataRowAssert hasNoSubRows()
{
Object subRowsObject = actual.get("subRows");
if(subRowsObject != null)
{
@SuppressWarnings("unchecked")
List<Map<String, Object>> subRowsList = (List<Map<String, Object>>) subRowsObject;
if(!subRowsList.isEmpty())
{
fail("Row [" + actual + "] should not have had any subRows, but it did.");
}
}
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataRowAssert hasSubRowWithColumnContaining(String columnName, String containingValue)
{
hasSubRowWithColumnContaining(columnName, containingValue, (row) ->
{
});
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
private TableDataRowAssert hasSubRowWithColumnPredicate(String columnName, Function<Object, Boolean> predicate, String predicateDescription, Consumer<TableDataRowAssert> rowAsserter)
{
Object subRowsObject = actual.get("subRows");
Assertions.assertThat(subRowsObject)
.withFailMessage("subRows should not be null").isNotNull()
.withFailMessage("subRows should be a List").isInstanceOf(List.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> subRowsList = (List<Map<String, Object>>) subRowsObject;
List<String> foundValuesInColumn = new ArrayList<>();
for(Map<String, Object> row : subRowsList)
{
if(row.containsKey(columnName))
{
String value = String.valueOf(row.get(columnName));
foundValuesInColumn.add(value);
if(value != null && predicate.apply(value))
{
TableDataRowAssert tableDataRowAssert = TableDataRowAssert.assertThat(row);
rowAsserter.accept(tableDataRowAssert);
return (this);
}
}
}
if(foundValuesInColumn.isEmpty())
{
failWithMessage("Failed to find any rows with a column named: [" + columnName + "]");
}
else
{
failWithMessage("Failed to find a row with column [" + columnName + "] " + predicateDescription
+ ".\nFound values were:\n" + StringUtils.join("\n", foundValuesInColumn));
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataRowAssert hasSubRowWithColumnMatching(String columnName, String matchesValue, Consumer<TableDataRowAssert> rowAsserter)
{
Function<Object, Boolean> predicate = (value) -> ValueUtils.getValueAsString(value).matches(matchesValue);
return hasSubRowWithColumnPredicate(columnName, predicate, " matching [" + matchesValue + "]", rowAsserter);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataRowAssert hasSubRowWithColumnContaining(String columnName, String containingValue, Consumer<TableDataRowAssert> rowAsserter)
{
Function<Object, Boolean> predicate = (value) -> ValueUtils.getValueAsString(value).contains(containingValue);
return hasSubRowWithColumnPredicate(columnName, predicate, " containing [" + containingValue + "]", rowAsserter);
}
/*******************************************************************************
**
*******************************************************************************/
public TableDataRowAssert doesNotHaveSubRowWithColumnContaining(String columnName, String containingValue)
{
Object subRowsObject = actual.get("subRows");
if(subRowsObject != null)
{
Assertions.assertThat(subRowsObject).withFailMessage("subRows should be a List").isInstanceOf(List.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> subRowsList = (List<Map<String, Object>>) subRowsObject;
for(Map<String, Object> row : subRowsList)
{
if(row.containsKey(columnName))
{
String value = String.valueOf(row.get(columnName));
if(value != null && value.contains(containingValue))
{
failWithMessage("Failed because a row was found with a value in the [" + columnName + "] column containing [" + containingValue + "]"
+ (containingValue.equals(value) ? "" : " (full value: [" + value + "])."));
}
}
}
}
return (this);
}
}

View File

@ -27,13 +27,21 @@ import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -53,7 +61,7 @@ public class RunBackendStepActionTest extends BaseTest
{
TestCallback callback = new TestCallback();
RunBackendStepInput request = new RunBackendStepInput();
request.setProcessName("greet");
request.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
request.setStepName("prepare");
request.setCallback(callback);
RunBackendStepOutput result = new RunBackendStepAction().execute(request);
@ -67,6 +75,60 @@ public class RunBackendStepActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMinMaxInputRecords() throws QException
{
////////////////////////////////////////////
// put a min-input-records on the process //
////////////////////////////////////////////
QContext.getQInstance().getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withMinInputRecords(5);
//////////////////////////////////////////////////////////////////////////////////////
// insert fewer than that min - then run w/ non-filtered filter, and assert we fail //
//////////////////////////////////////////////////////////////////////////////////////
for(int i = 0; i < 3; i++)
{
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord().withValue("firstName", String.valueOf(i))));
}
Supplier<RunBackendStepInput> inputSupplier = () ->
{
RunBackendStepInput input = new RunBackendStepInput();
input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
input.setStepName("prepare");
input.setCallback(QProcessCallbackFactory.forFilter(new QQueryFilter()));
return (input);
};
assertThatThrownBy(() -> new RunBackendStepAction().execute(inputSupplier.get()))
.isInstanceOf(QUserFacingException.class)
.hasMessageContaining("Too few records");
////////////////////////////////////////////////////
// insert a few more - and then it should succeed //
////////////////////////////////////////////////////
for(int i = 3; i < 10; i++)
{
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord().withValue("firstName", String.valueOf(i))));
}
new RunBackendStepAction().execute(inputSupplier.get());
////////////////////////////////////////////////////////////
// now put a max on the process, and it should fail again //
////////////////////////////////////////////////////////////
QContext.getQInstance().getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withMaxInputRecords(8);
assertThatThrownBy(() -> new RunBackendStepAction().execute(inputSupplier.get()))
.isInstanceOf(QUserFacingException.class)
.hasMessageContaining("Too many records");
}
/*******************************************************************************
**
*******************************************************************************/
@ -100,20 +162,20 @@ public class RunBackendStepActionTest extends BaseTest
for(QFieldMetaData field : fields)
{
rs.put(field.getName(), switch(field.getType())
{
case STRING -> "ABC";
case INTEGER -> 42;
case LONG -> 42L;
case DECIMAL -> new BigDecimal("47");
case BOOLEAN -> true;
case DATE, TIME, DATE_TIME -> null;
case TEXT -> """
ABC
XYZ""";
case HTML -> "<b>Oh my</b>";
case PASSWORD -> "myPa**word";
case BLOB -> new byte[] { 1, 2, 3, 4 };
});
{
case STRING -> "ABC";
case INTEGER -> 42;
case LONG -> 42L;
case DECIMAL -> new BigDecimal("47");
case BOOLEAN -> true;
case DATE, TIME, DATE_TIME -> null;
case TEXT -> """
ABC
XYZ""";
case HTML -> "<b>Oh my</b>";
case PASSWORD -> "myPa**word";
case BLOB -> new byte[] { 1, 2, 3, 4 };
});
}
return (rs);
}

View File

@ -1327,7 +1327,7 @@ public class QInstanceValidatorTest extends BaseTest
{
TableAutomationAction action = getAction0(qInstance);
action.setCodeReference(null);
action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
action.setProcessName(TestUtils.PROCESS_NAME_BASEPULL);
},
"different table");
}
@ -2281,7 +2281,7 @@ public class QInstanceValidatorTest extends BaseTest
///////////////////////////////////////////////
public abstract class TestAbstractClass extends AbstractTransformStep implements BackendStep
{
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
}
}
@ -2293,7 +2293,7 @@ public class QInstanceValidatorTest extends BaseTest
///////////////////////////////////////////////
private class TestPrivateClass extends AbstractTransformStep implements BackendStep
{
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
}
@ -2320,7 +2320,7 @@ public class QInstanceValidatorTest extends BaseTest
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
}

View File

@ -111,7 +111,7 @@ class BulkInsertTransformStepTest extends BaseTest
newQRecord("uuid-D", "SKU-2", 1) // violate sku/storeId UK from pre-existing records
));
bulkInsertTransformStep.preRun(input, output);
bulkInsertTransformStep.run(input, output);
bulkInsertTransformStep.runOnePage(input, output);
///////////////////////////////////////////////////////
// assert about the records that passed successfully //
@ -193,7 +193,7 @@ class BulkInsertTransformStepTest extends BaseTest
newQRecord("uuid-D", "SKU-2", 1) // violate sku/storeId UK from pre-existing records
));
bulkInsertTransformStep.preRun(input, output);
bulkInsertTransformStep.run(input, output);
bulkInsertTransformStep.runOnePage(input, output);
///////////////////////////////////////////////////////
// assert that all records pass.

View File

@ -76,7 +76,7 @@ class LoadViaInsertOrUpdateStepTest extends BaseTest
input.setRecords(inputRecordList);
input.addValue(LoadViaInsertOrUpdateStep.FIELD_DESTINATION_TABLE, TestUtils.TABLE_NAME_PERSON_MEMORY);
RunBackendStepOutput output = new RunBackendStepOutput();
new LoadViaInsertOrUpdateStep().run(input, output);
new LoadViaInsertOrUpdateStep().runOnePage(input, output);
List<QRecord> qRecords = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_PERSON_MEMORY);
assertEquals(2, qRecords.size());

View File

@ -56,7 +56,7 @@ class NoopTransformStepTest extends BaseTest
RunBackendStepOutput output = new RunBackendStepOutput();
NoopTransformStep noopTransformStep = new NoopTransformStep();
noopTransformStep.run(input, output);
noopTransformStep.runOnePage(input, output);
assertEquals(1, output.getRecords().size());
assertEquals(47, output.getRecords().get(0).getValueInteger("id"));

View File

@ -419,7 +419,7 @@ public class StreamedETLWithFrontendProcessTest extends BaseTest
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
for(QRecord qRecord : runBackendStepInput.getRecords())
{
@ -452,7 +452,7 @@ public class StreamedETLWithFrontendProcessTest extends BaseTest
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
for(QRecord qRecord : runBackendStepInput.getRecords())
{
@ -518,7 +518,7 @@ public class StreamedETLWithFrontendProcessTest extends BaseTest
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
for(QRecord qRecord : runBackendStepInput.getRecords())
{
@ -552,7 +552,7 @@ public class StreamedETLWithFrontendProcessTest extends BaseTest
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
for(QRecord qRecord : runBackendStepInput.getRecords())
{
@ -584,7 +584,7 @@ public class StreamedETLWithFrontendProcessTest extends BaseTest
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
///////////////////////////////////
// just pass the records through //

View File

@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.GREATER_THAN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IS_NOT_BLANK;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter.BooleanOperator.OR;
@ -352,4 +353,23 @@ class QQueryFilterDeduperTest extends BaseTest
assertEquals(contradiction, dedupeFilter(contradiction));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInAndIsNotBlank()
{
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 1, 2)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 1, 2))
.withCriteria(new QFilterCriteria("f", IS_NOT_BLANK))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 1, 2)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IS_NOT_BLANK))
.withCriteria(new QFilterCriteria("f", IN, 1, 2))
));
}
}

View File

@ -1132,21 +1132,21 @@ public class TestUtils
{
return new QProcessMetaData()
.withName(PROCESS_NAME_GREET_PEOPLE)
.withTableName(TABLE_NAME_PERSON)
.withTableName(TABLE_NAME_PERSON_MEMORY)
.addStep(new QBackendStepMetaData()
.withName("prepare")
.withCode(new QCodeReference()
.withName(MockBackendStep.class.getName())
.withCodeType(QCodeType.JAVA))
.withInputData(new QFunctionInputMetaData()
.withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON))
.withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON_MEMORY))
.withFieldList(List.of(
new QFieldMetaData("greetingPrefix", QFieldType.STRING),
new QFieldMetaData("greetingSuffix", QFieldType.STRING)
)))
.withOutputMetaData(new QFunctionOutputMetaData()
.withRecordListMetaData(new QRecordListMetaData()
.withTableName(TABLE_NAME_PERSON)
.withTableName(TABLE_NAME_PERSON_MEMORY)
.withField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
)
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))