Compare commits

...

39 Commits

Author SHA1 Message Date
3639702acc CE-1328 Add job status messages 2024-11-13 09:18:37 -06:00
1615aea10c CE-1328 Initial checkin of generic GC process 2024-11-13 09:18:23 -06:00
efe89c7043 Merge pull request #137 from Kingsrook/feature/allow-basepull-override-values-from-jobs
hotfix: allow basepull override values from jobs
2024-10-11 09:27:24 -05:00
bbf4c2c2ff hotfix: allow basepull override values from jobs 2024-10-10 18:12:45 -05:00
ff1e022798 CE-1836: wasn't properly using boolean values in backend step input 2024-10-10 11:31:43 -05:00
f09735c811 Merged feature/CE-1836-create-order-checkers into dev 2024-10-10 10:56:30 -05:00
7ab9171998 Merged feature/CE-1821-veryify-shipped-orders-process into dev 2024-10-10 10:56:07 -05:00
b979f413c8 Merged feature/CE-1654-warehouse-security-key-all-access-left-join into dev 2024-10-10 10:53:25 -05:00
766881dee0 CE-1836: fixed npe if last basepull runtime hadnt been set 2024-10-10 09:59:20 -05:00
f65b16df60 Merge pull request #136 from Kingsrook/feature/CE-1836-create-order-checkers
Feature/ce 1836 create order checkers
2024-10-09 14:59:54 -05:00
e0597827ef CE-1836: updates from code review 2024-10-09 10:30:49 -05:00
10014f16ae CE-1836: fixed to check as boolean 2024-10-08 16:20:23 -05:00
526ba6ca30 CE-1836: added potential to log output 2024-10-08 15:46:47 -05:00
4f92fb2ae2 CE-1836: updates to allow getting basepull key value and sync config perform insert/updates from input 2024-10-07 22:33:16 -05:00
b687d07e46 CE-1836: update abstract table sync to make members and functions protected 2024-10-04 12:24:58 -05:00
b955a20e18 CE-1654 - Checkstyle! 2024-10-02 16:22:41 -05:00
eb8781db77 CE-1654 - Update joins built for security-purposes, that if they're for an all-access key, to be outer (LEFT); update tests to reflect this 2024-10-02 16:16:16 -05:00
febda51233 CE-1821: added static utility method for returning a list of entities rather than records 2024-09-26 15:16:23 -05:00
791b77b938 Merged feature/CE-1654-warehouse-security-key into dev 2024-09-18 16:48:29 -05:00
e6864b89c1 Merged feature/javalin-query-default-limit into dev 2024-09-18 16:48:14 -05:00
c3171c335f Update to always impose a limit on queries (they were getting lost if there was a defaultQueryFilter passed in) 2024-09-17 16:41:41 -05:00
bb548b78d9 updates to allow override api utils to disable or alter request details 2024-09-10 17:28:05 -05:00
161591405b CE-1654 - do chicken-egg session before the OTHER call to finalizeCustomizeSession too... 2024-09-10 10:51:21 -05:00
3cc0cfd86c CE-1654 - Just log, don't throw, if missing a security key value (should this be a setting??) 2024-09-10 09:34:46 -05:00
9bf9825132 Option (turned on by default, controlled via javalin metadata) to not allow query requests without a limit 2024-09-05 18:33:37 -05:00
a7ca34ec92 CE-1546 Switch auditTable.id and auditUser.id back to INTEGER (one isn't expected to have 2,000,000,000 of those) - fixes possible-value lookups 2024-09-05 14:17:45 -05:00
403227bae1 Merge tag 'version-0.22.1' into dev
Tag release
2024-09-05 13:40:57 -05:00
ab4837ff16 Merge branch 'rel/0.22.1' 2024-09-05 13:38:04 -05:00
107acb5685 Update for next development version 2024-09-05 13:28:56 -05:00
65166150e6 Update versions for release 2024-09-05 13:28:54 -05:00
c678a8159e Merged feature/CE-1546-support-migrating-audit-detail-to-big-int into dev 2024-09-05 13:17:40 -05:00
6673a8fc47 Updating to 0.23.0 2024-09-05 08:45:49 -05:00
c4f4faf32b Merge tag 'version-0.22.0' into dev
Tag release
2024-09-05 08:45:45 -05:00
4349b37c8d Update for next development version 2024-09-05 07:56:20 -05:00
f457fd0860 CE-1654 activate chickenAndEggSession while calling customizer.finalCustomizeSession 2024-09-03 22:01:07 -05:00
c3834efad3 CE-1546 - fixing the use long for id in test 2024-08-27 13:05:24 -05:00
d513c8431b CE-1546 - fixing the use long for id in test 2024-08-27 10:01:34 -05:00
fc4e69f059 CE-1546 - feedback from code review 2024-08-26 12:14:01 -05:00
20a5130757 CE-1546 - Moving audit ids to longs and adding general support for long ids 2024-08-21 09:35:33 -05:00
25 changed files with 1241 additions and 87 deletions

View File

@ -46,7 +46,7 @@
</modules> </modules>
<properties> <properties>
<revision>0.22.0</revision> <revision>0.23.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -225,7 +225,13 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
{ {
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType())) 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()));
} }
} }
@ -272,7 +278,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
List<QRecord> auditDetailRecords = new ArrayList<>(); List<QRecord> auditDetailRecords = new ArrayList<>();
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList())) for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
{ {
Integer auditId = insertOutput.getRecords().get(i++).getValueInteger("id"); Long auditId = insertOutput.getRecords().get(i++).getValueLong("id");
if(auditId == null) if(auditId == null)
{ {
LOG.warn("Missing an id for inserted audit - so won't be able to store its child details..."); LOG.warn("Missing an id for inserted audit - so won't be able to store its child details...");

View File

@ -79,6 +79,7 @@ public class RunProcessAction
{ {
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class); 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_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey"; public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField"; public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
@ -517,9 +518,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(); 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 // // if process specifies that it uses variants, look for that data in the session and append to our basepull key //
@ -551,7 +556,7 @@ public class RunProcessAction
String basepullTableName = basepullConfiguration.getTableName(); String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField(); String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName(); String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration); String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
/////////////////////////////////////// ///////////////////////////////////////
// get the stored basepull timestamp // // get the stored basepull timestamp //
@ -631,7 +636,7 @@ public class RunProcessAction
String basepullKeyFieldName = basepullConfiguration.getKeyField(); String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName(); String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp(); Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration); String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
/////////////////////////////////////// ///////////////////////////////////////
// get the stored basepull timestamp // // 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.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; 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.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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 ** 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. ** 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 // // 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(); queryFilter = input.getDefaultQueryFilter();
} }
// todo - skip & limit as params
queryFilter.setLimit(250);
queryFilter.setOrderBys(possibleValueSource.getOrderByFields()); queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
queryInput.setFilter(queryFilter); 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 // // 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; private Level logLevelForFilter = Level.OFF;
@ -404,6 +404,12 @@ public class JoinsContext
chainIsInner = false; chainIsInner = false;
} }
if(hasAllAccessKey(recordSecurityLock))
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- "); addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
addedQueryJoins.add(queryJoin); addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getRightTable()); tmpTable = instance.getTable(join.getRightTable());
@ -423,6 +429,12 @@ public class JoinsContext
chainIsInner = false; chainIsInner = false;
} }
if(hasAllAccessKey(recordSecurityLock))
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- "); addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
addedQueryJoins.add(queryJoin); addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getLeftTable()); 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) private void addSubFilterForRecordSecurityLock(RecordSecurityLock recordSecurityLock, QTableMetaData table, String tableNameOrAlias, boolean isOuter, QueryJoin sourceQueryJoin)
{ {
QSession session = QContext.getQSession(); boolean haveAllAccessKey = hasAllAccessKey(recordSecurityLock);
if(haveAllAccessKey)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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()))
{ {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(sourceQueryJoin != null)
// 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))
{ {
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) ////////////////////////////////////////////////////////////////////////////////////////
{ // if we're in an AND filter, then we don't need a criteria for this lock, so return. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////
// in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it // boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
// this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough // if(inAnAndFilter)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// {
sourceQueryJoin.withSecurityCriteria(new ArrayList<>()); 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 else
{ {
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type); List<Serializable> securityKeyValues = QContext.getQSession().getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues)) if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -220,7 +220,7 @@ public class AuditsMetaDataProvider
.withRecordLabelFormat("%s %s") .withRecordLabelFormat("%s %s")
.withRecordLabelFields("auditTableId", "recordId") .withRecordLabelFields("auditTableId", "recordId")
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("id", QFieldType.LONG))
.withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE)) .withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE))
.withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER)) .withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER))
.withField(new QFieldMetaData("recordId", QFieldType.INTEGER)) .withField(new QFieldMetaData("recordId", QFieldType.INTEGER))
@ -243,8 +243,8 @@ public class AuditsMetaDataProvider
.withRecordLabelFormat("%s") .withRecordLabelFormat("%s")
.withRecordLabelFields("id") .withRecordLabelFields("id")
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("id", QFieldType.LONG))
.withField(new QFieldMetaData("auditId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT)) .withField(new QFieldMetaData("auditId", QFieldType.LONG).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS)) .withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("fieldName", QFieldType.STRING).withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS)) .withField(new QFieldMetaData("fieldName", QFieldType.STRING).withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("oldValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS)) .withField(new QFieldMetaData("oldValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))

View File

@ -247,10 +247,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired // // allow customizer to do custom things here, if so desired //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
if(getCustomizer() != null) finalCustomizeSession(qInstance, qSession);
{
getCustomizer().finalCustomizeSession(qInstance, qSession);
}
return (qSession); return (qSession);
} }
@ -311,10 +308,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired // // allow customizer to do custom things here, if so desired //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
if(getCustomizer() != null) finalCustomizeSession(qInstance, qSession);
{
getCustomizer().finalCustomizeSession(qInstance, qSession);
}
return (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 ** Insert a session as a new record into userSession table
*******************************************************************************/ *******************************************************************************/

View File

@ -375,7 +375,14 @@ public class MemoryRecordStore
///////////////////////////////////////////////// /////////////////////////////////////////////////
if(recordToInsert.getValue(primaryKeyField.getName()) == null && (primaryKeyField.getType().equals(QFieldType.INTEGER) || primaryKeyField.getType().equals(QFieldType.LONG))) if(recordToInsert.getValue(primaryKeyField.getName()) == null && (primaryKeyField.getType().equals(QFieldType.INTEGER) || primaryKeyField.getType().equals(QFieldType.LONG)))
{ {
recordToInsert.setValue(primaryKeyField.getName(), nextSerial++); if(primaryKeyField.getType().equals(QFieldType.LONG))
{
recordToInsert.setValue(primaryKeyField.getName(), (nextSerial++).longValue());
}
else
{
recordToInsert.setValue(primaryKeyField.getName(), nextSerial++);
}
} }
/////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////
@ -385,7 +392,7 @@ public class MemoryRecordStore
{ {
nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1; nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1;
} }
else if(primaryKeyField.getType().equals(QFieldType.LONG) && recordToInsert.getValueLong(primaryKeyField.getName()) > nextSerial) else if(primaryKeyField.getType().equals(QFieldType.LONG) && recordToInsert.getValueInteger(primaryKeyField.getName()) > nextSerial)
{ {
////////////////////////////////////// //////////////////////////////////////
// todo - mmm, could overflow here? // // todo - mmm, could overflow here? //

View File

@ -40,6 +40,10 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
*******************************************************************************/ *******************************************************************************/
public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep implements BasepullExtractStepInterface 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 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. // // 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); Serializable basepullConfigurationValue = runBackendStepInput.getValue(RunProcessAction.BASEPULL_CONFIGURATION);
if(basepullConfigurationValue instanceof BasepullConfiguration basepullConfiguration && basepullConfiguration.getSecondsToSubtractFromLastRunTimeForTimestampQuery() != null) 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 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); Serializable basepullConfigurationValue = runBackendStepInput.getValue(RunProcessAction.BASEPULL_CONFIGURATION);
if(basepullConfigurationValue instanceof BasepullConfiguration basepullConfiguration && basepullConfiguration.getSecondsToSubtractFromThisRunTimeForTimestampQuery() != null) 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

@ -0,0 +1,287 @@
/*
* 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.processes.implementations.garbagecollector;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
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.Status;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class GenericGarbageCollectorExecuteStep implements BackendStep
{
private static final QLogger LOG = QLogger.getLogger(GenericGarbageCollectorExecuteStep.class);
private ProcessSummaryLine partitionsLine = new ProcessSummaryLine(Status.INFO)
.withSingularPastMessage("partition was processed")
.withPluralPastMessage("partitions were processed");
private ProcessSummaryLine deletedLine = new ProcessSummaryLine(Status.OK)
.withSingularPastMessage("record was deleted")
.withPluralPastMessage("records were deleted");
private ProcessSummaryLine warningLine = new ProcessSummaryLine(Status.WARNING, "had an warning");
private ProcessSummaryLine errorLine = new ProcessSummaryLine(Status.ERROR, "had an error");
private AsyncJobCallback asyncJobCallback;
private Integer total = null;
private Integer deletedSoFar = 0;
/***************************************************************************
**
***************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
String tableName = runBackendStepInput.getValueString("table");
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
throw new QUserFacingException("Unrecognized table: " + tableName);
}
String fieldName = runBackendStepInput.getValueString("field");
QFieldMetaData field = table.getFields().get(fieldName);
if(field == null)
{
throw new QUserFacingException("Unrecognized field: " + fieldName);
}
if(!QFieldType.DATE_TIME.equals(field.getType()) && !QFieldType.DATE.equals(field.getType()))
{
throw new QUserFacingException("Field " + field + " is not a date-time or date type field.");
}
Integer daysBack = runBackendStepInput.getValueInteger("daysBack");
if(daysBack == null || daysBack < 0)
{
throw new QUserFacingException("Illegal value for daysBack: " + daysBack + "; Must be positive.");
}
Integer maxPageSize = runBackendStepInput.getValueInteger("maxPageSize");
if(maxPageSize == null || maxPageSize < 0)
{
throw new QUserFacingException("Illegal value for maxPageSize: " + maxPageSize + "; Must be positive.");
}
asyncJobCallback = runBackendStepInput.getAsyncJobCallback();
long start = System.currentTimeMillis();
execute(table, field, daysBack, maxPageSize);
long end = System.currentTimeMillis();
deletedLine.prepareForFrontend(true);
partitionsLine.prepareForFrontend(true);
ArrayList<ProcessSummaryLineInterface> processSummary = new ArrayList<>();
processSummary.add(partitionsLine);
processSummary.add(deletedLine);
warningLine.addSelfToListIfAnyCount(processSummary);
errorLine.addSelfToListIfAnyCount(processSummary);
processSummary.add(new ProcessSummaryLine(Status.INFO, "Total time: " + String.format("%,d", ((end - start) / 1000)) + " seconds"));
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, processSummary);
}
/***************************************************************************
**
***************************************************************************/
private void execute(QTableMetaData table, QFieldMetaData field, Integer daysBack, Integer maxPageSize) throws QException
{
asyncJobCallback.updateStatus("Counting records");
Instant maxDate = Instant.now().minusSeconds(daysBack * 60 * 60 * 24);
Instant minDate = findMinDateInTable(table, field);
processDateRange(table, field, maxPageSize, minDate, maxDate);
}
/***************************************************************************
**
***************************************************************************/
private void processDateRange(QTableMetaData table, QFieldMetaData field, Integer maxPageSize, Instant minDate, Instant maxDate) throws QException
{
partitionsLine.incrementCount();
LOG.info("Counting", logPair("table", table.getName()), logPair("field", field.getName()), logPair("minDate", minDate), logPair("maxDate", maxDate));
Integer count = count(table, field, minDate, maxDate);
LOG.info("Count", logPair("count", count), logPair("table", table.getName()), logPair("field", field.getName()), logPair("minDate", minDate), logPair("maxDate", maxDate));
if(this.total == null)
{
this.total = count;
}
if(count == 0)
{
LOG.info("0 rows in this partition - nothing to delete", logPair("count", count), logPair("table", table.getName()), logPair("field", field.getName()), logPair("minDate", minDate), logPair("maxDate", maxDate));
}
else if(count <= maxPageSize)
{
asyncJobCallback.updateStatus("Deleting records", deletedSoFar, total);
LOG.info("Deleting", logPair("count", count), logPair("table", table.getName()), logPair("field", field.getName()), logPair("minDate", minDate), logPair("maxDate", maxDate));
delete(table, field, minDate, maxDate);
this.deletedSoFar += count;
}
else
{
if(deletedSoFar == 0)
{
asyncJobCallback.updateStatus("Partitioning table", deletedSoFar, total);
}
LOG.info("Too many rows", logPair("count", count), logPair("maxPageSize", maxPageSize), logPair("table", table.getName()), logPair("field", field.getName()), logPair("minDate", minDate), logPair("maxDate", maxDate));
partition(table, field, minDate, maxDate, count, maxPageSize);
}
}
/***************************************************************************
**
***************************************************************************/
private void partition(QTableMetaData table, QFieldMetaData field, Instant minDate, Instant maxDate, Integer count, Integer maxPageSize) throws QException
{
int noOfPartitions = (int) Math.ceil((float) count / (float) maxPageSize);
long milliDiff = maxDate.toEpochMilli() - minDate.toEpochMilli();
long milliPerPartition = milliDiff / noOfPartitions;
if(milliPerPartition < 1000)
{
throw (new QUserFacingException("To find a maxPageSize under " + String.format("%,d", maxPageSize) + ", the partition size would become smaller than 1 second (between " + minDate + " and " + maxDate + " there are " + String.format("%,d", count) + " rows) - you must use a larger maxPageSize to continue."));
}
LOG.info("Partitioning", logPair("count", count), logPair("noOfPartitions", noOfPartitions), logPair("milliDiff", milliDiff), logPair("milliPerPartition", milliPerPartition), logPair("table", table.getName()), logPair("field", field.getName()), logPair("minDate", minDate), logPair("maxDate", maxDate));
for(int i = 0; i < noOfPartitions; i++)
{
maxDate = minDate.plusMillis(milliPerPartition);
processDateRange(table, field, maxPageSize, minDate, maxDate);
minDate = maxDate;
}
}
/***************************************************************************
**
***************************************************************************/
private void delete(QTableMetaData table, QFieldMetaData field, Instant minDate, Instant maxDate) throws QException
{
DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(table.getName())
.withQueryFilter(new QQueryFilter(new QFilterCriteria(field.getName(), QCriteriaOperator.BETWEEN, minDate, maxDate))));
deletedLine.incrementCount(deleteOutput.getDeletedRecordCount());
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
{
warningLine.incrementCount(deleteOutput.getRecordsWithWarnings().size());
}
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
{
errorLine.incrementCount(deleteOutput.getRecordsWithErrors().size());
}
}
/***************************************************************************
**
***************************************************************************/
private Integer count(QTableMetaData table, QFieldMetaData field, Instant minDate, Instant maxDate) throws QException
{
Aggregate countDateField = new Aggregate(field.getName(), AggregateOperator.COUNT).withFieldType(QFieldType.INTEGER);
AggregateInput aggregateInput = new AggregateInput();
aggregateInput.setTableName(table.getName());
aggregateInput.withFilter(new QQueryFilter(new QFilterCriteria(field.getName(), QCriteriaOperator.BETWEEN, minDate, maxDate)));
aggregateInput.withAggregate(countDateField);
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
List<AggregateResult> results = aggregateOutput.getResults();
if(CollectionUtils.nullSafeIsEmpty(results))
{
throw (new QUserFacingException("Could not count rows table (null or empty aggregate result)."));
}
return (ValueUtils.getValueAsInteger(results.get(0).getAggregateValue(countDateField)));
}
/***************************************************************************
**
***************************************************************************/
private Instant findMinDateInTable(QTableMetaData table, QFieldMetaData field) throws QException
{
Aggregate minDate = new Aggregate(field.getName(), AggregateOperator.MIN);
AggregateInput aggregateInput = new AggregateInput();
aggregateInput.setTableName(table.getName());
aggregateInput.withAggregate(minDate);
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
List<AggregateResult> results = aggregateOutput.getResults();
if(CollectionUtils.nullSafeIsEmpty(results))
{
throw (new QUserFacingException("Could not find min date value in table (null or empty aggregate result)."));
}
return (ValueUtils.getValueAsInstant(results.get(0).getAggregateValue(minDate)));
}
}

View File

@ -0,0 +1,88 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.garbagecollector;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
/*******************************************************************************
** Generic process that can perform garbage collection on any table (at least,
** any table with a date or date-time field).
**
** When running, this process prompts for:
** - table name
** - field name (e.g., the date/date-time field on that table)
** - daysBack - any records older than that many days ago will be deleted.
** - maxPageSize - to avoid running "1 huge query", if there are more than
** this number of records between the min-date in the table and the max-date
** (based on daysBack), then the time range is partitioned recursively until
** pages smaller than this parameter are found. The partitioning attempts to
** be smart (e.g., not just ÷ 2), by doing count / maxPageSize.
*******************************************************************************/
public class GenericGarbageCollectorProcessMetaDataProducer extends MetaDataProducer<QProcessMetaData>
{
public static final String NAME = "GenericGarbageCollector";
/*******************************************************************************
** See class header for param descriptions.
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
QProcessMetaData processMetaData = new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("auto_delete"))
.withStepList(List.of(
new QFrontendStepMetaData()
.withName("input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("table", QFieldType.STRING))
.withFormField(new QFieldMetaData("field", QFieldType.STRING))
.withFormField(new QFieldMetaData("daysBack", QFieldType.INTEGER).withDefaultValue(90))
.withFormField(new QFieldMetaData("maxPageSize", QFieldType.INTEGER).withDefaultValue(100000)),
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(GenericGarbageCollectorExecuteStep.class)),
new QFrontendStepMetaData()
.withName("result")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.PROCESS_SUMMARY_RESULTS))
));
return (processMetaData);
}
}

View File

@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.processes.implementations.tablesync;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -35,6 +38,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; 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.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; 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.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.general.StandardProcessSummaryLineProducer; 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 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(); protected ProcessSummaryLine okToInsert = StandardProcessSummaryLineProducer.getOkToInsertLine();
private ProcessSummaryLine okToUpdate = StandardProcessSummaryLineProducer.getOkToUpdateLine(); 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.") .withMessageSuffix("because this process is not configured to insert records.")
.withSingularFutureMessage("will not be inserted ") .withSingularFutureMessage("will not be inserted ")
.withPluralFutureMessage("will not be inserted ") .withPluralFutureMessage("will not be inserted ")
.withSingularPastMessage("was not inserted ") .withSingularPastMessage("was not inserted ")
.withPluralPastMessage("were 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.") .withMessageSuffix("because this process is not configured to update records.")
.withSingularFutureMessage("will not be updated ") .withSingularFutureMessage("will not be updated ")
.withPluralFutureMessage("will not be updated ") .withPluralFutureMessage("will not be updated ")
.withSingularPastMessage("was not updated ") .withSingularPastMessage("was not updated ")
.withPluralPastMessage("were 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.") .withMessageSuffix("missing a value for the key field.")
.withSingularFutureMessage("will not be synced, because it is ") .withSingularFutureMessage("will not be synced, because it is ")
.withPluralFutureMessage("will not be synced, because they are ") .withPluralFutureMessage("will not be synced, because they are ")
.withSingularPastMessage("was not synced, because it is ") .withSingularPastMessage("was not synced, because it is ")
.withPluralPastMessage("were not synced, because they are "); .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: ") .withMessageSuffix("of an unexpected error: ")
.withSingularFutureMessage("will not be synced, ") .withSingularFutureMessage("will not be synced, ")
.withPluralFutureMessage("will not be synced, ") .withPluralFutureMessage("will not be synced, ")
@ -109,7 +114,11 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
protected RunBackendStepOutput runBackendStepOutput = null; protected RunBackendStepOutput runBackendStepOutput = null;
protected RecordLookupHelper recordLookupHelper = 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())) if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{ {
LOG.info("No input records were found.");
return; return;
} }
@ -222,6 +232,19 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
SyncProcessConfig config = getSyncProcessConfig(); 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 sourceTableKeyField = config.sourceTableKeyField;
String destinationTableForeignKeyField = config.destinationTableForeignKey; String destinationTableForeignKeyField = config.destinationTableForeignKey;
String destinationTableName = config.destinationTable; String destinationTableName = config.destinationTable;
@ -371,9 +394,63 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
possibleValueTranslator.translatePossibleValuesInRecords(QContext.getQInstance().getTable(destinationTableName), runBackendStepOutput.getRecords()); 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. ** 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.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.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.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput; import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider; 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.processes.utils.GeneralProcessUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -83,6 +86,7 @@ class AuditActionTest extends BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
@Disabled("this behavior has been changed to just log... should this be a setting?")
void testFailWithoutSecurityKey() throws QException void testFailWithoutSecurityKey() throws QException
{ {
QInstance qInstance = TestUtils.defineInstance(); 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"));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -180,7 +228,7 @@ class AuditActionTest extends BaseTest
QRecord auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow("audit", "recordId", recordId1); QRecord auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow("audit", "recordId", recordId1);
assertEquals("Test Audit", auditRecord.getValueString("message")); assertEquals("Test Audit", auditRecord.getValueString("message"));
List<QRecord> auditDetails = GeneralProcessUtils.getRecordListByField("auditDetail", "auditId", auditRecord.getValue("id")); List<QRecord> auditDetails = GeneralProcessUtils.getRecordListByField("auditDetail", "auditId", auditRecord.getValueLong("id"));
assertEquals(2, auditDetails.size()); assertEquals(2, auditDetails.size());
assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail1")); assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail1"));
assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail2")); assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail2"));
@ -188,13 +236,13 @@ class AuditActionTest extends BaseTest
auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow("audit", "recordId", recordId2); auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow("audit", "recordId", recordId2);
assertEquals("Test Another Audit", auditRecord.getValueString("message")); assertEquals("Test Another Audit", auditRecord.getValueString("message"));
assertEquals(47, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE)); assertEquals(47, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE));
auditDetails = GeneralProcessUtils.getRecordListByField("auditDetail", "auditId", auditRecord.getValue("id")); auditDetails = GeneralProcessUtils.getRecordListByField("auditDetail", "auditId", auditRecord.getValueLong("id"));
assertEquals(0, auditDetails.size()); assertEquals(0, auditDetails.size());
auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow("audit", "recordId", recordId3); auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow("audit", "recordId", recordId3);
assertEquals("Audit 3", auditRecord.getValueString("message")); assertEquals("Audit 3", auditRecord.getValueString("message"));
assertEquals(42, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE)); assertEquals(42, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE));
auditDetails = GeneralProcessUtils.getRecordListByField("auditDetail", "auditId", auditRecord.getValue("id")); auditDetails = GeneralProcessUtils.getRecordListByField("auditDetail", "auditId", auditRecord.getValueLong("id"));
assertEquals(1, auditDetails.size()); assertEquals(1, auditDetails.size());
assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail3")); assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail3"));
} }

View File

@ -0,0 +1,243 @@
/*
* 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.processes.implementations.garbagecollector;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for GenericGarbageCollectorExecuteStep
*******************************************************************************/
class GenericGarbageCollectorExecuteStepTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfterEach()
{
MemoryRecordStore.getInstance().reset();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testErrors() throws Exception
{
QContext.getQInstance().addProcess(new GenericGarbageCollectorProcessMetaDataProducer().produce(QContext.getQInstance()));
RunProcessInput input = new RunProcessInput();
input.setProcessName(GenericGarbageCollectorProcessMetaDataProducer.NAME);
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("Unrecognized table: null");
input.addValue("table", "notATable");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("Unrecognized table: notATable");
input.addValue("table", TestUtils.TABLE_NAME_PERSON_MEMORY);
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("Unrecognized field: null");
input.addValue("field", "notAField");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("Unrecognized field: notAField");
input.addValue("field", "firstName");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("not a date");
input.addValue("field", "timestamp");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("daysBack: null");
input.addValue("daysBack", "-1");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("daysBack: -1");
input.addValue("daysBack", "1");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("maxPageSize: null");
input.addValue("maxPageSize", "-1");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("maxPageSize: -1");
input.addValue("maxPageSize", "1");
assertThatThrownBy(() -> runAndThrow(input)).isInstanceOf(QUserFacingException.class).hasMessageContaining("Could not find min date value in table");
}
/***************************************************************************
**
***************************************************************************/
private void runAndThrow(RunProcessInput input) throws Exception
{
input.setStartAfterStep(null);
input.setProcessUUID(null);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
if(runProcessOutput.getException().isPresent())
{
throw (runProcessOutput.getException().get());
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test30days() throws QException
{
insertAndRunGC(30);
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withFilter(new QQueryFilter()));
assertEquals(2, queryOutput.getRecords().size());
assertEquals(Set.of(4, 5), queryOutput.getRecords().stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
@Disabled("memory aggregator is failing to return an aggregate when no rows found, which is throwing an error...")
void test100days() throws QException
{
insertAndRunGC(100);
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withFilter(new QQueryFilter()));
assertEquals(5, queryOutput.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test10days() throws QException
{
insertAndRunGC(10);
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withFilter(new QQueryFilter()));
assertEquals(1, queryOutput.getRecords().size());
assertEquals(Set.of(5), queryOutput.getRecords().stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test1day() throws QException
{
insertAndRunGC(1);
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withFilter(new QQueryFilter()));
assertEquals(0, queryOutput.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test1dayPartitioned() throws QException
{
insertAndRunGC(1, 2);
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withFilter(new QQueryFilter()));
assertEquals(0, queryOutput.getRecords().size());
}
/***************************************************************************
**
***************************************************************************/
private void insertAndRunGC(Integer daysBack) throws QException
{
insertAndRunGC(daysBack, 1000);
}
/***************************************************************************
**
***************************************************************************/
private void insertAndRunGC(Integer daysBack, Integer maxPageSize) throws QException
{
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(getPersonRecords()));
QContext.getQInstance().addProcess(new GenericGarbageCollectorProcessMetaDataProducer().produce(QContext.getQInstance()));
RunProcessInput input = new RunProcessInput();
input.setProcessName(GenericGarbageCollectorProcessMetaDataProducer.NAME);
input.addValue("table", TestUtils.TABLE_NAME_PERSON_MEMORY);
input.addValue("field", "timestamp");
input.addValue("daysBack", daysBack);
input.addValue("maxPageSize", maxPageSize);
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
new RunProcessAction().execute(input);
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> getPersonRecords()
{
List<QRecord> records = List.of(
new QRecord().withValue("id", 1).withValue("timestamp", Instant.now().minus(90, ChronoUnit.DAYS)),
new QRecord().withValue("id", 2).withValue("timestamp", Instant.now().minus(31, ChronoUnit.DAYS)),
new QRecord().withValue("id", 3).withValue("timestamp", Instant.now().minus(30, ChronoUnit.DAYS).minus(5, ChronoUnit.MINUTES)),
new QRecord().withValue("id", 4).withValue("timestamp", Instant.now().minus(29, ChronoUnit.DAYS).minus(23, ChronoUnit.HOURS)),
new QRecord().withValue("id", 5).withValue("timestamp", Instant.now().minus(5, ChronoUnit.DAYS)));
return records;
}
}

View File

@ -748,7 +748,7 @@ public class BaseAPIActionUtil
{ {
try try
{ {
String uri = request.getURI().toString(); String uri = request.getURI().toString();
String pair = backendMetaData.getApiKeyQueryParamName() + "=" + getApiKey(); 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); setupContentTypeInRequest(request);
setupAdditionalHeaders(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())) if("POST".equals(request.getMethod()))
{ {
LOG.info("POST contents [" + ((HttpPost) request).getEntity().toString() + "]"); LOG.info("POST contents [" + ((HttpPost) request).getEntity().toString() + "]");

View File

@ -146,8 +146,8 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
// todo sql customization - can edit sql and/or param list // todo sql customization - can edit sql and/or param list
// todo - non-serial-id style tables // todo - non-serial-id style tables
// todo - other generated values, e.g., createDate... maybe need to re-select? // todo - other generated values, e.g., createDate... maybe need to re-select?
List<Integer> idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); List<Serializable> idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params, table.getField(table.getPrimaryKeyField()).getType());
int index = 0; int index = 0;
for(QRecord record : page) for(QRecord record : page)
{ {
QRecord outputRecord = new QRecord(record); QRecord outputRecord = new QRecord(record);
@ -155,7 +155,7 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
if(CollectionUtils.nullSafeIsEmpty(record.getErrors())) if(CollectionUtils.nullSafeIsEmpty(record.getErrors()))
{ {
Integer id = idList.get(index++); Serializable id = idList.get(index++);
outputRecord.setValue(table.getPrimaryKeyField(), id); outputRecord.setValue(table.getPrimaryKeyField(), id);
} }
} }

View File

@ -53,6 +53,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -526,18 +527,45 @@ public class QueryManager
/******************************************************************************* /*******************************************************************************
** todo - needs (specific) unit test ** todo - needs (specific) unit test
*******************************************************************************/ *******************************************************************************/
public static List<Integer> executeInsertForGeneratedIds(Connection connection, String sql, List<Object> params) throws SQLException public static List<Serializable> executeInsertForGeneratedIds(Connection connection, String sql, List<Object> params, QFieldType idType) throws SQLException
{ {
try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS))
{ {
bindParams(params.toArray(), statement); bindParams(params.toArray(), statement);
incrementStatistic(STAT_QUERIES_RAN); incrementStatistic(STAT_QUERIES_RAN);
statement.executeUpdate(); statement.executeUpdate();
ResultSet generatedKeys = statement.getGeneratedKeys();
List<Integer> rs = new ArrayList<>(); /////////////////////////////////////////////////////////////
// We default to idType of INTEGER if it was not passed in //
/////////////////////////////////////////////////////////////
if(idType == null)
{
idType = QFieldType.INTEGER;
}
ResultSet generatedKeys = statement.getGeneratedKeys();
List<Serializable> rs = new ArrayList<>();
while(generatedKeys.next()) while(generatedKeys.next())
{ {
rs.add(getInteger(generatedKeys, 1)); switch(idType)
{
case INTEGER:
{
rs.add(getInteger(generatedKeys, 1));
break;
}
case LONG:
{
rs.add(getLong(generatedKeys, 1));
break;
}
default:
{
LOG.warn("Unknown id data type, attempting to getInteger.", logPair("sql", sql));
rs.add(getInteger(generatedKeys, 1));
break;
}
}
} }
return (rs); return (rs);
} }

View File

@ -212,7 +212,7 @@ public class RDBMSCountActionTest extends RDBMSActionTest
CountInput countInput = new CountInput(); CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE); 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 @Test
void testRecordSecurityWithLockFromJoinTableWhereTheKeyIsOnTheManySide() throws QException void testRecordSecurityWithLockFromJoinTableWhereTheKeyIsOnTheManySide() throws QException
@ -897,8 +955,9 @@ public class RDBMSQueryActionJoinsTest extends RDBMSActionTest
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE); queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
assertThat(new QueryAction().execute(queryInput).getRecords()) List<QRecord> records = new QueryAction().execute(queryInput).getRecords();
.hasSize(1); assertThat(records)
.hasSize(4);
} }

View File

@ -1 +1 @@
0.22.0 0.23.0

View File

@ -1279,6 +1279,11 @@ public class QJavalinImplementation
queryInput.getFilter().setLimit(limit); queryInput.getFilter().setLimit(limit);
} }
if(queryInput.getFilter() == null || queryInput.getFilter().getLimit() == null)
{
handleQueryNullLimit(context, queryInput);
}
List<QueryJoin> queryJoins = processQueryJoinsParam(context); List<QueryJoin> queryJoins = processQueryJoinsParam(context);
queryInput.setQueryJoins(queryJoins); queryInput.setQueryJoins(queryJoins);
@ -1299,6 +1304,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 java.util.function.Function;
import org.apache.logging.log4j.Level;
/******************************************************************************* /*******************************************************************************
@ -36,6 +37,10 @@ public class QJavalinMetaData
private Function<QJavalinAccessLogger.LogEntry, Boolean> logFilter; 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); 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 java.util.function.Function;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; 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.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; 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 com.kingsrook.qqq.backend.core.utils.SleepUtils;
import kong.unirest.HttpResponse; import kong.unirest.HttpResponse;
import kong.unirest.Unirest; import kong.unirest.Unirest;
import org.apache.logging.log4j.Level;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; 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. // // filter use-case, with no values, should return options. //
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/pet/possibleValues/ownerPersonId?searchTerm=&useCase=filter").asString(); 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); assertNotNull(options);
assertThat(options.length()).isGreaterThanOrEqualTo(5); assertThat(options.length()).isGreaterThanOrEqualTo(5);

View File

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