diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java
index 7dbe05ca..45cce9cd 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java
@@ -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.TableAutomationAction;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.apache.commons.lang.NotImplementedException;
+import org.json.JSONObject;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -387,13 +388,15 @@ public class PollingAutomationPerTableRunner implements Runnable
if(filterId != null)
{
GetInput getInput = new GetInput();
- getInput.setTableName(SavedFilter.TABLE_NAME);
+ getInput.setTableName(SavedView.TABLE_NAME);
getInput.setPrimaryKey(filterId);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
- SavedFilter savedFilter = new SavedFilter(getOutput.getRecord());
- filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class);
+ SavedView savedView = new SavedView(getOutput.getRecord());
+ JSONObject viewJson = new JSONObject(savedView.getViewJson());
+ JSONObject queryFilter = viewJson.getJSONObject("queryFilter");
+ filter = JsonUtils.toObject(queryFilter.toString(), QQueryFilter.class);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/DateTimeGroupBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/DateTimeGroupBy.java
index f69596af..2cc0ba81 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/DateTimeGroupBy.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/DateTimeGroupBy.java
@@ -297,4 +297,21 @@ public enum DateTimeGroupBy
ZonedDateTime zoned = instant.atZone(zoneId);
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;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index 43d496fb..624c22e3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -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.ExposedJoin;
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.Tier;
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.StringUtils;
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);
validateTableAssociations(qInstance, 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;
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java
index a3c82b4f..e8b04e65 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java
@@ -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.QRecordEntity;
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;
@@ -51,7 +51,7 @@ public class TableTrigger extends QRecordEntity
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName;
- @QField(possibleValueSourceName = SavedFilter.TABLE_NAME)
+ @QField(possibleValueSourceName = SavedView.TABLE_NAME)
private Integer filterId;
@QField(possibleValueSourceName = Script.TABLE_NAME)
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java
index d0dc48e4..6c36388b 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java
@@ -22,6 +22,7 @@
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;
@@ -69,4 +70,16 @@ public abstract class QSupplementalTableMetaData
// noop in base class //
////////////////////////
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
+ {
+ ////////////////////////
+ // noop in base class //
+ ////////////////////////
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedView.java
similarity index 89%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedView.java
index 9263b5d5..fe382185 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedView.java
@@ -1,6 +1,6 @@
/*
* 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
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.model.savedfilters;
+package com.kingsrook.qqq.backend.core.model.savedviews;
import java.time.Instant;
@@ -32,9 +32,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
** 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)
private Integer id;
@@ -55,7 +55,7 @@ public class SavedFilter extends QRecordEntity
private String userId;
@QField(isEditable = false)
- private String filterJson;
+ private String viewJson;
@@ -63,7 +63,7 @@ public class SavedFilter extends QRecordEntity
** Constructor
**
*******************************************************************************/
- public SavedFilter()
+ public SavedView()
{
}
@@ -73,7 +73,7 @@ public class SavedFilter extends QRecordEntity
** Constructor
**
*******************************************************************************/
- public SavedFilter(QRecord qRecord) throws QException
+ public SavedView(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
@@ -172,7 +172,7 @@ public class SavedFilter extends QRecordEntity
** Fluent setter for label
**
*******************************************************************************/
- public SavedFilter withLabel(String label)
+ public SavedView withLabel(String label)
{
this.label = label;
return (this);
@@ -206,7 +206,7 @@ public class SavedFilter extends QRecordEntity
** Fluent setter for tableName
**
*******************************************************************************/
- public SavedFilter withTableName(String tableName)
+ public SavedView withTableName(String tableName)
{
this.tableName = tableName;
return (this);
@@ -240,7 +240,7 @@ public class SavedFilter extends QRecordEntity
** Fluent setter for userId
**
*******************************************************************************/
- public SavedFilter withUserId(String userId)
+ public SavedView withUserId(String userId)
{
this.userId = userId;
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);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java
similarity index 60%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java
index 0347db77..2581e67d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java
@@ -1,6 +1,6 @@
/*
* 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
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,25 +19,31 @@
* along with this program. If not, see .
*/
-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 com.kingsrook.qqq.backend.core.exceptions.QException;
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.QPossibleValueSource;
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.processes.implementations.savedfilters.DeleteSavedFilterProcess;
-import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.QuerySavedFilterProcess;
-import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.StoreSavedFilterProcess;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
+import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.DeleteSavedViewProcess;
+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 backendDetailEnricher) throws QException
{
- instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher));
- instance.addPossibleValueSource(defineSavedFilterPossibleValueSource());
- instance.addProcess(QuerySavedFilterProcess.getProcessMetaData());
- instance.addProcess(StoreSavedFilterProcess.getProcessMetaData());
- instance.addProcess(DeleteSavedFilterProcess.getProcessMetaData());
+ instance.addTable(defineSavedViewTable(backendName, backendDetailEnricher));
+ instance.addPossibleValueSource(defineSavedViewPossibleValueSource());
+ instance.addProcess(QuerySavedViewProcess.getProcessMetaData());
+ instance.addProcess(StoreSavedViewProcess.getProcessMetaData());
+ instance.addProcess(DeleteSavedViewProcess.getProcessMetaData());
}
@@ -58,16 +64,21 @@ public class SavedFiltersMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
- private QTableMetaData defineSavedFilterTable(String backendName, Consumer backendDetailEnricher) throws QException
+ public QTableMetaData defineSavedViewTable(String backendName, Consumer backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
- .withName(SavedFilter.TABLE_NAME)
- .withLabel("Saved Filter")
+ .withName(SavedView.TABLE_NAME)
+ .withLabel("Saved View")
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withBackendName(backendName)
.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)
{
@@ -82,12 +93,12 @@ public class SavedFiltersMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
- private QPossibleValueSource defineSavedFilterPossibleValueSource()
+ private QPossibleValueSource defineSavedViewPossibleValueSource()
{
return new QPossibleValueSource()
- .withName(SavedFilter.TABLE_NAME)
+ .withName(SavedView.TABLE_NAME)
.withType(QPossibleValueSourceType.TABLE)
- .withTableName(SavedFilter.TABLE_NAME)
+ .withTableName(SavedView.TABLE_NAME)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY)
.withOrderByField("label");
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
index 4685b7e3..890d7c7f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
@@ -24,6 +24,10 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.io.Serializable;
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.Collection;
import java.util.Collections;
@@ -35,6 +39,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
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.context.QContext;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -577,7 +583,11 @@ public class MemoryRecordStore
for(GroupBy groupBy : groupBys)
{
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);
}
@@ -629,7 +639,9 @@ public class MemoryRecordStore
/////////////////////
if(aggregateInput.getFilter() != null && CollectionUtils.nullSafeHasContents(aggregateInput.getFilter().getOrderBys()))
{
- Comparator comparator = null;
+ /////////////////////////////////////////////////////////////////////////////////////
+ // lambda to compare 2 serializables, as we'll assume (& cast) them to Comparables //
+ /////////////////////////////////////////////////////////////////////////////////////
Comparator serializableComparator = (Serializable a, Serializable b) ->
{
if(a == null && b == null)
@@ -647,9 +659,15 @@ public class MemoryRecordStore
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 reverseSerializableComparator = (Serializable a, Serializable b) -> -serializableComparator.compare(a, b);
+
////////////////////////////////////////////////
// build a comparator out of all the orderBys //
////////////////////////////////////////////////
+ Comparator comparator = null;
for(QFilterOrderBy orderBy : aggregateInput.getFilter().getOrderBys())
{
Function keyExtractor = aggregateResult ->
@@ -670,16 +688,11 @@ public class MemoryRecordStore
if(comparator == null)
{
- comparator = Comparator.comparing(keyExtractor, serializableComparator);
+ comparator = Comparator.comparing(keyExtractor, orderBy.getIsAscending() ? serializableComparator : reverseSerializableComparator);
}
else
{
- comparator = comparator.thenComparing(keyExtractor, serializableComparator);
- }
-
- if(!orderBy.getIsAscending())
- {
- comparator = comparator.reversed();
+ comparator = comparator.thenComparing(keyExtractor, orderBy.getIsAscending() ? serializableComparator : reverseSerializableComparator);
}
}
@@ -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));
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java
index bfeb47f4..39dbd7f1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java
@@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.processes.implementations.columnstats;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -173,11 +175,10 @@ public class ColumnStatsStep implements BackendStep
Aggregate aggregate = new Aggregate(table.getPrimaryKeyField(), AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL);
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))
{
- // groupBy = new GroupBy(field.getType(), fieldName, "DATE(%s)");
- String sqlExpression = DateTimeGroupBy.HOUR.getSqlExpression();
+ String sqlExpression = DateTimeGroupBy.HOUR.getSqlExpression(ZoneId.systemDefault());
groupBy = new GroupBy(QFieldType.STRING, fieldName, sqlExpression);
}
@@ -230,6 +231,12 @@ public class ColumnStatsStep implements BackendStep
for(AggregateResult result : aggregateOutput.getResults())
{
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));
valueCounts.add(new QRecord().withValue(fieldName, value).withValue("count", count));
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/DeleteSavedViewProcess.java
similarity index 82%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/DeleteSavedViewProcess.java
index a0a6f1f8..8ff2fc06 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/DeleteSavedViewProcess.java
@@ -1,6 +1,6 @@
/*
* 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
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters;
+package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
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.processes.QBackendStepMetaData;
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()
{
return (new QProcessMetaData()
- .withName("deleteSavedFilter")
+ .withName("deleteSavedView")
.withStepList(List.of(
new QBackendStepMetaData()
- .withCode(new QCodeReference(DeleteSavedFilterProcess.class))
+ .withCode(new QCodeReference(DeleteSavedViewProcess.class))
.withName("delete")
)));
}
@@ -72,16 +72,16 @@ public class DeleteSavedFilterProcess implements BackendStep
try
{
- Integer savedFilterId = runBackendStepInput.getValueInteger("id");
+ Integer savedViewId = runBackendStepInput.getValueInteger("id");
DeleteInput input = new DeleteInput();
- input.setTableName(SavedFilter.TABLE_NAME);
- input.setPrimaryKeys(List.of(savedFilterId));
+ input.setTableName(SavedView.TABLE_NAME);
+ input.setPrimaryKeys(List.of(savedViewId));
new DeleteAction().execute(input);
}
catch(Exception e)
{
- LOG.warn("Error deleting saved filter", e);
+ LOG.warn("Error deleting saved view", e);
throw (e);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/QuerySavedViewProcess.java
similarity index 81%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/QuerySavedViewProcess.java
index dc50ed17..f4f57516 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/QuerySavedViewProcess.java
@@ -1,6 +1,6 @@
/*
* 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
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters;
+package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
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.processes.QBackendStepMetaData;
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()
{
return (new QProcessMetaData()
- .withName("querySavedFilter")
+ .withName("querySavedView")
.withStepList(List.of(
new QBackendStepMetaData()
- .withCode(new QCodeReference(QuerySavedFilterProcess.class))
+ .withCode(new QCodeReference(QuerySavedViewProcess.class))
.withName("query")
)));
}
@@ -81,36 +81,36 @@ public class QuerySavedFilterProcess implements BackendStep
try
{
- Integer savedFilterId = runBackendStepInput.getValueInteger("id");
- if(savedFilterId != null)
+ Integer savedViewId = runBackendStepInput.getValueInteger("id");
+ if(savedViewId != null)
{
GetInput input = new GetInput();
- input.setTableName(SavedFilter.TABLE_NAME);
- input.setPrimaryKey(savedFilterId);
+ input.setTableName(SavedView.TABLE_NAME);
+ input.setPrimaryKey(savedViewId);
GetOutput output = new GetAction().execute(input);
runBackendStepOutput.addRecord(output.getRecord());
- runBackendStepOutput.addValue("savedFilter", output.getRecord());
- runBackendStepOutput.addValue("savedFilterList", (Serializable) List.of(output.getRecord()));
+ runBackendStepOutput.addValue("savedView", output.getRecord());
+ runBackendStepOutput.addValue("savedViewList", (Serializable) List.of(output.getRecord()));
}
else
{
String tableName = runBackendStepInput.getValueString("tableName");
QueryInput input = new QueryInput();
- input.setTableName(SavedFilter.TABLE_NAME);
+ input.setTableName(SavedView.TABLE_NAME);
input.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName))
.withOrderBy(new QFilterOrderBy("label")));
QueryOutput output = new QueryAction().execute(input);
runBackendStepOutput.setRecords(output.getRecords());
- runBackendStepOutput.addValue("savedFilterList", (Serializable) output.getRecords());
+ runBackendStepOutput.addValue("savedViewList", (Serializable) output.getRecords());
}
}
catch(Exception e)
{
- LOG.warn("Error deleting saved filter", e);
+ LOG.warn("Error querying for saved views", e);
throw (e);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/StoreSavedViewProcess.java
similarity index 55%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/StoreSavedViewProcess.java
index 37bc167e..26974f55 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/StoreSavedViewProcess.java
@@ -1,6 +1,6 @@
/*
* 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
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,37 +19,45 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters;
+package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
import java.io.Serializable;
-import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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.QueryAction;
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.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.UpdateOutput;
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.processes.QBackendStepMetaData;
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()
{
return (new QProcessMetaData()
- .withName("storeSavedFilter")
+ .withName("storeSavedView")
.withStepList(List.of(
new QBackendStepMetaData()
- .withCode(new QCodeReference(StoreSavedFilterProcess.class))
+ .withCode(new QCodeReference(StoreSavedViewProcess.class))
.withName("store")
)));
}
@@ -79,39 +87,73 @@ public class StoreSavedFilterProcess implements BackendStep
try
{
+ String userId = QContext.getQSession().getUser().getIdReference();
+ String tableName = runBackendStepInput.getValueString("tableName");
+ String label = runBackendStepInput.getValueString("label");
+
QRecord qRecord = new QRecord()
.withValue("id", runBackendStepInput.getValueInteger("id"))
- .withValue("label", runBackendStepInput.getValueString("label"))
- .withValue("tableName", runBackendStepInput.getValueString("tableName"))
- .withValue("filterJson", runBackendStepInput.getValueString("filterJson"))
- .withValue("userId", runBackendStepInput.getSession().getUser().getIdReference());
+ .withValue("viewJson", runBackendStepInput.getValueString("viewJson"))
+ .withValue("label", label)
+ .withValue("tableName", tableName)
+ .withValue("userId", userId);
- List savedFilterList = new ArrayList<>();
+ List savedViewList;
if(qRecord.getValueInteger("id") == null)
{
+ checkForDuplicates(userId, tableName, label, null);
+
InsertInput input = new InsertInput();
- input.setTableName(SavedFilter.TABLE_NAME);
+ input.setTableName(SavedView.TABLE_NAME);
input.setRecords(List.of(qRecord));
InsertOutput output = new InsertAction().execute(input);
- savedFilterList = output.getRecords();
+ savedViewList = output.getRecords();
}
else
{
+ checkForDuplicates(userId, tableName, label, qRecord.getValueInteger("id"));
+
UpdateInput input = new UpdateInput();
- input.setTableName(SavedFilter.TABLE_NAME);
+ input.setTableName(SavedView.TABLE_NAME);
input.setRecords(List.of(qRecord));
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)
{
- LOG.warn("Error storing data saved filter", e);
+ LOG.warn("Error storing saved view", 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."));
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/lambdas/UnsafeLambda.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/lambdas/UnsafeLambda.java
new file mode 100644
index 00000000..f74ba61e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/lambdas/UnsafeLambda.java
@@ -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 .
+ */
+
+package com.kingsrook.qqq.backend.core.utils.lambdas;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+@FunctionalInterface
+public interface UnsafeLambda
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ void run() throws Exception;
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStepTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStepTest.java
index d2b6fa0b..1e59efd9 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStepTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStepTest.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.columnstats;
import java.io.Serializable;
import java.math.BigDecimal;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
@@ -91,4 +92,50 @@ class ColumnStatsStepTest extends BaseTest
.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 values = output.getValues();
+
+ @SuppressWarnings("unchecked")
+ List valueCounts = (List) 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);
+ }
+
}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java
deleted file mode 100644
index d3c0bf5b..00000000
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java
+++ /dev/null
@@ -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 .
- */
-
-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 savedFilterList = (List) 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 savedFilterList = (List) 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 savedFilterList = (List) 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());
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/SavedViewProcessTests.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/SavedViewProcessTests.java
new file mode 100644
index 00000000..f4474bef
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedviews/SavedViewProcessTests.java
@@ -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 .
+ */
+
+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 savedViewList = (List) 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 savedViewList = (List) 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 savedViewList = (List) 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 savedViewList = (List) 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());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
index 0245e235..430f72ae 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -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.tables.AssociatedScript;
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.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
@@ -157,7 +157,7 @@ public class TestUtils
qInstance.addBackend(defineMemoryBackend());
try
{
- new SavedFiltersMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null);
+ new SavedViewsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null);
new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null);
}
catch(Exception e)