CE-881 - Formalize savedReport.columnsJSON as a ReportColumns class.

This commit is contained in:
2024-03-29 09:06:16 -05:00
parent 52b64ffbc0
commit 5384eb9927
6 changed files with 312 additions and 105 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ReportColumn> columns;
/*******************************************************************************
** Getter for columns
*******************************************************************************/
public List<ReportColumn> getColumns()
{
return (this.columns);
}
/*******************************************************************************
** Setter for columns
*******************************************************************************/
public void setColumns(List<ReportColumn> columns)
{
this.columns = columns;
}
/*******************************************************************************
** Fluent setter for columns
*******************************************************************************/
public ReportColumns withColumns(List<ReportColumn> 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);
}
}

View File

@ -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<String> 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<QReportField> reportColumns = new ArrayList<>();
view.setColumns(reportColumns);
Map<String, Object> columnsObject = JsonUtils.toObject(savedReport.getColumnsJson(), new TypeReference<>() {});
List<Map<String, Object>> columns = (List<Map<String, Object>>) columnsObject.get("columns");
for(Map<String, Object> column : columns)
Set<String> 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<String> 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;
}
}

View File

@ -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);

View File

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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"))
)));

View File

@ -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<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/savedReport/renderSavedReport/" + reportId + "?reportFormat=CSV").asString();
assertEquals(HttpStatus.OK_200, response.getStatus());