diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 8a952f04..d29ad206 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -98,6 +98,23 @@ 5.2.2 + + + com.google.api-client + google-api-client + 1.35.2 + + + com.google.auth + google-auth-library-oauth2-http + 1.11.0 + + + com.google.apis + google-api-services-drive + v3-rev20220815-2.0.0 + + org.apache.maven.plugins diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java index 330b4fb9..70a9221e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java @@ -50,18 +50,25 @@ public class FormulaInterpreter *******************************************************************************/ public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException { - List results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0)); - if(results.size() == 1) + try { - return (results.get(0)); + List results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0)); + if(results.size() == 1) + { + return (results.get(0)); + } + else if(results.isEmpty()) + { + throw (new QFormulaException("No results from formula")); + } + else + { + throw (new QFormulaException("More than 1 result from formula")); + } } - else if(results.isEmpty()) + catch(Exception e) { - throw (new QFormulaException("No results from formula")); - } - else - { - throw (new QFormulaException("More than 1 result from formula")); + throw (new QFormulaException("Error interpreting formula [" + formula + "]", e)); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 3a5f4184..efa1f874 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -29,15 +29,19 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QFormulaException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +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.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; @@ -48,11 +52,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.reporting.QReportDataSource; 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.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -63,7 +69,16 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; /******************************************************************************* - ** Action to generate a report!! + ** Action to generate a report. + ** + ** A report can contain 1 or more Data Sources - e.g., tables + filters that define + ** data that goes into the report. + ** + ** A report can also contain 1 or more Views - e.g., sheets in a spreadsheet workbook. + ** (how do those work in non-XLSX formats??). Views can either be plain tables, + ** summaries (like pivot tables, but called summary to avoid confusion with "native" + ** pivot tables), or native pivot tables (not initially supported, due to lack of + ** support in fastexcel...). *******************************************************************************/ public class GenerateReportAction { @@ -76,7 +91,6 @@ public class GenerateReportAction Map> totalAggregates = new HashMap<>(); Map> varianceTotalAggregates = new HashMap<>(); - private boolean includeTableView = false; private QReportMetaData report; private ReportFormat reportFormat; private ExportStreamerInterface reportStreamer; @@ -89,19 +103,52 @@ public class GenerateReportAction public void execute(ReportInput reportInput) throws QException { report = reportInput.getInstance().getReport(reportInput.getReportName()); - Optional tableView = report.getViews().stream().filter(v -> v.getType().equals(ReportType.TABLE)).findFirst(); - reportFormat = reportInput.getReportFormat(); reportStreamer = reportFormat.newReportStreamer(); - if(tableView.isPresent()) + for(QReportDataSource dataSource : report.getDataSources()) { - includeTableView = true; - startTableView(reportInput, tableView.get()); - } + List dataSourceTableViews = report.getViews().stream() + .filter(v -> v.getType().equals(ReportType.TABLE)) + .filter(v -> v.getDataSourceName().equals(dataSource.getName())) + .toList(); - gatherData(reportInput); - gatherVarianceData(reportInput); + List dataSourcePivotViews = report.getViews().stream() + .filter(v -> v.getType().equals(ReportType.SUMMARY)) + .filter(v -> v.getDataSourceName().equals(dataSource.getName())) + .toList(); + + List dataSourceVariantViews = report.getViews().stream() + .filter(v -> v.getType().equals(ReportType.SUMMARY)) + .filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName())) + .toList(); + + if(dataSourceTableViews.isEmpty()) + { + if(!dataSourcePivotViews.isEmpty() || !dataSourceVariantViews.isEmpty()) + { + gatherData(reportInput, dataSource, null, dataSourcePivotViews, dataSourceVariantViews); + } + } + else + { + for(QReportView dataSourceTableView : dataSourceTableViews) + { + if(dataSourceTableView.getViewCustomizer() != null) + { + Function viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer()); + if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer) + { + reportViewCustomizer.setReportInput(reportInput); + } + dataSourceTableView = viewCustomizerFunction.apply(dataSourceTableView.clone()); // todo - will this throw concurrent mod exception?? + } + + startTableView(reportInput, dataSource, dataSourceTableView); + gatherData(reportInput, dataSource, dataSourceTableView, dataSourcePivotViews, dataSourceVariantViews); + } + } + } outputPivots(reportInput); } @@ -111,9 +158,9 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private void startTableView(ReportInput reportInput, QReportView reportView) throws QReportingException + private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QReportingException { - QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); + QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable()); ExportInput exportInput = new ExportInput(reportInput.getInstance()); exportInput.setSession(reportInput.getSession()); @@ -129,7 +176,14 @@ public class GenerateReportAction fields = new ArrayList<>(); for(QReportField column : reportView.getColumns()) { - fields.add(table.getField(column.getName())); + if(column.getIsVirtual()) + { + fields.add(column.toField()); + } + else + { + fields.add(table.getField(column.getName())); + } } } else @@ -145,50 +199,70 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private void gatherData(ReportInput reportInput) throws QException + private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List pivotViews, List variantViews) throws QException { - QQueryFilter queryFilter = report.getQueryFilter(); + QQueryFilter queryFilter = dataSource.getQueryFilter(); setInputValuesInQueryFilter(reportInput, queryFilter); + //////////////////////////////////////////////////////////////////////////////////////// + // check if this view has a transform step - if so, set it up now and run its pre-run // + //////////////////////////////////////////////////////////////////////////////////////// + AbstractTransformStep transformStep = null; + RunBackendStepInput transformStepInput = null; + RunBackendStepOutput transformStepOutput = null; + if(tableView != null && tableView.getRecordTransformStep() != null) + { + transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep()); + + transformStepInput = new RunBackendStepInput(reportInput.getInstance()); + transformStepInput.setSession(reportInput.getSession()); + transformStepInput.setValues(reportInput.getInputValues()); + + transformStepOutput = new RunBackendStepOutput(); + + transformStep.preRun(transformStepInput, transformStepOutput); + } + + //////////////////////////////////////////////////////////////////// + // create effectively-final versions of these vars for the lambda // + //////////////////////////////////////////////////////////////////// + AbstractTransformStep finalTransformStep = transformStep; + RunBackendStepInput finalTransformStepInput = transformStepInput; + RunBackendStepOutput finalTransformStepOutput = transformStepOutput; + + ///////////////////////////////////////////////////////////////// + // run a record pipe loop, over the query for this data source // + ///////////////////////////////////////////////////////////////// RecordPipe recordPipe = new RecordPipe(); new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> { QueryInput queryInput = new QueryInput(reportInput.getInstance()); queryInput.setSession(reportInput.getSession()); queryInput.setRecordPipe(recordPipe); - queryInput.setTableName(report.getSourceTable()); + queryInput.setTableName(dataSource.getSourceTable()); queryInput.setFilter(queryFilter); queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? return (new QueryAction().execute(queryInput)); - }, () -> consumeRecords(reportInput, recordPipe, false)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private void gatherVarianceData(ReportInput reportInput) throws QException - { - QQueryFilter varianceQueryFilter = report.getVarianceQueryFilter(); - if(varianceQueryFilter == null) + }, () -> { - return; + List records = recordPipe.consumeAvailableRecords(); + if(finalTransformStep != null) + { + finalTransformStepInput.setRecords(records); + finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput); + records = finalTransformStepOutput.getRecords(); + } + + return (consumeRecords(reportInput, dataSource, records, tableView, pivotViews, variantViews)); + }); + + //////////////////////////////////////////////// + // if there's a transformer, run its post-run // + //////////////////////////////////////////////// + if(transformStep != null) + { + transformStep.postRun(transformStepInput, transformStepOutput); } - - setInputValuesInQueryFilter(reportInput, varianceQueryFilter); - - RecordPipe recordPipe = new RecordPipe(); - new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> - { - QueryInput queryInput = new QueryInput(reportInput.getInstance()); - queryInput.setSession(reportInput.getSession()); - queryInput.setRecordPipe(recordPipe); - queryInput.setTableName(report.getSourceTable()); - queryInput.setFilter(varianceQueryFilter); - queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? - return (new QueryAction().execute(queryInput)); - }, () -> consumeRecords(reportInput, recordPipe, true)); } @@ -227,11 +301,14 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private Integer consumeRecords(ReportInput reportInput, RecordPipe recordPipe, boolean isForVariance) throws QReportingException + private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List records, QReportView tableView, List pivotViews, List variantViews) throws QException { - List records = recordPipe.consumeAvailableRecords(); + QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable()); - if(includeTableView && !isForVariance) + //////////////////////////////////////////////////////////////////////////// + // if this record goes on a table view, add it to the report streamer now // + //////////////////////////////////////////////////////////////////////////// + if(tableView != null) { reportStreamer.addRecords(records); } @@ -239,20 +316,38 @@ public class GenerateReportAction ////////////////////////////// // do aggregates for pivots // ////////////////////////////// - QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); - report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).forEach((view) -> + if(pivotViews != null) { - addRecordsToPivotAggregates(view, table, records, isForVariance ? variancePivotAggregates : pivotAggregates); - }); + for(QReportView pivotView : pivotViews) + { + addRecordsToPivotAggregates(pivotView, table, records, pivotAggregates); + } + } + + if(variantViews != null) + { + for(QReportView variantView : variantViews) + { + addRecordsToPivotAggregates(variantView, table, records, variancePivotAggregates); + } + } /////////////////////////////////////////// // do totals too, if any views want them // /////////////////////////////////////////// - if(report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).anyMatch(QReportView::getTotalRow)) + if(pivotViews != null && pivotViews.stream().anyMatch(QReportView::getTotalRow)) { for(QRecord record : records) { - addRecordToAggregatesMap(table, record, isForVariance ? varianceTotalAggregates : totalAggregates); + addRecordToAggregatesMap(table, record, totalAggregates); + } + } + + if(variantViews != null && variantViews.stream().anyMatch(QReportView::getTotalRow)) + { + for(QRecord record : records) + { + addRecordToAggregatesMap(table, record, varianceTotalAggregates); } } @@ -337,12 +432,12 @@ public class GenerateReportAction *******************************************************************************/ private void outputPivots(ReportInput reportInput) throws QReportingException, QFormulaException { - QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); - - List reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).toList(); + List reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList(); for(QReportView view : reportViews) { - PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table); + QReportDataSource dataSource = report.getDataSource(view.getDataSourceName()); + QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable()); + PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table); ExportInput exportInput = new ExportInput(reportInput.getInstance()); exportInput.setSession(reportInput.getSession()); @@ -564,11 +659,14 @@ public class GenerateReportAction variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates)); variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates)); + HashMap thisRowValues = new HashMap<>(); + variableInterpreter.addValueMap("thisRow", thisRowValues); for(QReportField column : view.getColumns()) { Serializable serializable = getValueForColumn(variableInterpreter, column); totalRow.setValue(column.getName(), serializable); + thisRowValues.put(column.getName(), serializable); String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable); System.out.printf("%25s", formatted); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java index 78b8f365..93a2065e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java @@ -46,8 +46,9 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface private ExportInput exportInput; private List fields; - private static List> list = new ArrayList<>(); - private static List headers = new ArrayList<>(); + private static Map>> rows = new LinkedHashMap<>(); + private static Map> headers = new LinkedHashMap<>(); + private static String currentSheetLabel; @@ -60,13 +61,25 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface + /******************************************************************************* + ** + *******************************************************************************/ + public static void reset() + { + rows.clear(); + headers.clear(); + currentSheetLabel = null; + } + + + /******************************************************************************* ** Getter for list ** *******************************************************************************/ - public static List> getList() + public static List> getList(String name) { - return (list); + return (rows.get(name)); } @@ -80,10 +93,13 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface this.exportInput = exportInput; this.fields = fields; - headers = new ArrayList<>(); + currentSheetLabel = label; + + rows.put(label, new ArrayList<>()); + headers.put(label, new ArrayList<>()); for(QFieldMetaData field : fields) { - headers.add(field.getLabel()); + headers.get(label).add(field.getLabel()); } } @@ -112,10 +128,10 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface private void addRecord(QRecord qRecord) { Map row = new LinkedHashMap<>(); - list.add(row); + rows.get(currentSheetLabel).add(row); for(int i = 0; i < fields.size(); i++) { - row.put(headers.get(i), qRecord.getValueString(fields.get(i).getName())); + row.put(headers.get(currentSheetLabel).get(i), qRecord.getValueString(fields.get(i).getName())); } } @@ -125,7 +141,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public void addTotalsRow(QRecord record) throws QReportingException + public void addTotalsRow(QRecord record) { addRecord(record); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java new file mode 100644 index 00000000..2f5d6f92 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java @@ -0,0 +1,41 @@ +/* + * 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.actions.reporting.customizers; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ReportViewCustomizer extends Function +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void setReportInput(ReportInput reportInput); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index b8fefbae..7572b4af 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -266,7 +266,7 @@ public class QInstanceEnricher /******************************************************************************* ** *******************************************************************************/ - static String nameToLabel(String name) + public static String nameToLabel(String name) { if(!StringUtils.hasContent(name)) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java index 3bad2944..dc0486a3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java @@ -37,7 +37,6 @@ public class QFrontendReportMetaData { private String name; private String label; - private String tableName; private String processName; private String iconName; @@ -55,7 +54,6 @@ public class QFrontendReportMetaData { this.name = reportMetaData.getName(); this.label = reportMetaData.getLabel(); - this.tableName = reportMetaData.getSourceTable(); this.processName = reportMetaData.getProcessName(); if(reportMetaData.getIcon() != null) @@ -88,17 +86,6 @@ public class QFrontendReportMetaData - /******************************************************************************* - ** Getter for primaryKeyField - ** - *******************************************************************************/ - public String getTableName() - { - return tableName; - } - - - /******************************************************************************* ** Getter for processName ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java index fad28dc4..785d40dd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java @@ -34,7 +34,8 @@ public enum QComponentType VIEW_FORM, DOWNLOAD_FORM, RECORD_LIST, - PROCESS_SUMMARY_RESULTS; + PROCESS_SUMMARY_RESULTS, + GOOGLE_DRIVE_SELECT_FOLDER; /////////////////////////////////////////////////////////////////////////// // keep these values in sync with QComponentType.ts in qqq-frontend-core // /////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java new file mode 100644 index 00000000..54f2f80e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java @@ -0,0 +1,139 @@ +/* + * 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.model.metadata.reporting; + + +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QReportDataSource +{ + private String name; + private String sourceTable; + private QQueryFilter queryFilter; + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QReportDataSource withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for sourceTable + ** + *******************************************************************************/ + public String getSourceTable() + { + return sourceTable; + } + + + + /******************************************************************************* + ** Setter for sourceTable + ** + *******************************************************************************/ + public void setSourceTable(String sourceTable) + { + this.sourceTable = sourceTable; + } + + + + /******************************************************************************* + ** Fluent setter for sourceTable + ** + *******************************************************************************/ + public QReportDataSource withSourceTable(String sourceTable) + { + this.sourceTable = sourceTable; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryFilter + ** + *******************************************************************************/ + public QQueryFilter getQueryFilter() + { + return queryFilter; + } + + + + /******************************************************************************* + ** Setter for queryFilter + ** + *******************************************************************************/ + public void setQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + } + + + + /******************************************************************************* + ** Fluent setter for queryFilter + ** + *******************************************************************************/ + public QReportDataSource withQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java index cb53f132..ead57b64 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java @@ -22,16 +22,40 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; + + /******************************************************************************* ** Field within a report *******************************************************************************/ public class QReportField { - private String name; - private String label; - private String formula; - private String displayFormat; - // todo - type? + private String name; + private String label; + private QFieldType type; + private String formula; + private String displayFormat; + + /////////////////////////////////////////////////////////////////////////// + // Noew: new attributes added here probably belong in the toField method // + /////////////////////////////////////////////////////////////////////////// + + private boolean isVirtual = false; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFieldMetaData toField() + { + return new QFieldMetaData() + .withName(name) + .withLabel(label) + .withType(type) + .withDisplayFormat(displayFormat); + } @@ -103,6 +127,40 @@ public class QReportField + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public QFieldType getType() + { + return type; + } + + + + /******************************************************************************* + ** Setter for type + ** + *******************************************************************************/ + public void setType(QFieldType type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + ** + *******************************************************************************/ + public QReportField withType(QFieldType type) + { + this.type = type; + return (this); + } + + + /******************************************************************************* ** Getter for formula ** @@ -169,4 +227,37 @@ public class QReportField return (this); } + + + /******************************************************************************* + ** Getter for isVirtual + ** + *******************************************************************************/ + public boolean getIsVirtual() + { + return isVirtual; + } + + + + /******************************************************************************* + ** Setter for isVirtual + ** + *******************************************************************************/ + public void setIsVirtual(boolean isVirtual) + { + this.isVirtual = isVirtual; + } + + + + /******************************************************************************* + ** Fluent setter for isVirtual + ** + *******************************************************************************/ + public QReportField withIsVirtual(boolean isVirtual) + { + this.isVirtual = isVirtual; + return (this); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java index ccb00b3e..67ef09b1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java @@ -23,10 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; import java.util.List; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -34,14 +34,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; *******************************************************************************/ public class QReportMetaData implements QAppChildMetaData { - private String name; - private String label; - private List inputFields; - private String sourceTable; + private String name; + private String label; + private String processName; - private QQueryFilter queryFilter; - private QQueryFilter varianceQueryFilter; - private List views; + private List inputFields; + + private List dataSources; + private List views; private String parentAppName; private QIcon icon; @@ -150,40 +150,6 @@ public class QReportMetaData implements QAppChildMetaData - /******************************************************************************* - ** Getter for sourceTable - ** - *******************************************************************************/ - public String getSourceTable() - { - return sourceTable; - } - - - - /******************************************************************************* - ** Setter for sourceTable - ** - *******************************************************************************/ - public void setSourceTable(String sourceTable) - { - this.sourceTable = sourceTable; - } - - - - /******************************************************************************* - ** Fluent setter for sourceTable - ** - *******************************************************************************/ - public QReportMetaData withSourceTable(String sourceTable) - { - this.sourceTable = sourceTable; - return (this); - } - - - /******************************************************************************* ** Getter for processName ** @@ -215,72 +181,38 @@ public class QReportMetaData implements QAppChildMetaData this.processName = processName; return (this); } - + /******************************************************************************* - ** Getter for queryFilter + ** Getter for dataSources ** *******************************************************************************/ - public QQueryFilter getQueryFilter() + public List getDataSources() { - return queryFilter; + return dataSources; } /******************************************************************************* - ** Setter for queryFilter + ** Setter for dataSources ** *******************************************************************************/ - public void setQueryFilter(QQueryFilter queryFilter) + public void setDataSources(List dataSources) { - this.queryFilter = queryFilter; + this.dataSources = dataSources; } /******************************************************************************* - ** Fluent setter for queryFilter + ** Fluent setter for dataSources ** *******************************************************************************/ - public QReportMetaData withQueryFilter(QQueryFilter queryFilter) + public QReportMetaData withDataSources(List dataSources) { - this.queryFilter = queryFilter; - return (this); - } - - - - /******************************************************************************* - ** Getter for varianceQueryFilter - ** - *******************************************************************************/ - public QQueryFilter getVarianceQueryFilter() - { - return varianceQueryFilter; - } - - - - /******************************************************************************* - ** Setter for varianceQueryFilter - ** - *******************************************************************************/ - public void setVarianceQueryFilter(QQueryFilter varianceQueryFilter) - { - this.varianceQueryFilter = varianceQueryFilter; - } - - - - /******************************************************************************* - ** Fluent setter for varianceQueryFilter - ** - *******************************************************************************/ - public QReportMetaData withVarianceQueryFilter(QQueryFilter varianceQueryFilter) - { - this.varianceQueryFilter = varianceQueryFilter; + this.dataSources = dataSources; return (this); } @@ -374,4 +306,22 @@ public class QReportMetaData implements QAppChildMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public QReportDataSource getDataSource(String dataSourceName) + { + for(QReportDataSource dataSource : CollectionUtils.nonNullList(dataSources)) + { + if(dataSource.getName().equals(dataSourceName)) + { + return (dataSource); + } + } + + return (null); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java index 3d32d340..624683b0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java @@ -22,17 +22,21 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; +import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; /******************************************************************************* ** *******************************************************************************/ -public class QReportView +public class QReportView implements Cloneable { private String name; private String label; + private String dataSourceName; + private String varianceDataSourceName; private ReportType type; private String titleFormat; private List titleFields; @@ -42,6 +46,13 @@ public class QReportView private List columns; private List orderByFields; + private QCodeReference recordTransformStep; + private QCodeReference viewCustomizer; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Note: This class is Cloneable - think about if new fields added here need deep-copied in the clone method! // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /******************************************************************************* @@ -112,6 +123,74 @@ public class QReportView + /******************************************************************************* + ** Getter for dataSourceName + ** + *******************************************************************************/ + public String getDataSourceName() + { + return dataSourceName; + } + + + + /******************************************************************************* + ** Setter for dataSourceName + ** + *******************************************************************************/ + public void setDataSourceName(String dataSourceName) + { + this.dataSourceName = dataSourceName; + } + + + + /******************************************************************************* + ** Fluent setter for dataSourceName + ** + *******************************************************************************/ + public QReportView withDataSourceName(String dataSourceName) + { + this.dataSourceName = dataSourceName; + return (this); + } + + + + /******************************************************************************* + ** Getter for varianceDataSourceName + ** + *******************************************************************************/ + public String getVarianceDataSourceName() + { + return varianceDataSourceName; + } + + + + /******************************************************************************* + ** Setter for varianceDataSourceName + ** + *******************************************************************************/ + public void setVarianceDataSourceName(String varianceDataSourceName) + { + this.varianceDataSourceName = varianceDataSourceName; + } + + + + /******************************************************************************* + ** Fluent setter for varianceDataSourceName + ** + *******************************************************************************/ + public QReportView withVarianceDataSourceName(String varianceDataSourceName) + { + this.varianceDataSourceName = varianceDataSourceName; + return (this); + } + + + /******************************************************************************* ** Getter for type ** @@ -382,4 +461,114 @@ public class QReportView return (this); } + + + /******************************************************************************* + ** Getter for recordTransformerStep + ** + *******************************************************************************/ + public QCodeReference getRecordTransformStep() + { + return recordTransformStep; + } + + + + /******************************************************************************* + ** Setter for recordTransformerStep + ** + *******************************************************************************/ + public void setRecordTransformStep(QCodeReference recordTransformStep) + { + this.recordTransformStep = recordTransformStep; + } + + + + /******************************************************************************* + ** Fluent setter for recordTransformerStep + ** + *******************************************************************************/ + public QReportView withRecordTransformStep(QCodeReference recordTransformerStep) + { + this.recordTransformStep = recordTransformerStep; + return (this); + } + + + + /******************************************************************************* + ** Getter for viewCustomizer + ** + *******************************************************************************/ + public QCodeReference getViewCustomizer() + { + return viewCustomizer; + } + + + + /******************************************************************************* + ** Setter for viewCustomizer + ** + *******************************************************************************/ + public void setViewCustomizer(QCodeReference viewCustomizer) + { + this.viewCustomizer = viewCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for viewCustomizer + ** + *******************************************************************************/ + public QReportView withViewCustomizer(QCodeReference viewCustomizer) + { + this.viewCustomizer = viewCustomizer; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QReportView clone() + { + try + { + QReportView clone = (QReportView) super.clone(); + + ///////////////////////// + // copy any lists, etc // + ///////////////////////// + if(titleFields != null) + { + clone.setTitleFields(new ArrayList<>(titleFields)); + } + + if(pivotFields != null) + { + clone.setPivotFields(new ArrayList<>(pivotFields)); + } + + if(columns != null) + { + clone.setColumns(new ArrayList<>(columns)); + } + + if(orderByFields != null) + { + clone.setOrderByFields(new ArrayList<>(orderByFields)); + } + + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java index 2b0b12bd..8492537a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java @@ -27,6 +27,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; *******************************************************************************/ public enum ReportType { - PIVOT, - TABLE + TABLE, // e.g., raw data in tabular form. + SUMMARY, // e.g., summaries computed within QQQ + PIVOT // e.g., a true spreadsheet pivot. Not initially supported... } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java index 4b762ec8..48d854f0 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java @@ -398,7 +398,7 @@ public class CollectionUtils else { endAt = startAt + limit; - if (endAt > list.size()) + if(endAt > list.size()) { endAt = list.size(); } @@ -406,4 +406,20 @@ public class CollectionUtils return list.subList(startAt, endAt); } + + + + /******************************************************************************* + ** Returns the input list, unless it was null - in which case a new array list is returned. + ** + ** Meant to help avoid null checks on foreach loops. + *******************************************************************************/ + public static List nonNullList(List list) + { + if(list == null) + { + return (new ArrayList<>()); + } + return (list); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java index 0758b2c8..62bafe5b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java @@ -93,12 +93,12 @@ class FormulaInterpreterTest QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); vi.addValueMap("input", Map.of("i", 5, "c", 'c')); - assertThatThrownBy(() -> interpretFormula(vi, "")).hasMessageContaining("No results"); - assertThatThrownBy(() -> interpretFormula(vi, "NOT-A-FUN(1,2)")).hasMessageContaining("unrecognized expression"); - assertThatThrownBy(() -> interpretFormula(vi, "ADD(1)")).hasMessageContaining("Wrong number of arguments"); - assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,2,3)")).hasMessageContaining("Wrong number of arguments"); - assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,A)")).hasMessageContaining("[A] as a number"); - assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,${input.c})")).hasMessageContaining("[c] as a number"); + assertThatThrownBy(() -> interpretFormula(vi, "")).hasRootCauseMessage("No results from formula"); + assertThatThrownBy(() -> interpretFormula(vi, "NOT-A-FUN(1,2)")).hasRootCauseMessage("Unable to evaluate unrecognized expression: NOT-A-FUN"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1)")).hasRootCauseMessage("Wrong number of arguments (required: 2, received: 1)"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,2,3)")).hasRootCauseMessage("Wrong number of arguments (required: 2, received: 3)"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,A)")).hasRootCauseMessage("Could not process [A] as a number"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,${input.c})")).hasRootCauseMessage("Could not process [c] as a number"); // todo - bad syntax (e.g., missing ')' } @@ -168,7 +168,7 @@ class FormulaInterpreterTest assertTrue((Boolean) interpretFormula(vi, "GTE(${input.two},${input.one})")); // todo - google sheets compares strings differently... - assertThatThrownBy(() -> interpretFormula(vi, "LT(${input.foo},${input.one})")).hasMessageContaining("[bar] as a number"); + assertThatThrownBy(() -> interpretFormula(vi, "LT(${input.foo},${input.one})")).hasRootCauseMessage("Could not process [bar] as a number"); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index f6ba54cc..cce7bfbb 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; +import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; import java.time.Month; @@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; 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.reporting.QReportDataSource; 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; @@ -72,7 +74,7 @@ public class GenerateReportActionTest @AfterEach void beforeAndAfterEach() { - ListOfMapsExportStreamer.getList().clear(); + ListOfMapsExportStreamer.reset(); MemoryRecordStore.getInstance().reset(); } @@ -85,11 +87,11 @@ public class GenerateReportActionTest void testPivot1() throws QException { QInstance qInstance = TestUtils.defineInstance(); - qInstance.addReport(defineReport(true)); + qInstance.addReport(definePersonShoesPivotReport(true)); insertPersonRecords(qInstance); - runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31)); + runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1), "endDate", LocalDate.of(1980, Month.DECEMBER, 31))); - List> list = ListOfMapsExportStreamer.getList(); + List> list = ListOfMapsExportStreamer.getList("pivot"); Iterator> iterator = list.iterator(); Map row = iterator.next(); assertEquals(3, list.size()); @@ -140,7 +142,7 @@ public class GenerateReportActionTest void testPivot2() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QReportMetaData report = defineReport(false); + QReportMetaData report = definePersonShoesPivotReport(false); ////////////////////////////////////////////// // change from the default to sort reversed // @@ -148,9 +150,9 @@ public class GenerateReportActionTest report.getViews().get(0).getOrderByFields().get(0).setIsAscending(false); qInstance.addReport(report); insertPersonRecords(qInstance); - runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31)); + runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1), "endDate", LocalDate.of(1980, Month.DECEMBER, 31))); - List> list = ListOfMapsExportStreamer.getList(); + List> list = ListOfMapsExportStreamer.getList("pivot"); Iterator> iterator = list.iterator(); Map row = iterator.next(); assertEquals(2, list.size()); @@ -172,19 +174,19 @@ public class GenerateReportActionTest void testPivot3() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QReportMetaData report = defineReport(false); + QReportMetaData report = definePersonShoesPivotReport(false); ////////////////////////////////////////////////////////////////////////////////////////////// // remove the filters, change to sort by personCount (to get some ties), then sumPrice desc // // this also shows the behavior of a null value in an order by // ////////////////////////////////////////////////////////////////////////////////////////////// - report.setQueryFilter(null); + report.getDataSources().get(0).getQueryFilter().setCriteria(null); report.getViews().get(0).setOrderByFields(List.of(new QFilterOrderBy("personCount"), new QFilterOrderBy("sumPrice", false))); qInstance.addReport(report); insertPersonRecords(qInstance); - runReport(qInstance, LocalDate.now(), LocalDate.now()); + runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now())); - List> list = ListOfMapsExportStreamer.getList(); + List> list = ListOfMapsExportStreamer.getList("pivot"); Iterator> iterator = list.iterator(); Map row = iterator.next(); @@ -224,21 +226,21 @@ public class GenerateReportActionTest void testPivot4() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QReportMetaData report = defineReport(false); + QReportMetaData report = definePersonShoesPivotReport(false); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // remove the filter, change to have 2 pivot columns - homeStateId and lastName - we should get no roll-up like this. // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - report.setQueryFilter(null); + report.getDataSources().get(0).getQueryFilter().setCriteria(null); report.getViews().get(0).setPivotFields(List.of( "homeStateId", "lastName" )); qInstance.addReport(report); insertPersonRecords(qInstance); - runReport(qInstance, LocalDate.now(), LocalDate.now()); + runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now())); - List> list = ListOfMapsExportStreamer.getList(); + List> list = ListOfMapsExportStreamer.getList("pivot"); Iterator> iterator = list.iterator(); Map row = iterator.next(); assertEquals(6, list.size()); @@ -282,18 +284,18 @@ public class GenerateReportActionTest void testPivot5() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QReportMetaData report = defineReport(false); + QReportMetaData report = definePersonShoesPivotReport(false); ///////////////////////////////////////////////////////////////////////////////////// // remove the filter, and just pivot on homeStateId - should aggregate differently // ///////////////////////////////////////////////////////////////////////////////////// - report.setQueryFilter(null); + report.getDataSources().get(0).getQueryFilter().setCriteria(null); report.getViews().get(0).setPivotFields(List.of("homeStateId")); qInstance.addReport(report); insertPersonRecords(qInstance); - runReport(qInstance, LocalDate.now(), LocalDate.now()); + runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now())); - List> list = ListOfMapsExportStreamer.getList(); + List> list = ListOfMapsExportStreamer.getList("pivot"); Iterator> iterator = list.iterator(); Map row = iterator.next(); assertEquals(2, list.size()); @@ -319,7 +321,7 @@ public class GenerateReportActionTest try(FileOutputStream fileOutputStream = new FileOutputStream(name)) { QInstance qInstance = TestUtils.defineInstance(); - qInstance.addReport(defineReport(true)); + qInstance.addReport(definePersonShoesPivotReport(true)); insertPersonRecords(qInstance); ReportInput reportInput = new ReportInput(qInstance); @@ -345,7 +347,7 @@ public class GenerateReportActionTest try(FileOutputStream fileOutputStream = new FileOutputStream(name)) { QInstance qInstance = TestUtils.defineInstance(); - qInstance.addReport(defineReport(true)); + qInstance.addReport(definePersonShoesPivotReport(true)); insertPersonRecords(qInstance); ReportInput reportInput = new ReportInput(qInstance); @@ -364,14 +366,14 @@ public class GenerateReportActionTest /******************************************************************************* ** *******************************************************************************/ - private void runReport(QInstance qInstance, LocalDate startDate, LocalDate endDate) throws QException + private void runReport(QInstance qInstance, Map inputValues) throws QException { ReportInput reportInput = new ReportInput(qInstance); reportInput.setSession(new QSession()); reportInput.setReportName(REPORT_NAME); reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS); reportInput.setReportOutputStream(new ByteArrayOutputStream()); - reportInput.setInputValues(Map.of("startDate", startDate, "endDate", endDate)); + reportInput.setInputValues(inputValues); new GenerateReportAction().execute(reportInput); } @@ -397,23 +399,29 @@ public class GenerateReportActionTest /******************************************************************************* ** *******************************************************************************/ - public static QReportMetaData defineReport(boolean includeTotalRow) + public static QReportMetaData definePersonShoesPivotReport(boolean includeTotalRow) { return new QReportMetaData() .withName(REPORT_NAME) - .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withDataSources(List.of( + new QReportDataSource() + .withName("persons") + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.STARTS_WITH, List.of("K"))) + .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.BETWEEN, List.of("${input.startDate}", "${input.endDate}"))) + ) + )) .withInputFields(List.of( new QFieldMetaData("startDate", QFieldType.DATE_TIME), new QFieldMetaData("endDate", QFieldType.DATE_TIME) )) - .withQueryFilter(new QQueryFilter() - .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.STARTS_WITH, List.of("K"))) - .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.BETWEEN, List.of("${input.startDate}", "${input.endDate}"))) - ) .withViews(List.of( new QReportView() .withName("pivot") - .withType(ReportType.PIVOT) + .withLabel("pivot") + .withDataSourceName("persons") + .withType(ReportType.SUMMARY) .withPivotFields(List.of("lastName")) .withTotalRow(includeTotalRow) .withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC") @@ -438,4 +446,112 @@ public class GenerateReportActionTest )); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableOnlyReport() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = new QReportMetaData() + .withName(REPORT_NAME) + .withDataSources(List.of( + new QReportDataSource() + .withName("persons") + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}"))) + ) + )) + .withInputFields(List.of( + new QFieldMetaData("startDate", QFieldType.DATE_TIME) + )) + .withViews(List.of( + new QReportView() + .withName("table1") + .withLabel("table1") + .withDataSourceName("persons") + .withType(ReportType.TABLE) + .withColumns(List.of( + new QReportField().withName("id"), + new QReportField().withName("firstName"), + new QReportField().withName("lastName") + )) + )); + + qInstance.addReport(report); + + insertPersonRecords(qInstance); + runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1))); + + List> list = ListOfMapsExportStreamer.getList("table1"); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + assertEquals(5, list.size()); + assertThat(row).containsOnlyKeys("Id", "First Name", "Last Name"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoTableViewsOneDataSourceReport() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = new QReportMetaData() + .withName(REPORT_NAME) + .withDataSources(List.of( + new QReportDataSource() + .withName("persons") + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}"))) + ) + )) + .withInputFields(List.of( + new QFieldMetaData("startDate", QFieldType.DATE_TIME) + )) + .withViews(List.of( + new QReportView() + .withName("table1") + .withLabel("table1") + .withDataSourceName("persons") + .withType(ReportType.TABLE) + .withColumns(List.of( + new QReportField().withName("id"), + new QReportField().withName("firstName"), + new QReportField().withName("lastName") + )), + new QReportView() + .withName("table2") + .withLabel("table2") + .withDataSourceName("persons") + .withType(ReportType.TABLE) + .withColumns(List.of( + new QReportField().withName("birthDate") + )) + )); + + qInstance.addReport(report); + + insertPersonRecords(qInstance); + runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1))); + + List> list = ListOfMapsExportStreamer.getList("table1"); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + assertEquals(5, list.size()); + assertThat(row).containsOnlyKeys("Id", "First Name", "Last Name"); + + list = ListOfMapsExportStreamer.getList("table2"); + iterator = list.iterator(); + row = iterator.next(); + assertEquals(5, list.size()); + assertThat(row).containsOnlyKeys("Birth Date"); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java index 2cd859b3..1fd08515 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java @@ -49,7 +49,7 @@ class BasicRunReportProcessTest void testRunReport() throws QException { QInstance instance = TestUtils.defineInstance(); - QReportMetaData report = GenerateReportActionTest.defineReport(true); + QReportMetaData report = GenerateReportActionTest.definePersonShoesPivotReport(true); QProcessMetaData runReportProcess = BasicRunReportProcess.defineProcessMetaData(); instance.addReport(report);