From c89fe958c366dcfcf8e1026ecd2ec2687ec3bac3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 14 Apr 2024 20:14:32 -0500 Subject: [PATCH] CE-1115 pre-QA commit on saved report UI, including: - adding pre-insert/update validation - move json field value formatting from post-query customizer to a FieldDisplayBehavior instead (works for audits this way :) --- .../core/actions/audits/DMLAuditAction.java | 11 +- .../reporting/GenerateReportAction.java | 21 +- .../model/savedreports/ReportColumns.java | 22 ++ ...dReportJsonFieldDisplayValueFormatter.java | 150 ++++++++++++ .../SavedReportTableCustomizer.java | 223 +++++++++++++++-- .../SavedReportsMetaDataProvider.java | 10 +- .../SavedReportToReportMetaDataAdapter.java | 11 +- ...ortJsonFieldDisplayValueFormatterTest.java | 166 +++++++++++++ .../SavedReportTableCustomizerTest.java | 228 ++++++++++++++++++ 9 files changed, 800 insertions(+), 42 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatterTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizerTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java index 3ab02ce4..044a24b7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java @@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction -1) { @@ -1097,5 +1097,22 @@ public class GenerateReportAction extends AbstractQActionFunction columns; + /******************************************************************************* ** Getter for columns *******************************************************************************/ @@ -45,6 +48,23 @@ public class ReportColumns implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public List extractVisibleColumns() + { + return CollectionUtils.nonNullList(getColumns()).stream() + ////////////////////////////////////////////////////// + // if isVisible is missing, we assume it to be true // + ////////////////////////////////////////////////////// + .filter(rc -> rc.getIsVisible() == null || rc.getIsVisible()) + .filter(rc -> StringUtils.hasContent(rc.getName())) + .filter(rc -> !rc.getName().startsWith("__check")) + .toList(); + } + + + /******************************************************************************* ** Setter for columns *******************************************************************************/ @@ -65,6 +85,7 @@ public class ReportColumns implements Serializable } + /******************************************************************************* ** Fluent setter to add 1 column *******************************************************************************/ @@ -79,6 +100,7 @@ public class ReportColumns implements Serializable } + /******************************************************************************* ** Fluent setter to add 1 column w/ just a name *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java new file mode 100644 index 00000000..cb1af39c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java @@ -0,0 +1,150 @@ +/* + * 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.model.savedreports; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition; +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.metadata.fields.FieldDisplayBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SavedReportJsonFieldDisplayValueFormatter implements FieldDisplayBehavior +{ + private static SavedReportJsonFieldDisplayValueFormatter savedReportJsonFieldDisplayValueFormatter = null; + + + + /******************************************************************************* + ** Singleton constructor + *******************************************************************************/ + private SavedReportJsonFieldDisplayValueFormatter() + { + + } + + + + /******************************************************************************* + ** Singleton accessor + *******************************************************************************/ + public static SavedReportJsonFieldDisplayValueFormatter getInstance() + { + if(savedReportJsonFieldDisplayValueFormatter == null) + { + savedReportJsonFieldDisplayValueFormatter = new SavedReportJsonFieldDisplayValueFormatter(); + } + return (savedReportJsonFieldDisplayValueFormatter); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public SavedReportJsonFieldDisplayValueFormatter getDefault() + { + return getInstance(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + for(QRecord record : CollectionUtils.nonNullList(recordList)) + { + if(field.getName().equals("queryFilterJson")) + { + String queryFilterJson = record.getValueString("queryFilterJson"); + if(StringUtils.hasContent(queryFilterJson)) + { + try + { + QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson); + int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size(); + record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount)); + } + catch(Exception e) + { + record.setDisplayValue("queryFilterJson", "Invalid Filter..."); + } + } + } + + if(field.getName().equals("columnsJson")) + { + String columnsJson = record.getValueString("columnsJson"); + if(StringUtils.hasContent(columnsJson)) + { + try + { + ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson); + int columnCount = reportColumns.extractVisibleColumns().size(); + + record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural(columnCount)); + } + catch(Exception e) + { + record.setDisplayValue("columnsJson", "Invalid Columns..."); + } + } + } + + if(field.getName().equals("pivotTableJson")) + { + String pivotTableJson = record.getValueString("pivotTableJson"); + if(StringUtils.hasContent(pivotTableJson)) + { + try + { + PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson); + int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size(); + int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size(); + int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size(); + record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount)); + } + catch(Exception e) + { + record.setDisplayValue("pivotTableJson", "Invalid Pivot Table..."); + } + } + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java index f66a6313..73679dcf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java @@ -22,17 +22,26 @@ package com.kingsrook.qqq.backend.core.model.savedreports; +import java.io.IOException; +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; +import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition; -import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -45,63 +54,229 @@ public class SavedReportTableCustomizer implements TableCustomizerInterface ** *******************************************************************************/ @Override - public List postQuery(QueryOrGetInputInterface queryInput, List records) throws QException + public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException + { + return (preInsertOrUpdate(records)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List preUpdate(UpdateInput updateInput, List records, boolean isPreview, Optional> oldRecordList) throws QException + { + return (preInsertOrUpdate(records)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List preInsertOrUpdate(List records) { for(QRecord record : CollectionUtils.nonNullList(records)) { + preValidateRecord(record); + } + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + void preValidateRecord(QRecord record) + { + try + { + String tableName = record.getValueString("tableName"); String queryFilterJson = record.getValueString("queryFilterJson"); String columnsJson = record.getValueString("columnsJson"); String pivotTableJson = record.getValueString("pivotTableJson"); + Set usedColumns = new HashSet<>(); + + QTableMetaData table = QContext.getQInstance().getTable(tableName); + if(table == null) + { + record.addError(new BadInputStatusMessage("Unrecognized table name: " + tableName)); + } + if(StringUtils.hasContent(queryFilterJson)) { try { - QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson); - int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size(); - record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount)); + //////////////////////////////////////////////////////////////// + // nothing to validate on filter, other than, we can parse it // + //////////////////////////////////////////////////////////////// + SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson); } - catch(Exception e) + catch(IOException e) { - record.setDisplayValue("queryFilterJson", "Invalid Filter..."); + record.addError(new BadInputStatusMessage("Unable to parse queryFilterJson: " + e.getMessage())); } } + boolean hadColumnParseError = false; if(StringUtils.hasContent(columnsJson)) { try { + ///////////////////////////////////////////////////////////////////////// + // make sure we can parse columns, and that we have at least 1 visible // + ///////////////////////////////////////////////////////////////////////// ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson); - long columnCount = CollectionUtils.nonNullList(reportColumns.getColumns()) - .stream().filter(rc -> BooleanUtils.isTrue(rc.getIsVisible())) - .count(); - - record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural((int) columnCount)); + for(ReportColumn column : reportColumns.extractVisibleColumns()) + { + usedColumns.add(column.getName()); + } } - catch(Exception e) + catch(IOException e) { - record.setDisplayValue("columnsJson", "Invalid Columns..."); + record.addError(new BadInputStatusMessage("Unable to parse columnsJson: " + e.getMessage())); + hadColumnParseError = true; } } + if(usedColumns.isEmpty() && !hadColumnParseError) + { + record.addError(new BadInputStatusMessage("A Report must contain at least 1 column")); + } + if(StringUtils.hasContent(pivotTableJson)) { try { - PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson); - int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size(); - int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size(); - int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size(); - record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount)); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure we can parse pivot table, and we have ... at least 1 ... row? maybe that's all that's needed // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson); + boolean anyRows = false; + boolean missingAnyFieldNamesInRows = false; + boolean missingAnyFieldNamesInColumns = false; + boolean missingAnyFieldNamesInValues = false; + boolean missingAnyFunctionsInValues = false; + + ////////////////// + // look at rows // + ////////////////// + for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotTableDefinition.getRows())) + { + anyRows = true; + if(StringUtils.hasContent(row.getFieldName())) + { + if(!usedColumns.contains(row.getFieldName()) && !hadColumnParseError) + { + record.addError(new BadInputStatusMessage("A pivot table row is using field (" + getFieldLabelElseName(table, row.getFieldName()) + ") which is not an active column on this report.")); + } + } + else + { + missingAnyFieldNamesInRows = true; + } + } + + if(!anyRows) + { + record.addError(new BadInputStatusMessage("A Pivot Table must contain at least 1 row")); + } + + ///////////////////// + // look at columns // + ///////////////////// + for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotTableDefinition.getColumns())) + { + if(StringUtils.hasContent(column.getFieldName())) + { + if(!usedColumns.contains(column.getFieldName()) && !hadColumnParseError) + { + record.addError(new BadInputStatusMessage("A pivot table column is using field (" + getFieldLabelElseName(table, column.getFieldName()) + ") which is not an active column on this report.")); + } + } + else + { + missingAnyFieldNamesInColumns = true; + } + } + + //////////////////// + // look at values // + //////////////////// + for(PivotTableValue value : CollectionUtils.nonNullList(pivotTableDefinition.getValues())) + { + if(StringUtils.hasContent(value.getFieldName())) + { + if(!usedColumns.contains(value.getFieldName()) && !hadColumnParseError) + { + record.addError(new BadInputStatusMessage("A pivot table value is using field (" + getFieldLabelElseName(table, value.getFieldName()) + ") which is not an active column on this report.")); + } + } + else + { + missingAnyFieldNamesInValues = true; + } + + if(value.getFunction() == null) + { + missingAnyFunctionsInValues = true; + } + } + + //////////////////////////////////////////////// + // errors based on missing things found above // + //////////////////////////////////////////////// + if(missingAnyFieldNamesInRows) + { + record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table row.")); + } + + if(missingAnyFieldNamesInColumns) + { + record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table column.")); + } + + if(missingAnyFieldNamesInValues) + { + record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table value.")); + } + + if(missingAnyFunctionsInValues) + { + record.addError(new BadInputStatusMessage("Missing function for at least one pivot table value.")); + } } - catch(Exception e) + catch(IOException e) { - record.setDisplayValue("pivotTableJson", "Invalid Pivot Table..."); + record.addError(new BadInputStatusMessage("Unable to parse pivotTableJson: " + e.getMessage())); } } } + catch(Exception e) + { + LOG.warn("Error validating a savedReport"); + } + } - return (records); + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getFieldLabelElseName(QTableMetaData table, String fieldName) + { + try + { + GenerateReportAction.FieldAndJoinTable fieldAndJoinTable = GenerateReportAction.getFieldAndJoinTable(table, fieldName); + return (fieldAndJoinTable.getLabel(table)); + } + catch(Exception e) + { + return (fieldName); + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java index 16d09754..ec088bfa 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java @@ -30,6 +30,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; +import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; @@ -147,6 +149,7 @@ public class SavedReportsMetaDataProvider .withBackendName(backendName) .withPrimaryKeyField("id") .withFieldsFromEntity(SavedReport.class) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD)) .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName"))) .withSection(new QFieldSection("filtersAndColumns", new QIcon().withName("table_chart"), Tier.T2).withLabel("Filters and Columns").withWidgetName("reportSetupWidget")) .withSection(new QFieldSection("pivotTable", new QIcon().withName("pivot_table_chart"), Tier.T2).withLabel("Pivot Table").withWidgetName("pivotTableSetupWidget")) @@ -154,7 +157,12 @@ public class SavedReportsMetaDataProvider .withSection(new QFieldSection("hidden", new QIcon().withName("text_snippet"), Tier.T2, List.of("inputFieldsJson", "userId")).withIsHidden(true)) .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); - table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(SavedReportTableCustomizer.class)); + table.getField("queryFilterJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance()); + table.getField("columnsJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance()); + table.getField("pivotTableJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance()); + + table.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SavedReportTableCustomizer.class)); + table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(SavedReportTableCustomizer.class)); if(backendDetailEnricher != null) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java index 0d09967d..55ab7270 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java @@ -57,7 +57,6 @@ 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.ListBuilder; -import org.apache.commons.lang.BooleanUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -122,16 +121,8 @@ public class SavedReportToReportMetaDataAdapter Set neededJoinTables = new HashSet<>(); - for(ReportColumn column : columnsObject.getColumns()) + for(ReportColumn column : columnsObject.extractVisibleColumns()) { - //////////////////////////////////////////////////////////////////////////////////////////////////// - // if isVisible is missing, we assume it to be true - so only if it isFalse do we skip the column // - //////////////////////////////////////////////////////////////////////////////////////////////////// - if(BooleanUtils.isFalse(column.getIsVisible())) - { - continue; - } - //////////////////////////////////////////////////// // figure out the field being named by the column // //////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatterTest.java new file mode 100644 index 00000000..4f00be6e --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatterTest.java @@ -0,0 +1,166 @@ +/* + * 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.model.savedreports; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue; +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.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for SavedReportJsonFieldDisplayValueFormatter + *******************************************************************************/ +class SavedReportJsonFieldDisplayValueFormatterTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws QException + { + QContext.getQInstance().add(new SavedReportsMetaDataProvider().defineSavedReportTable(TestUtils.MEMORY_BACKEND_NAME, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostQuery() throws QException + { + UnsafeFunction customize = savedReport -> + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(SavedReport.TABLE_NAME); + + QRecord record = savedReport.toQRecord(); + + for(String fieldName : List.of("queryFilterJson", "columnsJson", "pivotTableJson")) + { + SavedReportJsonFieldDisplayValueFormatter.getInstance().apply(ValueBehaviorApplier.Action.FORMATTING, List.of(record), qInstance, table, table.getField(fieldName)); + } + + return (record); + }; + + { + QRecord record = customize.apply(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter())) + .withColumnsJson(JsonUtils.toJson(new ReportColumns())) + .withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition()))); + + assertEquals("0 Filters", record.getDisplayValue("queryFilterJson")); + assertEquals("0 Columns", record.getDisplayValue("columnsJson")); + assertEquals("0 Rows, 0 Columns, and 0 Values", record.getDisplayValue("pivotTableJson")); + } + + { + QRecord record = customize.apply(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter())) + .withColumnsJson(JsonUtils.toJson(new ReportColumns()))); + + assertEquals("0 Filters", record.getDisplayValue("queryFilterJson")); + assertEquals("0 Columns", record.getDisplayValue("columnsJson")); + assertNull(record.getDisplayValue("pivotTableJson")); + } + + { + QRecord record = customize.apply(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter() + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IS_NOT_BLANK)))) + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn(new ReportColumn().withName("birthDate")))) + .withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition() + .withRow(new PivotTableGroupBy()) + .withValue(new PivotTableValue()) + ))); + + assertEquals("1 Filter", record.getDisplayValue("queryFilterJson")); + assertEquals("1 Column", record.getDisplayValue("columnsJson")); + assertEquals("1 Row, 0 Columns, and 1 Value", record.getDisplayValue("pivotTableJson")); + } + + { + QRecord record = customize.apply(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter() + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, 1)) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IS_NOT_BLANK)))) + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn(new ReportColumn().withName("__check__").withIsVisible(true)) + .withColumn(new ReportColumn().withName("id")) + .withColumn(new ReportColumn().withName("firstName").withIsVisible(true)) + .withColumn(new ReportColumn().withName("lastName").withIsVisible(false)) + .withColumn(new ReportColumn().withName("birthDate")))) + .withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition() + .withRow(new PivotTableGroupBy()) + .withRow(new PivotTableGroupBy()) + .withColumn(new PivotTableGroupBy()) + .withValue(new PivotTableValue()) + .withValue(new PivotTableValue()) + .withValue(new PivotTableValue()) + ))); + + assertEquals("2 Filters", record.getDisplayValue("queryFilterJson")); + assertEquals("3 Columns", record.getDisplayValue("columnsJson")); + assertEquals("2 Rows, 1 Column, and 3 Values", record.getDisplayValue("pivotTableJson")); + } + + { + QRecord record = customize.apply(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson("blah") + .withColumnsJson("") + .withPivotTableJson("{]")); + + assertEquals("Invalid Filter...", record.getDisplayValue("queryFilterJson")); + assertEquals("Invalid Columns...", record.getDisplayValue("columnsJson")); + assertEquals("Invalid Pivot Table...", record.getDisplayValue("pivotTableJson")); + } + + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizerTest.java new file mode 100644 index 00000000..6bf5d188 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizerTest.java @@ -0,0 +1,228 @@ +/* + * 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.model.savedreports; + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +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.model.actions.reporting.pivottable.PivotTableDefinition; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableFunction; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy; +import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue; +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.QQueryFilter; +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.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for SavedReportTableCustomizer + *******************************************************************************/ +class SavedReportTableCustomizerTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws QException + { + QContext.getQInstance().add(new SavedReportsMetaDataProvider().defineSavedReportTable(TestUtils.MEMORY_BACKEND_NAME, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPreInsertAndPreUpdateAreWired() throws QException + { + SavedReport badRecord = new SavedReport() + .withLabel("My Report") + .withTableName("notATable"); + + ///////////////////////////////////////////////////////////////////// + // assertions to apply both to a failed insert and a failed update // + ///////////////////////////////////////////////////////////////////// + Consumer asserter = record -> assertThat(record.getErrors()) + .hasSizeGreaterThanOrEqualTo(2) + .anyMatch(e -> e.getMessage().contains("Unrecognized table name")) + .anyMatch(e -> e.getMessage().contains("must contain at least 1 column")); + + //////////////////////////////////////////////////////////// + // go through insert action, to ensure wired-up correctly // + //////////////////////////////////////////////////////////// + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(badRecord)); + asserter.accept(insertOutput.getRecords().get(0)); + + //////////////////////////////// + // likewise for update action // + //////////////////////////////// + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(SavedReport.TABLE_NAME).withRecordEntity(badRecord)); + asserter.accept(updateOutput.getRecords().get(0)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testParseFails() + { + QRecord record = new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson("...") + .withColumnsJson("x") + .withPivotTableJson("[") + .toQRecord(); + + new SavedReportTableCustomizer().preValidateRecord(record); + + assertThat(record.getErrors()) + .hasSize(3) + .anyMatch(e -> e.getMessage().contains("Unable to parse queryFilterJson")) + .anyMatch(e -> e.getMessage().contains("Unable to parse columnsJson")) + .anyMatch(e -> e.getMessage().contains("Unable to parse pivotTableJson")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNoColumns() + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // given a reportColumns object, serialize it to json, put it in a saved report record, and run the pre-validator // + // then assert we got error saying there were no columns. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Consumer asserter = reportColumns -> + { + SavedReport savedReport = new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter())) + .withColumnsJson(JsonUtils.toJson(reportColumns)); + + QRecord record = savedReport.toQRecord(); + new SavedReportTableCustomizer().preValidateRecord(record); + + assertThat(record.getErrors()) + .hasSize(1) + .anyMatch(e -> e.getMessage().contains("must contain at least 1 column")); + }; + + asserter.accept(new ReportColumns()); + asserter.accept(new ReportColumns().withColumns(null)); + asserter.accept(new ReportColumns().withColumns(new ArrayList<>())); + asserter.accept(new ReportColumns().withColumn(new ReportColumn() + .withName("id").withIsVisible(false))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPivotTables() + { + BiConsumer> asserter = (PivotTableDefinition ptd, List expectedAnyMessageToContain) -> + { + SavedReport savedReport = new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter())) + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("firstName") + .withColumn("lastName") + .withColumn("birthDate"))) + .withPivotTableJson(JsonUtils.toJson(ptd)); + + QRecord record = savedReport.toQRecord(); + new SavedReportTableCustomizer().preValidateRecord(record); + + assertThat(record.getErrors()).hasSize(expectedAnyMessageToContain.size()); + + for(String expected : expectedAnyMessageToContain) + { + assertThat(record.getErrors()) + .anyMatch(e -> e.getMessage().contains(expected)); + } + }; + + asserter.accept(new PivotTableDefinition(), List.of("must contain at least 1 row")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withRow(new PivotTableGroupBy()), + List.of("Missing field name for at least one pivot table row")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withRow(new PivotTableGroupBy().withFieldName("createDate")), + List.of("row is using field (Create Date) which is not an active column")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withColumn(new PivotTableGroupBy()), + List.of("Missing field name for at least one pivot table column")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withColumn(new PivotTableGroupBy().withFieldName("createDate")), + List.of("column is using field (Create Date) which is not an active column")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withValue(new PivotTableValue().withFunction(PivotTableFunction.SUM)), + List.of("Missing field name for at least one pivot table value")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withValue(new PivotTableValue().withFieldName("createDate").withFunction(PivotTableFunction.SUM)), + List.of("value is using field (Create Date) which is not an active column")); + + asserter.accept(new PivotTableDefinition() + .withRow(new PivotTableGroupBy().withFieldName("id")) + .withValue(new PivotTableValue().withFieldName("firstName")), + List.of("Missing function for at least one pivot table value")); + } + + +} \ No newline at end of file