diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumn.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumn.java new file mode 100644 index 00000000..44518fe2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumn.java @@ -0,0 +1,98 @@ +/* + * 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.io.Serializable; + + +/******************************************************************************* + ** single entry in ReportColumns object - as part of SavedReport + *******************************************************************************/ +public class ReportColumn implements Serializable +{ + private String name; + private Boolean isVisible; + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public ReportColumn withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for isVisible + *******************************************************************************/ + public Boolean getIsVisible() + { + return (this.isVisible); + } + + + + /******************************************************************************* + ** Setter for isVisible + *******************************************************************************/ + public void setIsVisible(Boolean isVisible) + { + this.isVisible = isVisible; + } + + + + /******************************************************************************* + ** Fluent setter for isVisible + *******************************************************************************/ + public ReportColumn withIsVisible(Boolean isVisible) + { + this.isVisible = isVisible; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumns.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumns.java new file mode 100644 index 00000000..3d05c9d9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumns.java @@ -0,0 +1,95 @@ +/* + * 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.io.Serializable; +import java.util.ArrayList; +import java.util.List; + + +/******************************************************************************* + ** type of object expected to be in the SavedReport columnsJSON field + *******************************************************************************/ +public class ReportColumns implements Serializable +{ + private List columns; + + + /******************************************************************************* + ** Getter for columns + *******************************************************************************/ + public List getColumns() + { + return (this.columns); + } + + + + /******************************************************************************* + ** Setter for columns + *******************************************************************************/ + public void setColumns(List columns) + { + this.columns = columns; + } + + + + /******************************************************************************* + ** Fluent setter for columns + *******************************************************************************/ + public ReportColumns withColumns(List columns) + { + this.columns = columns; + return (this); + } + + + /******************************************************************************* + ** Fluent setter to add 1 column + *******************************************************************************/ + public ReportColumns withColumn(ReportColumn column) + { + if(this.columns == null) + { + this.columns = new ArrayList<>(); + } + this.columns.add(column); + return (this); + } + + + /******************************************************************************* + ** Fluent setter to add 1 column w/ just a name + *******************************************************************************/ + public ReportColumns withColumn(String name) + { + if(this.columns == null) + { + this.columns = new ArrayList<>(); + } + this.columns.add(new ReportColumn().withName(name)); + return (this); + } + +} 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 431349ef..751f20d7 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 @@ -25,9 +25,8 @@ package com.kingsrook.qqq.backend.core.processes.implementations.savedreports; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -43,12 +42,15 @@ 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.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumn; +import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns; import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; 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.ValueUtils; 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; @@ -73,16 +75,15 @@ public class SavedReportToReportMetaDataAdapter QReportMetaData reportMetaData = new QReportMetaData(); reportMetaData.setLabel(savedReport.getLabel()); - //////////////////////////// - // set up the data-source // - //////////////////////////// + ///////////////////////////////////////////////////// + // set up the data-source - e.g., table and filter // + ///////////////////////////////////////////////////// QReportDataSource dataSource = new QReportDataSource(); reportMetaData.setDataSources(List.of(dataSource)); dataSource.setName("main"); QTableMetaData table = qInstance.getTable(savedReport.getTableName()); dataSource.setSourceTable(savedReport.getTableName()); - dataSource.setQueryFilter(JsonUtils.toObject(savedReport.getQueryFilterJson(), QQueryFilter.class)); ////////////////////////// @@ -96,69 +97,41 @@ public class SavedReportToReportMetaDataAdapter view.setLabel(savedReport.getLabel()); // todo eh? view.setIncludeHeaderRow(true); - // don't need: - // view.setOrderByFields(); - only used for summary reports - // view.setTitleFormat(); - not using at this time - // view.setTitleFields(); - not using at this time - // view.setRecordTransformStep(); - // view.setViewCustomizer(); - - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - // columns in the saved-report look like a JSON object, w/ a key "columns", which is an array of objects // - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - Set neededJoinTables = new HashSet<>(); + //////////////////////////////////////////////////////////////////////////////////////////////// + // columns in the saved-report should look like a serialized version of ReportColumns object // + // map them to a list of QReportField objects // + // also keep track of what joinTables we find that we need to select // + //////////////////////////////////////////////////////////////////////////////////////////////// + ReportColumns columnsObject = JsonUtils.toObject(savedReport.getColumnsJson(), ReportColumns.class, om -> om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)); List reportColumns = new ArrayList<>(); view.setColumns(reportColumns); - Map columnsObject = JsonUtils.toObject(savedReport.getColumnsJson(), new TypeReference<>() {}); - List> columns = (List>) columnsObject.get("columns"); - for(Map column : columns) + Set neededJoinTables = new HashSet<>(); + + for(ReportColumn column : columnsObject.getColumns()) { - if(column.containsKey("isVisible") && !"true".equals(ValueUtils.getValueAsString(column.get("isVisible")))) + //////////////////////////////////////////////////////////////////////////////////////////////////// + // 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; } - QFieldMetaData field; - String fieldName = ValueUtils.getValueAsString(column.get("name")); - if(fieldName.contains(".")) + //////////////////////////////////////////////////// + // figure out the field being named by the column // + //////////////////////////////////////////////////// + String fieldName = ValueUtils.getValueAsString(column.getName()); + QFieldMetaData field = getField(savedReport, fieldName, qInstance, neededJoinTables, table); + if(field == null) { - String joinTableName = fieldName.replaceAll("\\..*", ""); - String joinFieldName = fieldName.replaceAll(".*\\.", ""); - - QTableMetaData joinTable = qInstance.getTable(joinTableName); - if(joinTable == null) - { - LOG.warn("Saved Report has an unrecognized join table name", logPair("savedReportId", savedReport.getId()), logPair("joinTable", joinTable), logPair("fieldName", fieldName)); - continue; - } - - neededJoinTables.add(joinTableName); - - field = joinTable.getFields().get(joinFieldName); - if(field == null) - { - LOG.warn("Saved Report has an unrecognized join field name", logPair("savedReportId", savedReport.getId()), logPair("fieldName", fieldName)); - continue; - } - } - else - { - field = table.getFields().get(fieldName); - if(field == null) - { - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - // frontend may often pass __checked__ (or maybe other __ prefixes in the future - so - don't warn that. // - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(!fieldName.startsWith("__")) - { - LOG.warn("Saved Report has an unexpected unrecognized field name", logPair("savedReportId", savedReport.getId()), logPair("table", table.getName()), logPair("fieldName", fieldName)); - } - continue; - } + continue; } + ////////////////////////////////////////////////// + // make a QReportField based on the table field // + ////////////////////////////////////////////////// QReportField reportField = new QReportField(); reportColumns.add(reportField); @@ -192,9 +165,8 @@ public class SavedReportToReportMetaDataAdapter if(exposedJoin.getJoinPath().size() == 1) { - // this is similar logic that QFMD has - ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Note, this is similar logic (and comment) in QFMD ... // // todo - what about a join with a longer path? it would be nice to pass such joinNames through there too, // // but what, that would actually be multiple queryJoins? needs a fair amount of thought. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -213,7 +185,7 @@ public class SavedReportToReportMetaDataAdapter { QReportView pivotView = new QReportView(); reportMetaData.getViews().add(pivotView); - pivotView.setName("pivot"); // does this appear? + pivotView.setName("pivot"); pivotView.setType(ReportType.PIVOT); pivotView.setPivotTableSourceViewName(view.getName()); pivotView.setPivotTableDefinition(JsonUtils.toObject(savedReport.getPivotTableJson(), PivotTableDefinition.class)); @@ -240,4 +212,51 @@ public class SavedReportToReportMetaDataAdapter } } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QFieldMetaData getField(SavedReport savedReport, String fieldName, QInstance qInstance, Set neededJoinTables, QTableMetaData table) + { + QFieldMetaData field; + if(fieldName.contains(".")) + { + String joinTableName = fieldName.replaceAll("\\..*", ""); + String joinFieldName = fieldName.replaceAll(".*\\.", ""); + + QTableMetaData joinTable = qInstance.getTable(joinTableName); + if(joinTable == null) + { + LOG.warn("Saved Report has an unrecognized join table name", logPair("savedReportId", savedReport.getId()), logPair("joinTable", joinTable), logPair("fieldName", fieldName)); + return null; + } + + neededJoinTables.add(joinTableName); + + field = joinTable.getFields().get(joinFieldName); + if(field == null) + { + LOG.warn("Saved Report has an unrecognized join field name", logPair("savedReportId", savedReport.getId()), logPair("fieldName", fieldName)); + return null; + } + } + else + { + field = table.getFields().get(fieldName); + if(field == null) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // frontend may often pass __checked__ (or maybe other __ prefixes in the future - so - don't warn that. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(!fieldName.startsWith("__")) + { + LOG.warn("Saved Report has an unexpected unrecognized field name", logPair("savedReportId", savedReport.getId()), logPair("table", table.getName()), logPair("fieldName", fieldName)); + } + return null; + } + } + return field; + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java index c4fe7d71..8dc66bd9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java @@ -64,19 +64,24 @@ class RenderSavedReportProcessTest extends BaseTest String label = "Test Report"; + ////////////////////////////////////////////////////////////////////////////////////////// + // do columns json as a string, rather than a toJson'ed ReportColumns object, // + // to help verify that we don't choke on un-recognized properties (e.g., as QFMD sends) // + ////////////////////////////////////////////////////////////////////////////////////////// + String columnsJson = """ + {"columns":[ + {"name": "k"}, + {"name": "id"}, + {"name": "firstName", "isVisible": true}, + {"name": "lastName", "pinned": "left"}, + {"name": "createDate", "isVisible": false} + ]} + """; + QRecord savedReport = new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport() .withLabel(label) .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) - .withColumnsJson(""" - { - "columns": - [ - {"name": "id"}, - {"name": "firstName"}, - {"name": "lastName"} - ] - } - """) + .withColumnsJson(columnsJson) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter())) )).getRecords().get(0); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java index 0847c58d..d03c828e 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java @@ -51,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; 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.savedreports.ReportColumns; import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider; import com.kingsrook.qqq.backend.core.model.session.QSession; @@ -244,12 +245,10 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest { List lines = runSavedReportForCSV(new SavedReport() .withTableName(TestUtils.TABLE_NAME_ORDER) - .withColumnsJson(""" - {"columns":[ - {"name": "id"}, - {"name": "storeId"}, - {"name": "orderInstructions.instructions"} - ]}""") + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("storeId") + .withColumn("orderInstructions.instructions"))) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))); assertEquals(""" @@ -261,6 +260,7 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest } + /******************************************************************************* ** in here, by potentially ambiguous, we mean where there are possible joins ** between the order and orderInstructions tables. @@ -270,12 +270,10 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest { List lines = runSavedReportForCSV(new SavedReport() .withTableName(TestUtils.TABLE_NAME_ORDER) - .withColumnsJson(""" - {"columns":[ - {"name": "id"}, - {"name": "storeId"}, - {"name": "orderInstructions.instructions"} - ]}""") + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("storeId") + .withColumn("orderInstructions.instructions"))) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter() .withOrderBy(new QFilterOrderBy("orderInstructions.id", false)) ))); @@ -298,11 +296,9 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest { List lines = runSavedReportForCSV(new SavedReport() .withTableName(TestUtils.TABLE_NAME_ORDER) - .withColumnsJson(""" - {"columns":[ - {"name": "id"}, - {"name": "storeId"} - ]}""") + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("storeId"))) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter() .withCriteria(new QFilterCriteria("orderInstructions.instructions", QCriteriaOperator.CONTAINS, "v3")) ))); @@ -325,12 +321,10 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest { List lines = runSavedReportForCSV(new SavedReport() .withTableName(TestUtils.TABLE_NAME_ORDER) - .withColumnsJson(""" - {"columns":[ - {"name": "id"}, - {"name": "storeId"}, - {"name": "item.description"} - ]}""") + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("storeId") + .withColumn("item.description"))) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))); assertEquals(""" @@ -352,11 +346,9 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest { List lines = runSavedReportForCSV(new SavedReport() .withTableName(TestUtils.TABLE_NAME_ORDER) - .withColumnsJson(""" - {"columns":[ - {"name": "id"}, - {"name": "storeId"} - ]}""") + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("storeId"))) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter() .withCriteria(new QFilterCriteria("item.description", QCriteriaOperator.CONTAINS, "Item 7")) ))); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 8407541b..20fc10b1 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns; import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.SleepUtils; @@ -1417,13 +1418,10 @@ class QJavalinApiHandlerTest extends BaseTest .withLabel("Person Report") .withTableName(TestUtils.TABLE_NAME_PERSON) .withQueryFilterJson(JsonUtils.toJson(new QQueryFilter())) - .withColumnsJson(""" - {"columns":[ - {"name": "id"}, - {"name": "firstName"}, - {"name": "lastName"} - ]} - """)); + .withColumnsJson(JsonUtils.toJson(new ReportColumns() + .withColumn("id") + .withColumn("firstName") + .withColumn("lastName")))); HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/savedReport/renderSavedReport/" + reportId + "?reportFormat=CSV").asString(); assertEquals(HttpStatus.OK_200, response.getStatus());