Merged dev into feature/CE-847-bug-triggers-running

This commit is contained in:
2024-02-12 11:14:59 -06:00
17 changed files with 543 additions and 266 deletions

View File

@ -65,13 +65,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.NotImplementedException;
import org.json.JSONObject;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -387,13 +388,15 @@ public class PollingAutomationPerTableRunner implements Runnable
if(filterId != null) if(filterId != null)
{ {
GetInput getInput = new GetInput(); GetInput getInput = new GetInput();
getInput.setTableName(SavedFilter.TABLE_NAME); getInput.setTableName(SavedView.TABLE_NAME);
getInput.setPrimaryKey(filterId); getInput.setPrimaryKey(filterId);
GetOutput getOutput = new GetAction().execute(getInput); GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null) if(getOutput.getRecord() != null)
{ {
SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); SavedView savedView = new SavedView(getOutput.getRecord());
filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); JSONObject viewJson = new JSONObject(savedView.getViewJson());
JSONObject queryFilter = viewJson.getJSONObject("queryFilter");
filter = JsonUtils.toObject(queryFilter.toString(), QQueryFilter.class);
} }
} }

View File

@ -297,4 +297,21 @@ public enum DateTimeGroupBy
ZonedDateTime zoned = instant.atZone(zoneId); ZonedDateTime zoned = instant.atZone(zoneId);
return (zoned.plus(noOfChronoUnitsToAdd, chronoUnitToAdd).toInstant()); return (zoned.plus(noOfChronoUnitsToAdd, chronoUnitToAdd).toInstant());
} }
/*******************************************************************************
**
*******************************************************************************/
public static DateTimeFormatter sqlDateFormatToSelectedDateTimeFormatter(String sqlDateFormat)
{
for(DateTimeGroupBy value : values())
{
if(value.sqlDateFormat.equals(sqlDateFormat))
{
return (value.selectedStringFormatter);
}
}
return null;
}
} }

View File

@ -76,6 +76,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
@ -87,6 +88,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
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;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
/******************************************************************************* /*******************************************************************************
@ -493,6 +495,11 @@ public class QInstanceValidator
validateTableRecordSecurityLocks(qInstance, table); validateTableRecordSecurityLocks(qInstance, table);
validateTableAssociations(qInstance, table); validateTableAssociations(qInstance, table);
validateExposedJoins(qInstance, joinGraph, table); validateExposedJoins(qInstance, joinGraph, table);
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
{
supplementalTableMetaData.validate(qInstance, table, this);
}
}); });
} }
} }
@ -1784,20 +1791,6 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
@FunctionalInterface
interface UnsafeLambda
{
/*******************************************************************************
**
*******************************************************************************/
void run() throws Exception;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -28,7 +28,7 @@ import com.kingsrook.qqq.backend.core.model.data.QField;
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.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.Script;
@ -51,7 +51,7 @@ public class TableTrigger extends QRecordEntity
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME) @QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName; private String tableName;
@QField(possibleValueSourceName = SavedFilter.TABLE_NAME) @QField(possibleValueSourceName = SavedView.TABLE_NAME)
private Integer filterId; private Integer filterId;
@QField(possibleValueSourceName = Script.TABLE_NAME) @QField(possibleValueSourceName = Script.TABLE_NAME)

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables; package com.kingsrook.qqq.backend.core.model.metadata.tables;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -69,4 +70,16 @@ public abstract class QSupplementalTableMetaData
// noop in base class // // noop in base class //
//////////////////////// ////////////////////////
} }
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
{
////////////////////////
// noop in base class //
////////////////////////
}
} }

View File

@ -1,6 +1,6 @@
/* /*
* QQQ - Low-code Application Framework for Engineers. * QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com * contact@kingsrook.com
* https://github.com/Kingsrook/ * https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.kingsrook.qqq.backend.core.model.savedfilters; package com.kingsrook.qqq.backend.core.model.savedviews;
import java.time.Instant; import java.time.Instant;
@ -32,9 +32,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/******************************************************************************* /*******************************************************************************
** Entity bean for the saved filter table ** Entity bean for the saved filter table
*******************************************************************************/ *******************************************************************************/
public class SavedFilter extends QRecordEntity public class SavedView extends QRecordEntity
{ {
public static final String TABLE_NAME = "savedFilter"; public static final String TABLE_NAME = "savedView";
@QField(isEditable = false) @QField(isEditable = false)
private Integer id; private Integer id;
@ -55,7 +55,7 @@ public class SavedFilter extends QRecordEntity
private String userId; private String userId;
@QField(isEditable = false) @QField(isEditable = false)
private String filterJson; private String viewJson;
@ -63,7 +63,7 @@ public class SavedFilter extends QRecordEntity
** Constructor ** Constructor
** **
*******************************************************************************/ *******************************************************************************/
public SavedFilter() public SavedView()
{ {
} }
@ -73,7 +73,7 @@ public class SavedFilter extends QRecordEntity
** Constructor ** Constructor
** **
*******************************************************************************/ *******************************************************************************/
public SavedFilter(QRecord qRecord) throws QException public SavedView(QRecord qRecord) throws QException
{ {
populateFromQRecord(qRecord); populateFromQRecord(qRecord);
} }
@ -172,7 +172,7 @@ public class SavedFilter extends QRecordEntity
** Fluent setter for label ** Fluent setter for label
** **
*******************************************************************************/ *******************************************************************************/
public SavedFilter withLabel(String label) public SavedView withLabel(String label)
{ {
this.label = label; this.label = label;
return (this); return (this);
@ -206,7 +206,7 @@ public class SavedFilter extends QRecordEntity
** Fluent setter for tableName ** Fluent setter for tableName
** **
*******************************************************************************/ *******************************************************************************/
public SavedFilter withTableName(String tableName) public SavedView withTableName(String tableName)
{ {
this.tableName = tableName; this.tableName = tableName;
return (this); return (this);
@ -240,7 +240,7 @@ public class SavedFilter extends QRecordEntity
** Fluent setter for userId ** Fluent setter for userId
** **
*******************************************************************************/ *******************************************************************************/
public SavedFilter withUserId(String userId) public SavedView withUserId(String userId)
{ {
this.userId = userId; this.userId = userId;
return (this); return (this);
@ -249,34 +249,31 @@ public class SavedFilter extends QRecordEntity
/******************************************************************************* /*******************************************************************************
** Getter for filterJson ** Getter for viewJson
**
*******************************************************************************/ *******************************************************************************/
public String getFilterJson() public String getViewJson()
{ {
return filterJson; return (this.viewJson);
} }
/******************************************************************************* /*******************************************************************************
** Setter for filterJson ** Setter for viewJson
**
*******************************************************************************/ *******************************************************************************/
public void setFilterJson(String filterJson) public void setViewJson(String viewJson)
{ {
this.filterJson = filterJson; this.viewJson = viewJson;
} }
/******************************************************************************* /*******************************************************************************
** Fluent setter for filterJson ** Fluent setter for viewJson
**
*******************************************************************************/ *******************************************************************************/
public SavedFilter withFilterJson(String filterJson) public SavedView withViewJson(String viewJson)
{ {
this.filterJson = filterJson; this.viewJson = viewJson;
return (this); return (this);
} }

View File

@ -1,6 +1,6 @@
/* /*
* QQQ - Low-code Application Framework for Engineers. * QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com * contact@kingsrook.com
* https://github.com/Kingsrook/ * https://github.com/Kingsrook/
@ -19,25 +19,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.kingsrook.qqq.backend.core.model.savedfilters; package com.kingsrook.qqq.backend.core.model.savedviews;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.DeleteSavedFilterProcess; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.QuerySavedFilterProcess; import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.DeleteSavedViewProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.StoreSavedFilterProcess; import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.QuerySavedViewProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.StoreSavedViewProcess;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class SavedFiltersMetaDataProvider public class SavedViewsMetaDataProvider
{ {
@ -46,11 +52,11 @@ public class SavedFiltersMetaDataProvider
*******************************************************************************/ *******************************************************************************/
public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{ {
instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher)); instance.addTable(defineSavedViewTable(backendName, backendDetailEnricher));
instance.addPossibleValueSource(defineSavedFilterPossibleValueSource()); instance.addPossibleValueSource(defineSavedViewPossibleValueSource());
instance.addProcess(QuerySavedFilterProcess.getProcessMetaData()); instance.addProcess(QuerySavedViewProcess.getProcessMetaData());
instance.addProcess(StoreSavedFilterProcess.getProcessMetaData()); instance.addProcess(StoreSavedViewProcess.getProcessMetaData());
instance.addProcess(DeleteSavedFilterProcess.getProcessMetaData()); instance.addProcess(DeleteSavedViewProcess.getProcessMetaData());
} }
@ -58,16 +64,21 @@ public class SavedFiltersMetaDataProvider
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private QTableMetaData defineSavedFilterTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException public QTableMetaData defineSavedViewTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{ {
QTableMetaData table = new QTableMetaData() QTableMetaData table = new QTableMetaData()
.withName(SavedFilter.TABLE_NAME) .withName(SavedView.TABLE_NAME)
.withLabel("Saved Filter") .withLabel("Saved View")
.withRecordLabelFormat("%s") .withRecordLabelFormat("%s")
.withRecordLabelFields("label") .withRecordLabelFields("label")
.withBackendName(backendName) .withBackendName(backendName)
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withFieldsFromEntity(SavedFilter.class); .withFieldsFromEntity(SavedView.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "tableName", "viewJson")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
table.getField("viewJson").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json")));
if(backendDetailEnricher != null) if(backendDetailEnricher != null)
{ {
@ -82,12 +93,12 @@ public class SavedFiltersMetaDataProvider
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private QPossibleValueSource defineSavedFilterPossibleValueSource() private QPossibleValueSource defineSavedViewPossibleValueSource()
{ {
return new QPossibleValueSource() return new QPossibleValueSource()
.withName(SavedFilter.TABLE_NAME) .withName(SavedView.TABLE_NAME)
.withType(QPossibleValueSourceType.TABLE) .withType(QPossibleValueSourceType.TABLE)
.withTableName(SavedFilter.TABLE_NAME) .withTableName(SavedView.TABLE_NAME)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY)
.withOrderByField("label"); .withOrderByField("label");
} }

View File

@ -24,6 +24,10 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
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 +39,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.DateTimeGroupBy;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
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;
@ -66,6 +71,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -577,7 +583,11 @@ public class MemoryRecordStore
for(GroupBy groupBy : groupBys) for(GroupBy groupBy : groupBys)
{ {
Serializable groupByValue = record.getValue(groupBy.getFieldName()); Serializable groupByValue = record.getValue(groupBy.getFieldName());
if(groupBy.getType() != null) if(StringUtils.hasContent(groupBy.getFormatString()))
{
groupByValue = applyFormatString(groupByValue, groupBy);
}
else if(groupBy.getType() != null)
{ {
groupByValue = ValueUtils.getValueAsFieldType(groupBy.getType(), groupByValue); groupByValue = ValueUtils.getValueAsFieldType(groupBy.getType(), groupByValue);
} }
@ -629,7 +639,9 @@ public class MemoryRecordStore
///////////////////// /////////////////////
if(aggregateInput.getFilter() != null && CollectionUtils.nullSafeHasContents(aggregateInput.getFilter().getOrderBys())) if(aggregateInput.getFilter() != null && CollectionUtils.nullSafeHasContents(aggregateInput.getFilter().getOrderBys()))
{ {
Comparator<AggregateResult> comparator = null; /////////////////////////////////////////////////////////////////////////////////////
// lambda to compare 2 serializables, as we'll assume (& cast) them to Comparables //
/////////////////////////////////////////////////////////////////////////////////////
Comparator<Serializable> serializableComparator = (Serializable a, Serializable b) -> Comparator<Serializable> serializableComparator = (Serializable a, Serializable b) ->
{ {
if(a == null && b == null) if(a == null && b == null)
@ -647,9 +659,15 @@ public class MemoryRecordStore
return ((Comparable) a).compareTo(b); return ((Comparable) a).compareTo(b);
}; };
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// reverse of the lambda above (we had some errors calling .reversed() on the comparator we were building, so this seemed simpler & worked) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Comparator<Serializable> reverseSerializableComparator = (Serializable a, Serializable b) -> -serializableComparator.compare(a, b);
//////////////////////////////////////////////// ////////////////////////////////////////////////
// build a comparator out of all the orderBys // // build a comparator out of all the orderBys //
//////////////////////////////////////////////// ////////////////////////////////////////////////
Comparator<AggregateResult> comparator = null;
for(QFilterOrderBy orderBy : aggregateInput.getFilter().getOrderBys()) for(QFilterOrderBy orderBy : aggregateInput.getFilter().getOrderBys())
{ {
Function<AggregateResult, Serializable> keyExtractor = aggregateResult -> Function<AggregateResult, Serializable> keyExtractor = aggregateResult ->
@ -670,16 +688,11 @@ public class MemoryRecordStore
if(comparator == null) if(comparator == null)
{ {
comparator = Comparator.comparing(keyExtractor, serializableComparator); comparator = Comparator.comparing(keyExtractor, orderBy.getIsAscending() ? serializableComparator : reverseSerializableComparator);
} }
else else
{ {
comparator = comparator.thenComparing(keyExtractor, serializableComparator); comparator = comparator.thenComparing(keyExtractor, orderBy.getIsAscending() ? serializableComparator : reverseSerializableComparator);
}
if(!orderBy.getIsAscending())
{
comparator = comparator.reversed();
} }
} }
@ -696,6 +709,57 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
private Serializable applyFormatString(Serializable value, GroupBy groupBy) throws QException
{
if(value == null)
{
return (null);
}
String formatString = groupBy.getFormatString();
try
{
if(formatString.startsWith("DATE_FORMAT"))
{
/////////////////////////////////////////////////////////////////////////////
// one known-use case we have here looks like this: //
// DATE_FORMAT(CONVERT_TZ(%s, 'UTC', 'UTC'), '%%Y-%%m-%%dT%%H') //
// ... for now, let's just try to support the formatting bit at the end... //
// todo - support the CONVERT_TZ bit too! //
/////////////////////////////////////////////////////////////////////////////
String sqlDateTimeFormat = formatString.replaceFirst(".*'%%", "%%").replaceFirst("'.*", "");
DateTimeFormatter dateTimeFormatter = DateTimeGroupBy.sqlDateFormatToSelectedDateTimeFormatter(sqlDateTimeFormat);
if(dateTimeFormatter == null)
{
throw (new QException("Unsupported sql dateTime format string [" + sqlDateTimeFormat + "] for MemoryRecordStore"));
}
String valueAsString = ValueUtils.getValueAsString(value);
Instant valueAsInstant = ValueUtils.getValueAsInstant(valueAsString);
ZonedDateTime zonedDateTime = valueAsInstant.atZone(ZoneId.systemDefault());
return (dateTimeFormatter.format(zonedDateTime));
}
else
{
throw (new QException("Unsupported group-by format string [" + formatString + "] for MemoryRecordStore"));
}
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error applying format string [" + formatString + "] to group by value [" + value + "]", e));
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.processes.implementations.columnstats;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -173,11 +175,10 @@ public class ColumnStatsStep implements BackendStep
Aggregate aggregate = new Aggregate(table.getPrimaryKeyField(), AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL); Aggregate aggregate = new Aggregate(table.getPrimaryKeyField(), AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL);
GroupBy groupBy = new GroupBy(field.getType(), fieldName); GroupBy groupBy = new GroupBy(field.getType(), fieldName);
// todo - something here about "by-date, not time" // todo - something here about an input param to specify how you want dates & date-times grouped
if(field.getType().equals(QFieldType.DATE_TIME)) if(field.getType().equals(QFieldType.DATE_TIME))
{ {
// groupBy = new GroupBy(field.getType(), fieldName, "DATE(%s)"); String sqlExpression = DateTimeGroupBy.HOUR.getSqlExpression(ZoneId.systemDefault());
String sqlExpression = DateTimeGroupBy.HOUR.getSqlExpression();
groupBy = new GroupBy(QFieldType.STRING, fieldName, sqlExpression); groupBy = new GroupBy(QFieldType.STRING, fieldName, sqlExpression);
} }
@ -230,6 +231,12 @@ public class ColumnStatsStep implements BackendStep
for(AggregateResult result : aggregateOutput.getResults()) for(AggregateResult result : aggregateOutput.getResults())
{ {
Serializable value = result.getGroupByValue(groupBy); Serializable value = result.getGroupByValue(groupBy);
if(field.getType().equals(QFieldType.DATE_TIME) && value != null)
{
value = Instant.parse(value + ":00:00Z");
}
Integer count = ValueUtils.getValueAsInteger(result.getAggregateValue(aggregate)); Integer count = ValueUtils.getValueAsInteger(result.getAggregateValue(aggregate));
valueCounts.add(new QRecord().withValue(fieldName, value).withValue("count", count)); valueCounts.add(new QRecord().withValue(fieldName, value).withValue("count", count));
} }

View File

@ -1,6 +1,6 @@
/* /*
* QQQ - Low-code Application Framework for Engineers. * QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com * contact@kingsrook.com
* https://github.com/Kingsrook/ * https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
import java.util.List; import java.util.List;
@ -34,15 +34,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
/******************************************************************************* /*******************************************************************************
** Process used by the delete filter dialog ** Process used by the delete view dialog
*******************************************************************************/ *******************************************************************************/
public class DeleteSavedFilterProcess implements BackendStep public class DeleteSavedViewProcess implements BackendStep
{ {
private static final QLogger LOG = QLogger.getLogger(DeleteSavedFilterProcess.class); private static final QLogger LOG = QLogger.getLogger(DeleteSavedViewProcess.class);
@ -52,10 +52,10 @@ public class DeleteSavedFilterProcess implements BackendStep
public static QProcessMetaData getProcessMetaData() public static QProcessMetaData getProcessMetaData()
{ {
return (new QProcessMetaData() return (new QProcessMetaData()
.withName("deleteSavedFilter") .withName("deleteSavedView")
.withStepList(List.of( .withStepList(List.of(
new QBackendStepMetaData() new QBackendStepMetaData()
.withCode(new QCodeReference(DeleteSavedFilterProcess.class)) .withCode(new QCodeReference(DeleteSavedViewProcess.class))
.withName("delete") .withName("delete")
))); )));
} }
@ -72,16 +72,16 @@ public class DeleteSavedFilterProcess implements BackendStep
try try
{ {
Integer savedFilterId = runBackendStepInput.getValueInteger("id"); Integer savedViewId = runBackendStepInput.getValueInteger("id");
DeleteInput input = new DeleteInput(); DeleteInput input = new DeleteInput();
input.setTableName(SavedFilter.TABLE_NAME); input.setTableName(SavedView.TABLE_NAME);
input.setPrimaryKeys(List.of(savedFilterId)); input.setPrimaryKeys(List.of(savedViewId));
new DeleteAction().execute(input); new DeleteAction().execute(input);
} }
catch(Exception e) catch(Exception e)
{ {
LOG.warn("Error deleting saved filter", e); LOG.warn("Error deleting saved view", e);
throw (e); throw (e);
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* QQQ - Low-code Application Framework for Engineers. * QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com * contact@kingsrook.com
* https://github.com/Kingsrook/ * https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
import java.io.Serializable; import java.io.Serializable;
@ -43,15 +43,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
/******************************************************************************* /*******************************************************************************
** Process used by the saved filter dialogs ** Process used by the saved view dialogs
*******************************************************************************/ *******************************************************************************/
public class QuerySavedFilterProcess implements BackendStep public class QuerySavedViewProcess implements BackendStep
{ {
private static final QLogger LOG = QLogger.getLogger(QuerySavedFilterProcess.class); private static final QLogger LOG = QLogger.getLogger(QuerySavedViewProcess.class);
@ -61,10 +61,10 @@ public class QuerySavedFilterProcess implements BackendStep
public static QProcessMetaData getProcessMetaData() public static QProcessMetaData getProcessMetaData()
{ {
return (new QProcessMetaData() return (new QProcessMetaData()
.withName("querySavedFilter") .withName("querySavedView")
.withStepList(List.of( .withStepList(List.of(
new QBackendStepMetaData() new QBackendStepMetaData()
.withCode(new QCodeReference(QuerySavedFilterProcess.class)) .withCode(new QCodeReference(QuerySavedViewProcess.class))
.withName("query") .withName("query")
))); )));
} }
@ -81,36 +81,36 @@ public class QuerySavedFilterProcess implements BackendStep
try try
{ {
Integer savedFilterId = runBackendStepInput.getValueInteger("id"); Integer savedViewId = runBackendStepInput.getValueInteger("id");
if(savedFilterId != null) if(savedViewId != null)
{ {
GetInput input = new GetInput(); GetInput input = new GetInput();
input.setTableName(SavedFilter.TABLE_NAME); input.setTableName(SavedView.TABLE_NAME);
input.setPrimaryKey(savedFilterId); input.setPrimaryKey(savedViewId);
GetOutput output = new GetAction().execute(input); GetOutput output = new GetAction().execute(input);
runBackendStepOutput.addRecord(output.getRecord()); runBackendStepOutput.addRecord(output.getRecord());
runBackendStepOutput.addValue("savedFilter", output.getRecord()); runBackendStepOutput.addValue("savedView", output.getRecord());
runBackendStepOutput.addValue("savedFilterList", (Serializable) List.of(output.getRecord())); runBackendStepOutput.addValue("savedViewList", (Serializable) List.of(output.getRecord()));
} }
else else
{ {
String tableName = runBackendStepInput.getValueString("tableName"); String tableName = runBackendStepInput.getValueString("tableName");
QueryInput input = new QueryInput(); QueryInput input = new QueryInput();
input.setTableName(SavedFilter.TABLE_NAME); input.setTableName(SavedView.TABLE_NAME);
input.setFilter(new QQueryFilter() input.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName)) .withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName))
.withOrderBy(new QFilterOrderBy("label"))); .withOrderBy(new QFilterOrderBy("label")));
QueryOutput output = new QueryAction().execute(input); QueryOutput output = new QueryAction().execute(input);
runBackendStepOutput.setRecords(output.getRecords()); runBackendStepOutput.setRecords(output.getRecords());
runBackendStepOutput.addValue("savedFilterList", (Serializable) output.getRecords()); runBackendStepOutput.addValue("savedViewList", (Serializable) output.getRecords());
} }
} }
catch(Exception e) catch(Exception e)
{ {
LOG.warn("Error deleting saved filter", e); LOG.warn("Error querying for saved views", e);
throw (e); throw (e);
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* QQQ - Low-code Application Framework for Engineers. * QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com * contact@kingsrook.com
* https://github.com/Kingsrook/ * https://github.com/Kingsrook/
@ -19,37 +19,45 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
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.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
** Process used by the saved filter dialog ** Process used by the saved view dialog
*******************************************************************************/ *******************************************************************************/
public class StoreSavedFilterProcess implements BackendStep public class StoreSavedViewProcess implements BackendStep
{ {
private static final QLogger LOG = QLogger.getLogger(StoreSavedFilterProcess.class); private static final QLogger LOG = QLogger.getLogger(StoreSavedViewProcess.class);
@ -59,10 +67,10 @@ public class StoreSavedFilterProcess implements BackendStep
public static QProcessMetaData getProcessMetaData() public static QProcessMetaData getProcessMetaData()
{ {
return (new QProcessMetaData() return (new QProcessMetaData()
.withName("storeSavedFilter") .withName("storeSavedView")
.withStepList(List.of( .withStepList(List.of(
new QBackendStepMetaData() new QBackendStepMetaData()
.withCode(new QCodeReference(StoreSavedFilterProcess.class)) .withCode(new QCodeReference(StoreSavedViewProcess.class))
.withName("store") .withName("store")
))); )));
} }
@ -79,39 +87,73 @@ public class StoreSavedFilterProcess implements BackendStep
try try
{ {
String userId = QContext.getQSession().getUser().getIdReference();
String tableName = runBackendStepInput.getValueString("tableName");
String label = runBackendStepInput.getValueString("label");
QRecord qRecord = new QRecord() QRecord qRecord = new QRecord()
.withValue("id", runBackendStepInput.getValueInteger("id")) .withValue("id", runBackendStepInput.getValueInteger("id"))
.withValue("label", runBackendStepInput.getValueString("label")) .withValue("viewJson", runBackendStepInput.getValueString("viewJson"))
.withValue("tableName", runBackendStepInput.getValueString("tableName")) .withValue("label", label)
.withValue("filterJson", runBackendStepInput.getValueString("filterJson")) .withValue("tableName", tableName)
.withValue("userId", runBackendStepInput.getSession().getUser().getIdReference()); .withValue("userId", userId);
List<QRecord> savedFilterList = new ArrayList<>(); List<QRecord> savedViewList;
if(qRecord.getValueInteger("id") == null) if(qRecord.getValueInteger("id") == null)
{ {
checkForDuplicates(userId, tableName, label, null);
InsertInput input = new InsertInput(); InsertInput input = new InsertInput();
input.setTableName(SavedFilter.TABLE_NAME); input.setTableName(SavedView.TABLE_NAME);
input.setRecords(List.of(qRecord)); input.setRecords(List.of(qRecord));
InsertOutput output = new InsertAction().execute(input); InsertOutput output = new InsertAction().execute(input);
savedFilterList = output.getRecords(); savedViewList = output.getRecords();
} }
else else
{ {
checkForDuplicates(userId, tableName, label, qRecord.getValueInteger("id"));
UpdateInput input = new UpdateInput(); UpdateInput input = new UpdateInput();
input.setTableName(SavedFilter.TABLE_NAME); input.setTableName(SavedView.TABLE_NAME);
input.setRecords(List.of(qRecord)); input.setRecords(List.of(qRecord));
UpdateOutput output = new UpdateAction().execute(input); UpdateOutput output = new UpdateAction().execute(input);
savedFilterList = output.getRecords(); savedViewList = output.getRecords();
} }
runBackendStepOutput.addValue("savedFilterList", (Serializable) savedFilterList); runBackendStepOutput.addValue("savedViewList", (Serializable) savedViewList);
} }
catch(Exception e) catch(Exception e)
{ {
LOG.warn("Error storing data saved filter", e); LOG.warn("Error storing saved view", e);
throw (e); throw (e);
} }
} }
/*******************************************************************************
**
*******************************************************************************/
private static void checkForDuplicates(String userId, String tableName, String label, Integer id) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(SavedView.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(
new QFilterCriteria("userId", QCriteriaOperator.EQUALS, userId),
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName),
new QFilterCriteria("label", QCriteriaOperator.EQUALS, label)));
if(id != null)
{
queryInput.getFilter().addCriteria(new QFilterCriteria("id", QCriteriaOperator.NOT_EQUALS, id));
}
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
throw (new QUserFacingException("You already have a saved view on this table with this name."));
}
}
} }

View File

@ -0,0 +1,37 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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.utils.lambdas;
/*******************************************************************************
**
*******************************************************************************/
@FunctionalInterface
public interface UnsafeLambda
{
/*******************************************************************************
**
*******************************************************************************/
void run() throws Exception;
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.columnstats;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
@ -91,4 +92,50 @@ class ColumnStatsStepTest extends BaseTest
.hasFieldOrPropertyWithValue("percent", new BigDecimal("16.67")); .hasFieldOrPropertyWithValue("percent", new BigDecimal("16.67"));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDateTimesRollupByHour() throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
insertInput.setRecords(List.of(
new QRecord().withValue("timestamp", Instant.parse("2024-01-31T09:59:01Z")),
new QRecord().withValue("timestamp", Instant.parse("2024-01-31T09:59:59Z")),
new QRecord().withValue("timestamp", Instant.parse("2024-01-31T10:00:00Z")),
new QRecord().withValue("timestamp", Instant.parse("2024-01-31T10:01:01Z")),
new QRecord().withValue("timestamp", Instant.parse("2024-01-31T10:59:59Z")),
new QRecord().withValue("timestamp", null)
));
new InsertAction().execute(insertInput);
RunBackendStepInput input = new RunBackendStepInput();
input.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
input.addValue("fieldName", "timestamp");
input.addValue("orderBy", "count.desc");
RunBackendStepOutput output = new RunBackendStepOutput();
new ColumnStatsStep().run(input, output);
Map<String, Serializable> values = output.getValues();
@SuppressWarnings("unchecked")
List<QRecord> valueCounts = (List<QRecord>) values.get("valueCounts");
assertThat(valueCounts.get(0).getValues())
.hasFieldOrPropertyWithValue("timestamp", Instant.parse("2024-01-31T10:00:00Z"))
.hasFieldOrPropertyWithValue("count", 3);
assertThat(valueCounts.get(1).getValues())
.hasFieldOrPropertyWithValue("timestamp", Instant.parse("2024-01-31T09:00:00Z"))
.hasFieldOrPropertyWithValue("count", 2);
assertThat(valueCounts.get(2).getValues())
.hasFieldOrPropertyWithValue("timestamp", null)
.hasFieldOrPropertyWithValue("count", 1);
}
} }

View File

@ -1,143 +0,0 @@
/*
* 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.savedfilters;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for all saved filter processes
*******************************************************************************/
class SavedFilterProcessTests extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QInstance qInstance = QContext.getQInstance();
new SavedFiltersMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
{
///////////////////////////////////////////
// query - should be no filters to start //
///////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertEquals(0, ((List<?>) runProcessOutput.getValues().get("savedFilterList")).size());
}
Integer savedFilterId;
{
////////////////////////
// store a new filter //
////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName());
runProcessInput.addValue("label", "My Filter");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedFilterList = (List<QRecord>) runProcessOutput.getValues().get("savedFilterList");
assertEquals(1, savedFilterList.size());
savedFilterId = savedFilterList.get(0).getValueInteger("id");
assertNotNull(savedFilterId);
}
{
////////////////////////////////////
// query - should find our filter //
////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedFilterList = (List<QRecord>) runProcessOutput.getValues().get("savedFilterList");
assertEquals(1, savedFilterList.size());
assertEquals(1, savedFilterList.get(0).getValueInteger("id"));
assertEquals("My Filter", savedFilterList.get(0).getValueString("label"));
}
{
///////////////////////
// update our filter //
///////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName());
runProcessInput.addValue("id", savedFilterId);
runProcessInput.addValue("label", "My Updated Filter");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedFilterList = (List<QRecord>) runProcessOutput.getValues().get("savedFilterList");
assertEquals(1, savedFilterList.size());
assertEquals(1, savedFilterList.get(0).getValueInteger("id"));
assertEquals("My Updated Filter", savedFilterList.get(0).getValueString("label"));
}
{
///////////////////////
// delete our filter //
///////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(DeleteSavedFilterProcess.getProcessMetaData().getName());
runProcessInput.addValue("id", savedFilterId);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
}
{
////////////////////////////////////////
// query - should be no filters again //
////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertEquals(0, ((List<?>) runProcessOutput.getValues().get("savedFilterList")).size());
}
}
}

View File

@ -0,0 +1,189 @@
/*
* 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.savedviews;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
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.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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.savedviews.SavedViewsMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for all saved view processes
*******************************************************************************/
class SavedViewProcessTests extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QInstance qInstance = QContext.getQInstance();
new SavedViewsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
{
/////////////////////////////////////////
// query - should be no views to start //
/////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertEquals(0, ((List<?>) runProcessOutput.getValues().get("savedViewList")).size());
}
Integer savedViewId;
{
//////////////////////
// store a new view //
//////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("label", "My View");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size());
savedViewId = savedViewList.get(0).getValueInteger("id");
assertNotNull(savedViewId);
//////////////////////////////////////////////////////////////////
// try to store it again - should throw a "duplicate" exception //
//////////////////////////////////////////////////////////////////
assertThatThrownBy(() -> new RunProcessAction().execute(runProcessInput))
.isInstanceOf(QUserFacingException.class)
.hasMessageContaining("already have a saved view");
}
{
///////////////////////////////////
// query - should find our views //
///////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size());
assertEquals(1, savedViewList.get(0).getValueInteger("id"));
assertEquals("My View", savedViewList.get(0).getValueString("label"));
}
{
/////////////////////
// update our view //
/////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("id", savedViewId);
runProcessInput.addValue("label", "My Updated View");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size());
assertEquals(1, savedViewList.get(0).getValueInteger("id"));
assertEquals("My Updated View", savedViewList.get(0).getValueString("label"));
}
Integer anotherSavedViewId;
{
/////////////////////////////////////////////////////////////////////////////////////////////
// store a second one w/ different name (will be used below in update-dupe-check use-case) //
/////////////////////////////////////////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("label", "My Second View");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
anotherSavedViewId = savedViewList.get(0).getValueInteger("id");
}
{
/////////////////////////////////////////////////
// try to rename the second to match the first //
/////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("id", anotherSavedViewId);
runProcessInput.addValue("label", "My Updated View");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
//////////////////////////////////////////
// should throw a "duplicate" exception //
//////////////////////////////////////////
assertThatThrownBy(() -> new RunProcessAction().execute(runProcessInput))
.isInstanceOf(QUserFacingException.class)
.hasMessageContaining("already have a saved view");
}
{
//////////////////////
// delete our views //
//////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(DeleteSavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("id", savedViewId);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
runProcessInput.addValue("id", anotherSavedViewId);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
}
{
//////////////////////////////////////
// query - should be no views again //
//////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertEquals(0, ((List<?>) runProcessOutput.getValues().get("savedViewList")).size());
}
}
}

View File

@ -66,7 +66,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider; import com.kingsrook.qqq.backend.core.model.savedviews.SavedViewsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
@ -157,7 +157,7 @@ public class TestUtils
qInstance.addBackend(defineMemoryBackend()); qInstance.addBackend(defineMemoryBackend());
try try
{ {
new SavedFiltersMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); new SavedViewsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null);
new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null);
} }
catch(Exception e) catch(Exception e)