Merged dev into feature/CE-1887-mobile-android-app

This commit is contained in:
2024-11-04 07:58:14 -06:00
16 changed files with 570 additions and 69 deletions

View File

@ -225,7 +225,13 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName()));
///////////////////////////////////////////////////////
// originally, this case threw... //
// but i think it's better to record the audit, just //
// missing its security key value, then to fail... //
///////////////////////////////////////////////////////
// throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName()));
LOG.info("Missing securityKeyValue in audit request", logPair("table", auditSingleInput.getAuditTableName()), logPair("securityKey", recordSecurityLock.getSecurityKeyType()));
}
}

View File

@ -81,6 +81,7 @@ public class RunProcessAction
{
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
public static final String BASEPULL_KEY_VALUE = "basepullKeyValue";
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
@ -728,9 +729,13 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
protected String determineBasepullKeyValue(QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
protected String determineBasepullKeyValue(QProcessMetaData process, RunProcessInput runProcessInput, BasepullConfiguration basepullConfiguration) throws QException
{
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
if(runProcessInput.getValueString(BASEPULL_KEY_VALUE) != null)
{
basepullKeyValue = runProcessInput.getValueString(BASEPULL_KEY_VALUE);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if process specifies that it uses variants, look for that data in the session and append to our basepull key //
@ -762,7 +767,7 @@ public class RunProcessAction
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
///////////////////////////////////////
// get the stored basepull timestamp //
@ -842,7 +847,7 @@ public class RunProcessAction
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
///////////////////////////////////////
// get the stored basepull timestamp //

View File

@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -266,6 +267,22 @@ public class QueryAction
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** entities to be returned, and you just want to pass in a table name and filter.
*******************************************************************************/
public static <T extends QRecordEntity> List<T> execute(String tableName, Class<T> entityClass, QQueryFilter filter) throws QException
{
QueryAction queryAction = new QueryAction();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(filter);
QueryOutput queryOutput = queryAction.execute(queryInput);
return (queryOutput.getRecordEntities(entityClass));
}
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** records to be returned, and you just want to pass in a table name and filter.

View File

@ -260,9 +260,6 @@ public class SearchPossibleValueSourceAction
}
}
// todo - skip & limit as params
queryFilter.setLimit(250);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
///////////////////////////////////////////////////////////////////////////////////////////////////////
@ -272,6 +269,9 @@ public class SearchPossibleValueSourceAction
queryFilter = input.getDefaultQueryFilter();
}
// todo - skip & limit as params
queryFilter.setLimit(250);
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
queryInput.setFilter(queryFilter);

View File

@ -82,7 +82,7 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////
// we will get a TON of more output if this gets turned up, so be cautious //
/////////////////////////////////////////////////////////////////////////////
private Level logLevel = Level.OFF;
private Level logLevel = Level.OFF;
private Level logLevelForFilter = Level.OFF;
@ -404,6 +404,12 @@ public class JoinsContext
chainIsInner = false;
}
if(hasAllAccessKey(recordSecurityLock))
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
@ -423,6 +429,12 @@ public class JoinsContext
chainIsInner = false;
}
if(hasAllAccessKey(recordSecurityLock))
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getLeftTable());
@ -456,44 +468,53 @@ public class JoinsContext
/***************************************************************************
**
***************************************************************************/
private boolean hasAllAccessKey(RecordSecurityLock recordSecurityLock)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
QSession session = QContext.getQSession();
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return (true);
}
}
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
private void addSubFilterForRecordSecurityLock(RecordSecurityLock recordSecurityLock, QTableMetaData table, String tableNameOrAlias, boolean isOuter, QueryJoin sourceQueryJoin)
{
QSession session = QContext.getQSession();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
boolean haveAllAccessKey = false;
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
boolean haveAllAccessKey = hasAllAccessKey(recordSecurityLock);
if(haveAllAccessKey)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have all-access on this key, then we don't need a criterion for it (as long as we're in an AND filter) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
if(sourceQueryJoin != null)
{
haveAllAccessKey = true;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it //
// this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sourceQueryJoin.withSecurityCriteria(new ArrayList<>());
}
if(sourceQueryJoin != null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it //
// this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sourceQueryJoin.withSecurityCriteria(new ArrayList<>());
}
////////////////////////////////////////////////////////////////////////////////////////
// if we're in an AND filter, then we don't need a criteria for this lock, so return. //
////////////////////////////////////////////////////////////////////////////////////////
boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
if(inAnAndFilter)
{
return;
}
////////////////////////////////////////////////////////////////////////////////////////
// if we're in an AND filter, then we don't need a criteria for this lock, so return. //
////////////////////////////////////////////////////////////////////////////////////////
boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
if(inAnAndFilter)
{
return;
}
}
@ -545,7 +566,7 @@ public class JoinsContext
}
else
{
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
List<Serializable> securityKeyValues = QContext.getQSession().getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -247,10 +247,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
//////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired //
//////////////////////////////////////////////////////////////
if(getCustomizer() != null)
{
getCustomizer().finalCustomizeSession(qInstance, qSession);
}
finalCustomizeSession(qInstance, qSession);
return (qSession);
}
@ -311,10 +308,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
//////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired //
//////////////////////////////////////////////////////////////
if(getCustomizer() != null)
{
getCustomizer().finalCustomizeSession(qInstance, qSession);
}
finalCustomizeSession(qInstance, qSession);
return (qSession);
}
@ -360,6 +354,23 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
/***************************************************************************
**
***************************************************************************/
private void finalCustomizeSession(QInstance qInstance, QSession qSession)
{
if(getCustomizer() != null)
{
QContext.withTemporaryContext(QContext.capture(), () ->
{
QContext.setQSession(getChickenAndEggSession());
getCustomizer().finalCustomizeSession(qInstance, qSession);
});
}
}
/*******************************************************************************
** Insert a session as a new record into userSession table
*******************************************************************************/

View File

@ -40,6 +40,10 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
*******************************************************************************/
public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep implements BasepullExtractStepInterface
{
protected static final String SECONDS_TO_SUBTRACT_FROM_THIS_RUN_TIME_KEY = "secondsToSubtractFromThisRunTimeForTimestampQuery";
protected static final String SECONDS_TO_SUBTRACT_FROM_LAST_RUN_TIME_KEY = "secondsToSubtractFromLastRunTimeForTimestampQuery";
/*******************************************************************************
**
@ -124,7 +128,8 @@ public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep implements
*******************************************************************************/
protected String getLastRunTimeString(RunBackendStepInput runBackendStepInput) throws QException
{
Instant lastRunTime = runBackendStepInput.getBasepullLastRunTime();
Instant lastRunTime = runBackendStepInput.getBasepullLastRunTime();
Instant updatedRunTime = lastRunTime;
//////////////////////////////////////////////////////////////////////////////////////////////
// allow the timestamps to be adjusted by the specified number of seconds. //
@ -135,10 +140,19 @@ public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep implements
Serializable basepullConfigurationValue = runBackendStepInput.getValue(RunProcessAction.BASEPULL_CONFIGURATION);
if(basepullConfigurationValue instanceof BasepullConfiguration basepullConfiguration && basepullConfiguration.getSecondsToSubtractFromLastRunTimeForTimestampQuery() != null)
{
lastRunTime = lastRunTime.minusSeconds(basepullConfiguration.getSecondsToSubtractFromLastRunTimeForTimestampQuery());
updatedRunTime = lastRunTime.minusSeconds(basepullConfiguration.getSecondsToSubtractFromLastRunTimeForTimestampQuery());
}
return (lastRunTime.toString());
//////////////////////////////////////////////////////////////
// if an override was found in the params, use that instead //
//////////////////////////////////////////////////////////////
if(runBackendStepInput.getValueString(SECONDS_TO_SUBTRACT_FROM_LAST_RUN_TIME_KEY) != null)
{
int secondsBack = Integer.parseInt(runBackendStepInput.getValueString(SECONDS_TO_SUBTRACT_FROM_LAST_RUN_TIME_KEY));
updatedRunTime = lastRunTime.minusSeconds(secondsBack);
}
return (updatedRunTime.toString());
}
@ -148,14 +162,24 @@ public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep implements
*******************************************************************************/
protected String getThisRunTimeString(RunBackendStepInput runBackendStepInput) throws QException
{
Instant thisRunTime = runBackendStepInput.getValueInstant(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY);
Instant thisRunTime = runBackendStepInput.getValueInstant(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY);
Instant updatedRunTime = thisRunTime;
Serializable basepullConfigurationValue = runBackendStepInput.getValue(RunProcessAction.BASEPULL_CONFIGURATION);
if(basepullConfigurationValue instanceof BasepullConfiguration basepullConfiguration && basepullConfiguration.getSecondsToSubtractFromThisRunTimeForTimestampQuery() != null)
{
thisRunTime = thisRunTime.minusSeconds(basepullConfiguration.getSecondsToSubtractFromThisRunTimeForTimestampQuery());
updatedRunTime = thisRunTime.minusSeconds(basepullConfiguration.getSecondsToSubtractFromThisRunTimeForTimestampQuery());
}
return (thisRunTime.toString());
//////////////////////////////////////////////////////////////
// if an override was found in the params, use that instead //
//////////////////////////////////////////////////////////////
if(runBackendStepInput.getValueString(SECONDS_TO_SUBTRACT_FROM_THIS_RUN_TIME_KEY) != null)
{
int secondsBack = Integer.parseInt(runBackendStepInput.getValueString(SECONDS_TO_SUBTRACT_FROM_THIS_RUN_TIME_KEY));
updatedRunTime = thisRunTime.minusSeconds(secondsBack);
}
return (updatedRunTime.toString());
}
}

View File

@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.processes.implementations.tablesync;
import java.io.Serializable;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -35,6 +38,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -53,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.general.StandardProcessSummaryLineProducer;
@ -72,33 +77,33 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
*******************************************************************************/
public abstract class AbstractTableSyncTransformStep extends AbstractTransformStep
{
private static final QLogger LOG = QLogger.getLogger(AbstractTableSyncTransformStep.class);
protected static final QLogger LOG = QLogger.getLogger(AbstractTableSyncTransformStep.class);
private ProcessSummaryLine okToInsert = StandardProcessSummaryLineProducer.getOkToInsertLine();
private ProcessSummaryLine okToUpdate = StandardProcessSummaryLineProducer.getOkToUpdateLine();
protected ProcessSummaryLine okToInsert = StandardProcessSummaryLineProducer.getOkToInsertLine();
protected ProcessSummaryLine okToUpdate = StandardProcessSummaryLineProducer.getOkToUpdateLine();
private ProcessSummaryLine willNotInsert = new ProcessSummaryLine(Status.INFO)
protected ProcessSummaryLine willNotInsert = new ProcessSummaryLine(Status.INFO)
.withMessageSuffix("because this process is not configured to insert records.")
.withSingularFutureMessage("will not be inserted ")
.withPluralFutureMessage("will not be inserted ")
.withSingularPastMessage("was not inserted ")
.withPluralPastMessage("were not inserted ");
private ProcessSummaryLine willNotUpdate = new ProcessSummaryLine(Status.INFO)
protected ProcessSummaryLine willNotUpdate = new ProcessSummaryLine(Status.INFO)
.withMessageSuffix("because this process is not configured to update records.")
.withSingularFutureMessage("will not be updated ")
.withPluralFutureMessage("will not be updated ")
.withSingularPastMessage("was not updated ")
.withPluralPastMessage("were not updated ");
private ProcessSummaryLine errorMissingKeyField = new ProcessSummaryLine(Status.ERROR)
protected ProcessSummaryLine errorMissingKeyField = new ProcessSummaryLine(Status.ERROR)
.withMessageSuffix("missing a value for the key field.")
.withSingularFutureMessage("will not be synced, because it is ")
.withPluralFutureMessage("will not be synced, because they are ")
.withSingularPastMessage("was not synced, because it is ")
.withPluralPastMessage("were not synced, because they are ");
private ProcessSummaryLine unspecifiedError = new ProcessSummaryLine(Status.ERROR)
protected ProcessSummaryLine unspecifiedError = new ProcessSummaryLine(Status.ERROR)
.withMessageSuffix("of an unexpected error: ")
.withSingularFutureMessage("will not be synced, ")
.withPluralFutureMessage("will not be synced, ")
@ -109,7 +114,11 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
protected RunBackendStepOutput runBackendStepOutput = null;
protected RecordLookupHelper recordLookupHelper = null;
private QPossibleValueTranslator possibleValueTranslator;
protected QPossibleValueTranslator possibleValueTranslator;
protected static final String SYNC_TABLE_PERFORM_INSERTS_KEY = "syncTablePerformInsertsKey";
protected static final String SYNC_TABLE_PERFORM_UPDATES_KEY = "syncTablePerformUpdatesKey";
protected static final String LOG_TRANSFORM_RESULTS = "logTransformResults";
@ -214,6 +223,7 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{
LOG.info("No input records were found.");
return;
}
@ -222,6 +232,19 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
SyncProcessConfig config = getSyncProcessConfig();
////////////////////////////////////////////////////////////
// see if these fields have been updated via input fields //
////////////////////////////////////////////////////////////
if(runBackendStepInput.getValueString(SYNC_TABLE_PERFORM_INSERTS_KEY) != null)
{
boolean performInserts = Boolean.parseBoolean(runBackendStepInput.getValueString(SYNC_TABLE_PERFORM_INSERTS_KEY));
config = new SyncProcessConfig(config.sourceTable, config.sourceTableKeyField, config.destinationTable, config.destinationTableForeignKey, performInserts, config.performUpdates);
}
if(runBackendStepInput.getValueString(SYNC_TABLE_PERFORM_UPDATES_KEY) != null)
{
boolean performUpdates = Boolean.parseBoolean(runBackendStepInput.getValueString(SYNC_TABLE_PERFORM_UPDATES_KEY));
config = new SyncProcessConfig(config.sourceTable, config.sourceTableKeyField, config.destinationTable, config.destinationTableForeignKey, config.performUpdates, performUpdates);
}
String sourceTableKeyField = config.sourceTableKeyField;
String destinationTableForeignKeyField = config.destinationTableForeignKey;
String destinationTableName = config.destinationTable;
@ -371,9 +394,63 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
possibleValueTranslator.translatePossibleValuesInRecords(QContext.getQInstance().getTable(destinationTableName), runBackendStepOutput.getRecords());
}
}
if(Boolean.parseBoolean(runBackendStepInput.getValueString(LOG_TRANSFORM_RESULTS)))
{
logResults(runBackendStepInput, config);
}
}
/*******************************************************************************
** Log results of transformation
**
*******************************************************************************/
protected void logResults(RunBackendStepInput runBackendStepInput, SyncProcessConfig syncProcessConfig)
{
String timezone = QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE);
if(timezone == null)
{
timezone = QContext.getQInstance().getDefaultTimeZoneId();
}
Instant lastRunTime = Instant.now();
if(runBackendStepInput.getBasepullLastRunTime() != null)
{
lastRunTime = runBackendStepInput.getBasepullLastRunTime();
}
ZonedDateTime dateTime = lastRunTime.atZone(ZoneId.of(timezone));
if(syncProcessConfig.performInserts)
{
if(okToInsert.getCount() == 0)
{
LOG.info("No Records were found to insert since " + QValueFormatter.formatDateTimeWithZone(dateTime) + ".");
}
else
{
String pluralized = okToInsert.getCount() > 1 ? " Records were " : " Record was ";
LOG.info(okToInsert.getCount() + pluralized + " found to insert since " + QValueFormatter.formatDateTimeWithZone(dateTime) + ".", logPair("primaryKeys", okToInsert.getPrimaryKeys()));
}
}
if(syncProcessConfig.performUpdates)
{
if(okToUpdate.getCount() == 0)
{
LOG.info("No Records were found to update since " + QValueFormatter.formatDateTimeWithZone(dateTime) + ".");
}
else
{
String pluralized = okToUpdate.getCount() > 1 ? " Records were " : " Record was ";
LOG.info(okToUpdate.getCount() + pluralized + " found to update since " + QValueFormatter.formatDateTimeWithZone(dateTime) + ".", logPair("primaryKeys", okToInsert.getPrimaryKeys()));
}
}
}
/*******************************************************************************
** Given a source record, extract what we'll use as its key from it.
**

View File

@ -30,6 +30,8 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
@ -40,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -83,6 +86,7 @@ class AuditActionTest extends BaseTest
**
*******************************************************************************/
@Test
@Disabled("this behavior has been changed to just log... should this be a setting?")
void testFailWithoutSecurityKey() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
@ -117,6 +121,50 @@ class AuditActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLogWithoutSecurityKey() throws QException
{
int recordId = 1701;
QInstance qInstance = TestUtils.defineInstance();
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
String userName = "John Doe";
QContext.init(qInstance, new QSession().withUser(new QUser().withFullName(userName)));
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(AuditAction.class);
AuditAction.execute(TestUtils.TABLE_NAME_ORDER, recordId, Map.of(), "Test Audit");
///////////////////////////////////////////////////////////////////
// it should not throw, but it should also not insert the audit. //
///////////////////////////////////////////////////////////////////
Optional<QRecord> auditRecord = GeneralProcessUtils.getRecordByField("audit", "recordId", recordId);
assertTrue(auditRecord.isPresent());
assertThat(collectingLogger.getCollectedMessages()).anyMatch(m -> m.getMessage().contains("Missing securityKeyValue in audit request"));
collectingLogger.clear();
////////////////////////////////////////////////////////////////////////////////////////////////
// try again with a null value in the key - that should be ok - as at least you were thinking //
// about the key and put in SOME value (null has its own semantics in security keys) //
////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> securityKeys = new HashMap<>();
securityKeys.put(TestUtils.SECURITY_KEY_TYPE_STORE, null);
AuditAction.execute(TestUtils.TABLE_NAME_ORDER, recordId, securityKeys, "Test Audit");
/////////////////////////////////////
// now the audit should be stored. //
/////////////////////////////////////
auditRecord = GeneralProcessUtils.getRecordByField("audit", "recordId", recordId);
assertTrue(auditRecord.isPresent());
assertThat(collectingLogger.getCollectedMessages()).noneMatch(m -> m.getMessage().contains("Missing securityKeyValue in audit request"));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -748,7 +748,7 @@ public class BaseAPIActionUtil
{
try
{
String uri = request.getURI().toString();
String uri = request.getURI().toString();
String pair = backendMetaData.getApiKeyQueryParamName() + "=" + getApiKey();
///////////////////////////////////////////////////////////////////////////////////
@ -1167,6 +1167,16 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected void logRequestDetails(QTableMetaData table, HttpRequestBase request) throws QException
{
LOG.info("Making [" + request.getMethod() + "] request to URL [" + request.getURI() + "] on table [" + table.getName() + "].");
}
/*******************************************************************************
**
*******************************************************************************/
@ -1192,7 +1202,7 @@ public class BaseAPIActionUtil
setupContentTypeInRequest(request);
setupAdditionalHeaders(request);
LOG.info("Making [" + request.getMethod() + "] request to URL [" + request.getURI() + "] on table [" + table.getName() + "].");
logRequestDetails(table, request);
if("POST".equals(request.getMethod()))
{
LOG.info("POST contents [" + ((HttpPost) request).getEntity().toString() + "]");

View File

@ -212,7 +212,7 @@ public class RDBMSCountActionTest extends RDBMSActionTest
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(1);
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(4);
}
}

View File

@ -771,6 +771,61 @@ public class RDBMSQueryActionJoinsTest extends RDBMSActionTest
/*******************************************************************************
** Error seen in CTLive - query for a record in a sub-table, but whose security
** key comes from a main table, but the main-table record doesn't exist.
**
** In this QInstance, our warehouse table's security key comes from
** storeWarehouseInt.storeId - so if we insert a warehouse, but no stores, we
** might not be able to find it (if this bug exists!)
*******************************************************************************/
@Test
void testRequestedJoinWithTableWhoseSecurityFieldIsInMainTableAndNoRowIsInMainTable() throws Exception
{
runTestSql("INSERT INTO warehouse (name) VALUES ('Springfield')", null);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Springfield")));
/////////////////////////////////////////
// with all access key, should find it //
/////////////////////////////////////////
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(1);
////////////////////////////////////////////
// with a regular key, should not find it //
////////////////////////////////////////////
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(0);
/////////////////////////////////////////
// now assign the warehouse to a store //
/////////////////////////////////////////
runTestSql("INSERT INTO warehouse_store_int (store_id, warehouse_id) SELECT 1, id FROM warehouse WHERE name='Springfield'", null);
/////////////////////////////////////////
// with all access key, should find it //
/////////////////////////////////////////
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(1);
///////////////////////////////////////////////////////
// with a regular key, should find it if key matches //
///////////////////////////////////////////////////////
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(1);
//////////////////////////////////////////////////////////////////
// with a regular key, should not find it if key does not match //
//////////////////////////////////////////////////////////////////
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(0);
}
/*******************************************************************************
**
*******************************************************************************/
@ -888,7 +943,10 @@ public class RDBMSQueryActionJoinsTest extends RDBMSActionTest
/*******************************************************************************
**
** Note, this test was originally written asserting size=1... but reading
** the data, for an all-access key, that seems wrong - as the user should see
** all the records in this table, not just ones associated with a store...
** so, switching to 4 (same issue in CountActionTest too).
*******************************************************************************/
@Test
void testRecordSecurityWithLockFromJoinTableWhereTheKeyIsOnTheManySide() throws QException
@ -897,8 +955,9 @@ public class RDBMSQueryActionJoinsTest extends RDBMSActionTest
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(1);
List<QRecord> records = new QueryAction().execute(queryInput).getRecords();
assertThat(records)
.hasSize(4);
}

View File

@ -1286,6 +1286,11 @@ public class QJavalinImplementation
queryInput.getFilter().setLimit(limit);
}
if(queryInput.getFilter() == null || queryInput.getFilter().getLimit() == null)
{
handleQueryNullLimit(context, queryInput);
}
List<QueryJoin> queryJoins = processQueryJoinsParam(context);
queryInput.setQueryJoins(queryJoins);
@ -1306,6 +1311,28 @@ public class QJavalinImplementation
/***************************************************************************
**
***************************************************************************/
private static void handleQueryNullLimit(Context context, QueryInput queryInput)
{
boolean allowed = javalinMetaData.getQueryWithoutLimitAllowed();
if(!allowed)
{
if(queryInput.getFilter() == null)
{
queryInput.setFilter(new QQueryFilter());
}
queryInput.getFilter().setLimit(javalinMetaData.getQueryWithoutLimitDefault());
LOG.log(javalinMetaData.getQueryWithoutLimitLogLevel(), "Query request did not specify a limit, which is not allowed. Using default instead", null,
logPair("defaultLimit", javalinMetaData.getQueryWithoutLimitDefault()),
logPair("path", context.path()));
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.javalin;
import java.util.function.Function;
import org.apache.logging.log4j.Level;
/*******************************************************************************
@ -36,6 +37,10 @@ public class QJavalinMetaData
private Function<QJavalinAccessLogger.LogEntry, Boolean> logFilter;
private boolean queryWithoutLimitAllowed = false;
private Integer queryWithoutLimitDefault = 1000;
private Level queryWithoutLimitLogLevel = Level.INFO;
/*******************************************************************************
@ -143,4 +148,97 @@ public class QJavalinMetaData
return (this);
}
/*******************************************************************************
** Getter for queryWithoutLimitAllowed
*******************************************************************************/
public boolean getQueryWithoutLimitAllowed()
{
return (this.queryWithoutLimitAllowed);
}
/*******************************************************************************
** Setter for queryWithoutLimitAllowed
*******************************************************************************/
public void setQueryWithoutLimitAllowed(boolean queryWithoutLimitAllowed)
{
this.queryWithoutLimitAllowed = queryWithoutLimitAllowed;
}
/*******************************************************************************
** Fluent setter for queryWithoutLimitAllowed
*******************************************************************************/
public QJavalinMetaData withQueryWithoutLimitAllowed(boolean queryWithoutLimitAllowed)
{
this.queryWithoutLimitAllowed = queryWithoutLimitAllowed;
return (this);
}
/*******************************************************************************
** Getter for queryWithoutLimitDefault
*******************************************************************************/
public Integer getQueryWithoutLimitDefault()
{
return (this.queryWithoutLimitDefault);
}
/*******************************************************************************
** Setter for queryWithoutLimitDefault
*******************************************************************************/
public void setQueryWithoutLimitDefault(Integer queryWithoutLimitDefault)
{
this.queryWithoutLimitDefault = queryWithoutLimitDefault;
}
/*******************************************************************************
** Fluent setter for queryWithoutLimitDefault
*******************************************************************************/
public QJavalinMetaData withQueryWithoutLimitDefault(Integer queryWithoutLimitDefault)
{
this.queryWithoutLimitDefault = queryWithoutLimitDefault;
return (this);
}
/*******************************************************************************
** Getter for queryWithoutLimitLogLevel
*******************************************************************************/
public Level getQueryWithoutLimitLogLevel()
{
return (this.queryWithoutLimitLogLevel);
}
/*******************************************************************************
** Setter for queryWithoutLimitLogLevel
*******************************************************************************/
public void setQueryWithoutLimitLogLevel(Level queryWithoutLimitLogLevel)
{
this.queryWithoutLimitLogLevel = queryWithoutLimitLogLevel;
}
/*******************************************************************************
** Fluent setter for queryWithoutLimitLogLevel
*******************************************************************************/
public QJavalinMetaData withQueryWithoutLimitLogLevel(Level queryWithoutLimitLogLevel)
{
this.queryWithoutLimitLogLevel = queryWithoutLimitLogLevel;
return (this);
}
}

View File

@ -33,6 +33,8 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
@ -45,6 +47,7 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.apache.logging.log4j.Level;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONObject;
@ -469,6 +472,101 @@ class QJavalinImplementationTest extends QJavalinTestBase
/*******************************************************************************
** test a table query using an actual filter via POST, with no limit specified,
** and with that not being allowed.
**
*******************************************************************************/
@Test
public void test_dataQueryWithFilterPOSTWithoutLimitNotAllowed() throws QInstanceValidationException
{
try
{
qJavalinImplementation.getJavalinMetaData()
.withQueryWithoutLimitAllowed(false)
.withQueryWithoutLimitDefault(3)
.withQueryWithoutLimitLogLevel(Level.WARN);
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(QJavalinImplementation.class);
String filterJson = """
{"criteria":[]}""";
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/person/query")
.field("filter", filterJson)
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertTrue(jsonObject.has("records"));
JSONArray records = jsonObject.getJSONArray("records");
assertEquals(3, records.length());
assertThat(collectingLogger.getCollectedMessages())
.anyMatch(m -> m.getLevel().equals(Level.WARN) && m.getMessage().contains("Query request did not specify a limit"));
}
finally
{
QLogger.activateCollectingLoggerForClass(QJavalinImplementation.class);
resetMetaDataQueryWithoutLimitSettings();
}
}
/*******************************************************************************
** test a table query using an actual filter via POST, with no limit specified,
** but with that being allowed.
**
*******************************************************************************/
@Test
public void test_dataQueryWithFilterPOSTWithoutLimitAllowed() throws QInstanceValidationException
{
try
{
qJavalinImplementation.getJavalinMetaData()
.withQueryWithoutLimitAllowed(true);
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(QJavalinImplementation.class);
String filterJson = """
{"criteria":[]}""";
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/person/query")
.field("filter", filterJson)
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertTrue(jsonObject.has("records"));
JSONArray records = jsonObject.getJSONArray("records");
assertEquals(6, records.length());
assertThat(collectingLogger.getCollectedMessages())
.noneMatch(m -> m.getMessage().contains("Query request did not specify a limit"));
}
finally
{
QLogger.activateCollectingLoggerForClass(QJavalinImplementation.class);
resetMetaDataQueryWithoutLimitSettings();
}
}
/***************************************************************************
**
***************************************************************************/
private void resetMetaDataQueryWithoutLimitSettings()
{
qJavalinImplementation.getJavalinMetaData()
.withQueryWithoutLimitAllowed(false)
.withQueryWithoutLimitDefault(1000)
.withQueryWithoutLimitLogLevel(Level.INFO);
}
/*******************************************************************************
**
*******************************************************************************/
@ -1029,7 +1127,7 @@ class QJavalinImplementationTest extends QJavalinTestBase
// filter use-case, with no values, should return options. //
/////////////////////////////////////////////////////////////
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/pet/possibleValues/ownerPersonId?searchTerm=&useCase=filter").asString();
JSONArray options = assertPossibleValueSuccessfulResponseAndGetOptionsArray(response);
JSONArray options = assertPossibleValueSuccessfulResponseAndGetOptionsArray(response);
assertNotNull(options);
assertThat(options.length()).isGreaterThanOrEqualTo(5);

View File

@ -102,7 +102,7 @@ public class QJavalinTestBase
{
qJavalinImplementation.stopJavalinServer();
}
qJavalinImplementation = new QJavalinImplementation(qInstance);
qJavalinImplementation = new QJavalinImplementation(qInstance, new QJavalinMetaData());
QJavalinProcessHandler.setAsyncStepTimeoutMillis(250);
qJavalinImplementation.startJavalinServer(PORT);
}