mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-21 14:38:43 +00:00
Merged feature/sprint-43-cleanup into integration/sprint-43
This commit is contained in:
@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
|||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
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.RunBackendStepInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
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.QueryInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
@ -82,7 +83,7 @@ public class RunBackendStepAction
|
|||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
// ensure input data is set as needed - use callback object to get anything missing //
|
// ensure input data is set as needed - use callback object to get anything missing //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData);
|
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData, process);
|
||||||
ensureInputFieldsAreInRequest(runBackendStepInput, backendStepMetaData);
|
ensureInputFieldsAreInRequest(runBackendStepInput, backendStepMetaData);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////
|
||||||
@ -167,7 +168,7 @@ public class RunBackendStepAction
|
|||||||
** check if this step uses a record list - and if so, if we need to get one
|
** check if this step uses a record list - and if so, if we need to get one
|
||||||
** via the callback
|
** 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();
|
QFunctionInputMetaData inputMetaData = step.getInputMetaData();
|
||||||
if(inputMetaData != null && inputMetaData.getRecordListMetaData() != null)
|
if(inputMetaData != null && inputMetaData.getRecordListMetaData() != null)
|
||||||
@ -190,9 +191,44 @@ public class RunBackendStepAction
|
|||||||
|
|
||||||
queryInput.setFilter(callback.getQueryFilter());
|
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);
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
runBackendStepInput.setRecords(queryOutput.getRecords());
|
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."));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -335,6 +336,9 @@ public class UpdateAction
|
|||||||
QTableMetaData table = updateInput.getTable();
|
QTableMetaData table = updateInput.getTable();
|
||||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
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()));
|
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
|
||||||
|
|
||||||
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
|
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
|
||||||
@ -395,7 +399,7 @@ public class UpdateAction
|
|||||||
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
|
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
|
||||||
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
|
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))
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
{
|
{
|
||||||
errors.forEach(e -> record.addError(e));
|
errors.forEach(e -> record.addError(e));
|
||||||
|
@ -101,7 +101,7 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
// actually check lock values //
|
// actually check lock values //
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
|
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 //
|
// 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
|
** 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.
|
** 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)
|
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||||
{
|
{
|
||||||
@ -152,7 +152,7 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
||||||
{
|
{
|
||||||
treePosition.add(i);
|
treePosition.add(i);
|
||||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition);
|
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
|
||||||
treePosition.remove(treePosition.size() - 1);
|
treePosition.remove(treePosition.size() - 1);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@ -192,7 +192,7 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
Serializable recordSecurityValue = record.getValue(field.getName());
|
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))
|
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||||
{
|
{
|
||||||
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
|
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
|
||||||
@ -337,7 +337,7 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
|
|
||||||
for(QRecord inputRecord : inputRecords)
|
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))
|
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||||
{
|
{
|
||||||
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
|
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
|
||||||
@ -370,14 +370,14 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
{
|
{
|
||||||
String primaryKeyField = table.getPrimaryKeyField();
|
String primaryKeyField = table.getPrimaryKeyField();
|
||||||
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
|
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
|
||||||
Integer madeUpPrimaryKey = -1;
|
Integer madeUpPrimaryKey = Integer.MIN_VALUE / 2;
|
||||||
for(QRecord record : records)
|
for(QRecord record : records)
|
||||||
{
|
{
|
||||||
if(record.getValue(primaryKeyField) == null)
|
if(record.getValue(primaryKeyField) == null)
|
||||||
{
|
{
|
||||||
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
|
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
|
||||||
record.setValue(primaryKeyField, madeUpPrimaryKey);
|
record.setValue(primaryKeyField, madeUpPrimaryKey);
|
||||||
madeUpPrimaryKey--;
|
madeUpPrimaryKey++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return madeUpPrimaryKeys;
|
return madeUpPrimaryKeys;
|
||||||
@ -445,9 +445,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 //
|
// handle null values - error if the NullValueBehavior is DENY //
|
||||||
|
@ -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. //
|
// 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()
|
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);
|
return (chickenAndEggSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.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.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.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_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.QCriteriaOperator.NOT_IN;
|
||||||
|
|
||||||
@ -311,6 +312,28 @@ public class QQueryFilterDeduper
|
|||||||
log.add("Merge two not-equals as not-in");
|
log.add("Merge two not-equals as not-in");
|
||||||
continue;
|
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
|
else
|
||||||
{
|
{
|
||||||
log.add("Fail because unhandled operator pair");
|
log.add("Fail because unhandled operator pair");
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,13 +27,21 @@ import java.math.BigDecimal;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
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.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.RunBackendStepInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
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.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.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@ -53,7 +61,7 @@ public class RunBackendStepActionTest extends BaseTest
|
|||||||
{
|
{
|
||||||
TestCallback callback = new TestCallback();
|
TestCallback callback = new TestCallback();
|
||||||
RunBackendStepInput request = new RunBackendStepInput();
|
RunBackendStepInput request = new RunBackendStepInput();
|
||||||
request.setProcessName("greet");
|
request.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
|
||||||
request.setStepName("prepare");
|
request.setStepName("prepare");
|
||||||
request.setCallback(callback);
|
request.setCallback(callback);
|
||||||
RunBackendStepOutput result = new RunBackendStepAction().execute(request);
|
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)
|
for(QFieldMetaData field : fields)
|
||||||
{
|
{
|
||||||
rs.put(field.getName(), switch(field.getType())
|
rs.put(field.getName(), switch(field.getType())
|
||||||
{
|
{
|
||||||
case STRING -> "ABC";
|
case STRING -> "ABC";
|
||||||
case INTEGER -> 42;
|
case INTEGER -> 42;
|
||||||
case LONG -> 42L;
|
case LONG -> 42L;
|
||||||
case DECIMAL -> new BigDecimal("47");
|
case DECIMAL -> new BigDecimal("47");
|
||||||
case BOOLEAN -> true;
|
case BOOLEAN -> true;
|
||||||
case DATE, TIME, DATE_TIME -> null;
|
case DATE, TIME, DATE_TIME -> null;
|
||||||
case TEXT -> """
|
case TEXT -> """
|
||||||
ABC
|
ABC
|
||||||
XYZ""";
|
XYZ""";
|
||||||
case HTML -> "<b>Oh my</b>";
|
case HTML -> "<b>Oh my</b>";
|
||||||
case PASSWORD -> "myPa**word";
|
case PASSWORD -> "myPa**word";
|
||||||
case BLOB -> new byte[] { 1, 2, 3, 4 };
|
case BLOB -> new byte[] { 1, 2, 3, 4 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (rs);
|
return (rs);
|
||||||
}
|
}
|
||||||
|
@ -1304,7 +1304,7 @@ public class QInstanceValidatorTest extends BaseTest
|
|||||||
{
|
{
|
||||||
TableAutomationAction action = getAction0(qInstance);
|
TableAutomationAction action = getAction0(qInstance);
|
||||||
action.setCodeReference(null);
|
action.setCodeReference(null);
|
||||||
action.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE);
|
action.setProcessName(TestUtils.PROCESS_NAME_BASEPULL);
|
||||||
},
|
},
|
||||||
"different table");
|
"different table");
|
||||||
}
|
}
|
||||||
|
@ -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.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.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.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_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.QCriteriaOperator.NOT_IN;
|
||||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter.BooleanOperator.OR;
|
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));
|
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))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1132,21 +1132,21 @@ public class TestUtils
|
|||||||
{
|
{
|
||||||
return new QProcessMetaData()
|
return new QProcessMetaData()
|
||||||
.withName(PROCESS_NAME_GREET_PEOPLE)
|
.withName(PROCESS_NAME_GREET_PEOPLE)
|
||||||
.withTableName(TABLE_NAME_PERSON)
|
.withTableName(TABLE_NAME_PERSON_MEMORY)
|
||||||
.addStep(new QBackendStepMetaData()
|
.addStep(new QBackendStepMetaData()
|
||||||
.withName("prepare")
|
.withName("prepare")
|
||||||
.withCode(new QCodeReference()
|
.withCode(new QCodeReference()
|
||||||
.withName(MockBackendStep.class.getName())
|
.withName(MockBackendStep.class.getName())
|
||||||
.withCodeType(QCodeType.JAVA))
|
.withCodeType(QCodeType.JAVA))
|
||||||
.withInputData(new QFunctionInputMetaData()
|
.withInputData(new QFunctionInputMetaData()
|
||||||
.withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON))
|
.withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON_MEMORY))
|
||||||
.withFieldList(List.of(
|
.withFieldList(List.of(
|
||||||
new QFieldMetaData("greetingPrefix", QFieldType.STRING),
|
new QFieldMetaData("greetingPrefix", QFieldType.STRING),
|
||||||
new QFieldMetaData("greetingSuffix", QFieldType.STRING)
|
new QFieldMetaData("greetingSuffix", QFieldType.STRING)
|
||||||
)))
|
)))
|
||||||
.withOutputMetaData(new QFunctionOutputMetaData()
|
.withOutputMetaData(new QFunctionOutputMetaData()
|
||||||
.withRecordListMetaData(new QRecordListMetaData()
|
.withRecordListMetaData(new QRecordListMetaData()
|
||||||
.withTableName(TABLE_NAME_PERSON)
|
.withTableName(TABLE_NAME_PERSON_MEMORY)
|
||||||
.withField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
.withField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
||||||
)
|
)
|
||||||
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
||||||
|
@ -405,6 +405,9 @@ public class AbstractMongoDBAction
|
|||||||
QQueryFilter securityFilter = new QQueryFilter();
|
QQueryFilter securityFilter = new QQueryFilter();
|
||||||
securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// todo - evolve to use lock tree //
|
||||||
|
////////////////////////////////////
|
||||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||||
{
|
{
|
||||||
addSubFilterForRecordSecurityLock(QContext.getQInstance(), QContext.getQSession(), table, securityFilter, recordSecurityLock, null, table.getName(), false);
|
addSubFilterForRecordSecurityLock(QContext.getQInstance(), QContext.getQSession(), table, securityFilter, recordSecurityLock, null, table.getName(), false);
|
||||||
|
Reference in New Issue
Block a user