diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java index 00ba8fca..fe2ed2c1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; @@ -40,6 +41,8 @@ public class RenderWidgetAction *******************************************************************************/ public RenderWidgetOutput execute(RenderWidgetInput input) throws QException { + ActionHelper.validateSession(input); + AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference()); return (widgetRenderer.render(input)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 6f344f10..88bf8272 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -31,13 +31,16 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Stream; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; @@ -50,6 +53,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -118,6 +123,7 @@ public class QInstanceValidator validateAutomationProviders(qInstance); validateTables(qInstance); validateProcesses(qInstance); + validateReports(qInstance); validateApps(qInstance); validatePossibleValueSources(qInstance); validateQueuesAndProviders(qInstance); @@ -186,6 +192,8 @@ public class QInstanceValidator qInstance.getBackends().forEach((backendName, backend) -> { assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + "."); + + backend.performValidation(this); }); } } @@ -258,6 +266,8 @@ public class QInstanceValidator ////////////////////////////////////////// Set fieldNamesInSections = new HashSet<>(); QFieldSection tier1Section = null; + Set usedSectionNames = new HashSet<>(); + Set usedSectionLabels = new HashSet<>(); if(table.getSections() != null) { for(QFieldSection section : table.getSections()) @@ -268,6 +278,12 @@ public class QInstanceValidator assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1"); tier1Section = section; } + + assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName()); + usedSectionNames.add(section.getName()); + + assertCondition(!usedSectionLabels.contains(section.getLabel()), "Table " + tableName + " has more than 1 section labeled " + section.getLabel()); + usedSectionLabels.add(section.getLabel()); } } @@ -716,6 +732,133 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validateReports(QInstance qInstance) + { + if(CollectionUtils.nullSafeHasContents(qInstance.getReports())) + { + qInstance.getReports().forEach((reportName, report) -> + { + assertCondition(Objects.equals(reportName, report.getName()), "Inconsistent naming for report: " + reportName + "/" + report.getName() + "."); + validateAppChildHasValidParentAppName(qInstance, report); + + //////////////////////////////////////// + // validate dataSources in the report // + //////////////////////////////////////// + Set usedDataSourceNames = new HashSet<>(); + if(assertCondition(CollectionUtils.nullSafeHasContents(report.getDataSources()), "At least 1 data source must be defined in report " + reportName + ".")) + { + int index = 0; + for(QReportDataSource dataSource : report.getDataSources()) + { + assertCondition(StringUtils.hasContent(dataSource.getName()), "Missing name for a dataSource at index " + index + " in report " + reportName); + index++; + + assertCondition(!usedDataSourceNames.contains(dataSource.getName()), "More than one dataSource with name " + dataSource.getName() + " in report " + reportName); + usedDataSourceNames.add(dataSource.getName()); + + String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " "; + + if(StringUtils.hasContent(dataSource.getSourceTable())) + { + assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required)."); + if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance.")) + { + if(dataSource.getQueryFilter() != null) + { + validateQueryFilter("In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter()); + } + } + } + else if(dataSource.getStaticDataSupplier() != null) + { + validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class); + } + else + { + errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required)."); + } + } + } + + //////////////////////////////////////// + // validate dataSources in the report // + //////////////////////////////////////// + if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + ".")) + { + int index = 0; + Set usedViewNames = new HashSet<>(); + for(QReportView view : report.getViews()) + { + assertCondition(StringUtils.hasContent(view.getName()), "Missing name for a view at index " + index + " in report " + reportName); + index++; + + assertCondition(!usedViewNames.contains(view.getName()), "More than one view with name " + view.getName() + " in report " + reportName); + usedViewNames.add(view.getName()); + + String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " "; + assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type."); + if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName")) + { + assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + " has an unrecognized dataSourceName: " + view.getDataSourceName()); + } + + if(StringUtils.hasContent(view.getVarianceDataSourceName())) + { + assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + " has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName()); + } + + // actually, this is okay if there's a customizer, so... + assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns."); + + // todo - all these too... + // view.getPivotFields(); + // view.getViewCustomizer(); // validate code ref + // view.getRecordTransformStep(); // validate code ref + // view.getOrderByFields(); // make sure valid field names? + // view.getIncludePivotSubTotals(); // only for pivot type + // view.getTitleFormat(); view.getTitleFields(); // validate these match? + } + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateQueryFilter(String context, QTableMetaData table, QQueryFilter queryFilter) + { + for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria())) + { + if(assertCondition(StringUtils.hasContent(criterion.getFieldName()), context + "Missing fieldName for a criteria")) + { + assertNoException(() -> table.getField(criterion.getFieldName()), context + "Criteria fieldName " + criterion.getFieldName() + " is not a field in this table."); + } + assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + criterion.getFieldName()); + assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK) + } + + for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys())) + { + if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy")) + { + assertNoException(() -> table.getField(orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table."); + } + } + + for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters())) + { + validateQueryFilter(context, table, subFilter); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -978,7 +1121,7 @@ public class QInstanceValidator ** But if it's false, add the provided message to the list of errors (and return false, ** e.g., in case you need to stop evaluating rules to avoid exceptions). *******************************************************************************/ - private boolean assertCondition(boolean condition, String message) + public boolean assertCondition(boolean condition, String message) { if(!condition) { @@ -1035,4 +1178,15 @@ public class QInstanceValidator LOG.info("Validation warning: " + message); } } + + + + /******************************************************************************* + ** Getter for errors + ** + *******************************************************************************/ + public List getErrors() + { + return errors; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java index ba9d2c10..6c5d6985 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.serialization.QBackendMetaDataDeserializer; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -321,4 +322,15 @@ public class QBackendMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void performValidation(QInstanceValidator qInstanceValidator) + { + //////////////////////// + // noop in base class // + //////////////////////// + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java new file mode 100644 index 00000000..cc99b14d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java @@ -0,0 +1,68 @@ +/* + * 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.processes; + + +import java.io.Serializable; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AbstractProcessMetaDataBuilder +{ + protected QProcessMetaData processMetaData; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AbstractProcessMetaDataBuilder(QProcessMetaData processMetaData) + { + this.processMetaData = processMetaData; + } + + + + /******************************************************************************* + ** Getter for processMetaData + ** + *******************************************************************************/ + public QProcessMetaData getProcessMetaData() + { + return processMetaData; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void setInputFieldDefaultValue(String fieldName, Serializable value) + { + processMetaData.getInputFields().stream() + .filter(f -> f.getName().equals(fieldName)).findFirst() + .ifPresent(f -> f.setDefaultValue(value)); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java index 7ab36e91..2f0d9d00 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java @@ -90,10 +90,17 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe updateRecordsWithDisplayValuesAndPossibleValues(runBackendStepInput, loadedRecordList); runBackendStepOutput.setRecords(loadedRecordList); - //////////////////////////////////////////////////////////////////////////////////////////////////// - // get the process summary from the ... transform step? the load step? each knows some... todo? // - //////////////////////////////////////////////////////////////////////////////////////////////////// - runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true)); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // get the process summary from the load step, if it's a summary-provider -- else, use the transform step (which is always a provider) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(loadStep instanceof ProcessSummaryProviderInterface provider) + { + runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, provider.doGetProcessSummary(runBackendStepOutput, true)); + } + else + { + runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true)); + } transformStep.postRun(runBackendStepInput, runBackendStepOutput); loadStep.postRun(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java index caaf431d..ebbd80c9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; 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.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; @@ -131,8 +136,8 @@ public class StreamedETLWithFrontendProcess .withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE))) .withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true))) .withField(new QFieldMetaData(FIELD_DEFAULT_QUERY_FILTER, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DEFAULT_QUERY_FILTER))) - .withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(extractStepClass))) - .withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(transformStepClass))) + .withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(extractStepClass == null ? null : new QCodeReference(extractStepClass))) + .withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(transformStepClass == null ? null : new QCodeReference(transformStepClass))) .withField(new QFieldMetaData(FIELD_PREVIEW_MESSAGE, QFieldType.STRING).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_PREVIEW_MESSAGE, DEFAULT_PREVIEW_MESSAGE_FOR_INSERT))) ); @@ -153,7 +158,7 @@ public class StreamedETLWithFrontendProcess .withName(STEP_NAME_EXECUTE) .withCode(new QCodeReference(StreamedETLExecuteStep.class)) .withInputData(new QFunctionInputMetaData() - .withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(loadStepClass)))) + .withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(loadStepClass == null ? null : new QCodeReference(loadStepClass)))) .withOutputMetaData(new QFunctionOutputMetaData() .withField(new QFieldMetaData(FIELD_PROCESS_SUMMARY, QFieldType.STRING)) ); @@ -169,4 +174,204 @@ public class StreamedETLWithFrontendProcess .addStep(executeStep) .addStep(resultStep); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Builder processMetaDataBuilder() + { + return (new Builder(defineProcessMetaData(null, null, null, Collections.emptyMap()))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class Builder extends AbstractProcessMetaDataBuilder + { + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Builder(QProcessMetaData processMetaData) + { + super(processMetaData); + } + + + + /******************************************************************************* + ** Fluent setter for extractStepClass + ** + *******************************************************************************/ + public Builder withExtractStepClass(Class extractStepClass) + { + setInputFieldDefaultValue(FIELD_EXTRACT_CODE, new QCodeReference(extractStepClass)); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for transformStepClass + ** + *******************************************************************************/ + public Builder withTransformStepClass(Class transformStepClass) + { + setInputFieldDefaultValue(FIELD_TRANSFORM_CODE, new QCodeReference(transformStepClass)); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for loadStepClass + ** + *******************************************************************************/ + public Builder withLoadStepClass(Class loadStepClass) + { + setInputFieldDefaultValue(FIELD_LOAD_CODE, new QCodeReference(loadStepClass)); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for sourceTable + ** + *******************************************************************************/ + public Builder withSourceTable(String sourceTable) + { + setInputFieldDefaultValue(FIELD_SOURCE_TABLE, sourceTable); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for destinationTable + ** + *******************************************************************************/ + public Builder withDestinationTable(String destinationTable) + { + setInputFieldDefaultValue(FIELD_DESTINATION_TABLE, destinationTable); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for supportsFullValidation + ** + *******************************************************************************/ + public Builder withSupportsFullValidation(Boolean supportsFullValidation) + { + setInputFieldDefaultValue(FIELD_SUPPORTS_FULL_VALIDATION, supportsFullValidation); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for doFullValidation + ** + *******************************************************************************/ + public Builder withDoFullValidation(Boolean doFullValidation) + { + setInputFieldDefaultValue(FIELD_DO_FULL_VALIDATION, doFullValidation); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for defaultQueryFilter + ** + *******************************************************************************/ + public Builder withDefaultQueryFilter(QQueryFilter defaultQueryFilter) + { + setInputFieldDefaultValue(FIELD_DEFAULT_QUERY_FILTER, defaultQueryFilter); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for previewMessage + ** + *******************************************************************************/ + public Builder withPreviewMessage(String previewMessage) + { + setInputFieldDefaultValue(FIELD_PREVIEW_MESSAGE, previewMessage); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public Builder withName(String name) + { + processMetaData.setName(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public Builder withLabel(String name) + { + processMetaData.setLabel(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public Builder withTableName(String tableName) + { + processMetaData.setTableName(tableName); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public Builder withIcon(QIcon icon) + { + processMetaData.setIcon(icon); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Builder withReviewStepRecordFields(List fieldList) + { + QFrontendStepMetaData reviewStep = processMetaData.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW); + for(QFieldMetaData fieldMetaData : fieldList) + { + reviewStep.withRecordListField(fieldMetaData); + } + + return (this); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java index 40525957..ca0525fd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -46,6 +47,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; *******************************************************************************/ public class ExecuteReportStep implements BackendStep { + + /******************************************************************************* + ** + *******************************************************************************/ @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { @@ -70,10 +75,9 @@ public class ExecuteReportStep implements BackendStep new GenerateReportAction().execute(reportInput); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault()); - String datePart = formatter.format(Instant.now()); + String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, report); - runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx"); + runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + ".xlsx"); runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath()); } } @@ -82,4 +86,26 @@ public class ExecuteReportStep implements BackendStep throw (new QException("Error running report", e)); } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, QReportMetaData report) + { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault()); + String datePart = formatter.format(Instant.now()); + + String downloadFileBaseName = runBackendStepInput.getValueString("downloadFileBaseName"); + if(!StringUtils.hasContent(downloadFileBaseName)) + { + downloadFileBaseName = report.getLabel(); + } + + downloadFileBaseName = downloadFileBaseName.replaceAll("/", "-"); + + return (downloadFileBaseName + " - " + datePart); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java new file mode 100644 index 00000000..696c2133 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java @@ -0,0 +1,124 @@ +/* + * 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.processes.implementations.reports; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +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.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Version of PrepareReportStep for a report that runs off a single record. + *******************************************************************************/ +public class PrepareReportForRecordStep extends PrepareReportStep +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + super.run(runBackendStepInput, runBackendStepOutput); + + ////////////////////////////////////////////////////////////////////////////////// + // look for the recordId having been posted to the process - error if not found // + ////////////////////////////////////////////////////////////////////////////////// + Serializable recordId = null; + if("recordIds".equals(runBackendStepInput.getValueString("recordsParam"))) + { + String recordIdsString = runBackendStepInput.getValueString("recordIds"); + String[] recordIdsArray = recordIdsString.split(","); + if(recordIdsArray.length != 1) + { + throw (new QUserFacingException("Exactly 1 record must be selected as input to this report.")); + } + + recordId = recordIdsArray[0]; + } + else + { + throw (new QUserFacingException("No record was selected as input to this report.")); + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // look for the recordI input field on the process - put the input recordId in that field. // + // then remove that input field from the process's inputFieldList // + ///////////////////////////////////////////////////////////////////////////////////////////// + @SuppressWarnings("unchecked") + ArrayList inputFieldList = (ArrayList) runBackendStepOutput.getValue("inputFieldList"); + if(CollectionUtils.nullSafeHasContents(inputFieldList)) + { + Iterator inputFieldListIterator = inputFieldList.iterator(); + while(inputFieldListIterator.hasNext()) + { + QFieldMetaData fieldMetaData = inputFieldListIterator.next(); + if(fieldMetaData.getName().equals(RunReportForRecordProcess.FIELD_RECORD_ID)) + { + runBackendStepOutput.addValue(RunReportForRecordProcess.FIELD_RECORD_ID, recordId); + inputFieldListIterator.remove(); + runBackendStepOutput.addValue("inputFieldList", inputFieldList); + break; + } + } + } + + GetInput getInput = new GetInput(runBackendStepInput.getInstance()); + getInput.setSession(runBackendStepInput.getSession()); + getInput.setTableName(runBackendStepInput.getTableName()); + getInput.setPrimaryKey(recordId); + getInput.setShouldGenerateDisplayValues(true); + GetOutput getOutput = new GetAction().execute(getInput); + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QUserFacingException("The selected record for the report was not found.")); + } + + String reportName = runBackendStepInput.getValueString("reportName"); + QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName); + // runBackendStepOutput.addValue("downloadFileBaseName", runBackendStepInput.getTable().getLabel() + " " + record.getRecordLabel()); + runBackendStepOutput.addValue("downloadFileBaseName", report.getLabel() + " - " + record.getRecordLabel()); + + ///////////////////////////////////////////////////////////////////////////////////// + // if there are no more input fields, then remove the INPUT step from the process. // + ///////////////////////////////////////////////////////////////////////////////////// + inputFieldList = (ArrayList) runBackendStepOutput.getValue("inputFieldList"); + if(!CollectionUtils.nullSafeHasContents(inputFieldList)) + { + removeInputStepFromProcess(runBackendStepOutput); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java index a80af740..47c056b4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java @@ -43,6 +43,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class PrepareReportStep implements BackendStep { + + /******************************************************************************* + ** + *******************************************************************************/ @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { @@ -68,12 +72,22 @@ public class PrepareReportStep implements BackendStep } else { - ////////////////////////////////////////////////////////////// - // no input? re-route the process to skip the input screen // - ////////////////////////////////////////////////////////////// - List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList()); - stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT)); - runBackendStepOutput.getProcessState().setStepList(stepList); + removeInputStepFromProcess(runBackendStepOutput); } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void removeInputStepFromProcess(RunBackendStepOutput runBackendStepOutput) + { + ////////////////////////////////////////////////////////////// + // no input? re-route the process to skip the input screen // + ////////////////////////////////////////////////////////////// + List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList()); + stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT)); + runBackendStepOutput.getProcessState().setStepList(stepList); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java new file mode 100644 index 00000000..cd4c5aca --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java @@ -0,0 +1,167 @@ +/* + * 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.processes.implementations.reports; + + +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +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.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; + + +/******************************************************************************* + ** Definition for Basic process to run a report. + *******************************************************************************/ +public class RunReportForRecordProcess +{ + public static final String PROCESS_NAME = "reports.forRecord"; + + public static final String STEP_NAME_PREPARE = "prepare"; + public static final String STEP_NAME_INPUT = "input"; + public static final String STEP_NAME_EXECUTE = "execute"; + public static final String STEP_NAME_ACCESS = "accessReport"; + + public static final String FIELD_REPORT_NAME = "reportName"; + public static final String FIELD_RECORD_ID = "recordId"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Builder processMetaDataBuilder() + { + return (new Builder(defineProcessMetaData())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessMetaData() + { + QStepMetaData prepareStep = new QBackendStepMetaData() + .withName(STEP_NAME_PREPARE) + .withCode(new QCodeReference(PrepareReportForRecordStep.class)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING)) + .withField(new QFieldMetaData(FIELD_RECORD_ID, QFieldType.STRING))); + + QStepMetaData inputStep = new QFrontendStepMetaData() + .withName(STEP_NAME_INPUT) + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)); + + QStepMetaData executeStep = new QBackendStepMetaData() + .withName(STEP_NAME_EXECUTE) + .withCode(new QCodeReference(ExecuteReportStep.class)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING))); + + QStepMetaData accessStep = new QFrontendStepMetaData() + .withName(STEP_NAME_ACCESS) + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM)); + // .withViewField(new QFieldMetaData("outputFile", QFieldType.STRING)) + // .withViewField(new QFieldMetaData("message", QFieldType.STRING)); + + return new QProcessMetaData() + .withName(PROCESS_NAME) + .addStep(prepareStep) + .addStep(inputStep) + .addStep(executeStep) + .addStep(accessStep); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class Builder extends AbstractProcessMetaDataBuilder + { + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Builder(QProcessMetaData processMetaData) + { + super(processMetaData); + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public Builder withProcessName(String name) + { + processMetaData.setName(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public Builder withTableName(String tableName) + { + processMetaData.setTableName(tableName); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public Builder withIcon(QIcon icon) + { + processMetaData.setIcon(icon); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for reportName + ** + *******************************************************************************/ + public Builder withReportName(String reportName) + { + setInputFieldDefaultValue(RunReportForRecordProcess.FIELD_REPORT_NAME, reportName); + return (this); + } + } +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 30d6f610..2d20dbc6 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -143,9 +144,8 @@ class QInstanceValidatorTest qInstance.setTables(null); qInstance.setProcesses(null); }, - "At least 1 table must be defined", - "Unrecognized table shape for possibleValueSource shape", - "Unrecognized processName for queue"); + true, + "At least 1 table must be defined"); } @@ -162,9 +162,8 @@ class QInstanceValidatorTest qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); }, - "At least 1 table must be defined", - "Unrecognized table shape for possibleValueSource shape", - "Unrecognized processName for queue"); + true, + "At least 1 table must be defined"); } @@ -570,6 +569,40 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionDuplicateName() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("section1", "Section 2", new QIcon("person"), Tier.T2, List.of("name"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section named"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionDuplicateLabel() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("section2", "Section 1", new QIcon("person"), Tier.T2, List.of("name"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section labeled"); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1258,6 +1291,138 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportName() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(null), + "Inconsistent naming for report"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(""), + "Inconsistent naming for report"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName("wrongName"), + "Inconsistent naming for report"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportNoDataSources() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(null), + "At least 1 data source", + "unrecognized dataSourceName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(new ArrayList<>()), + "At least 1 data source", + "unrecognized dataSourceName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceNames() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(null), + "Missing name for a dataSource", + "unrecognized dataSourceName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(""), + "Missing name for a dataSource", + "unrecognized dataSourceName"); + + assertValidationFailureReasons((qInstance) -> + { + List dataSources = new ArrayList<>(qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources()); + dataSources.add(dataSources.get(0)); + qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).setDataSources(dataSources); + }, + "More than one dataSource with name"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceTables() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable("notATable"), + "is not a table in this instance"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(null), + "does not have a sourceTable"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(""), + "does not have a sourceTable"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceTablesFilter() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName(null), + "Missing fieldName for a criteria"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName("notAField"), + "is not a field in this table"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setOperator(null), + "Missing operator for a criteria"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy(null)), + "Missing fieldName for an orderBy"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy("notAField")), + "is not a field in this table"); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceStaticDataSupplier() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference()), + "has both a sourceTable and a staticDataSupplier"); + + assertValidationFailureReasons((qInstance) -> + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA, null)); + }, + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null)); + }, + "is not of the expected type"); + + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java new file mode 100644 index 00000000..57ae10cf --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java @@ -0,0 +1,62 @@ +/* + * 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.processes.implementations.reports; + + +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for BasicRunReportProcess + *******************************************************************************/ +class RunReportForRecordProcessTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRunReport() throws QException + { + QInstance instance = TestUtils.defineInstance(); + TestUtils.insertDefaultShapes(instance); + + RunProcessInput runProcessInput = new RunProcessInput(instance); + runProcessInput.setSession(TestUtils.getMockSession()); + runProcessInput.setProcessName(TestUtils.PROCESS_NAME_RUN_SHAPES_PERSON_REPORT); + runProcessInput.addValue(BasicRunReportProcess.FIELD_REPORT_NAME, TestUtils.REPORT_NAME_SHAPES_PERSON); + runProcessInput.addValue("recordsParam", "recordIds"); + runProcessInput.addValue("recordIds", "1"); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + + // runProcessOutput = new RunProcessAction().execute(runProcessInput); + // assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_ACCESS); + // assertThat(runProcessOutput.getValues()).containsKeys("downloadFileName", "serverFilePath"); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index defd1006..b22f1f00 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -76,6 +76,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; +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.model.metadata.tables.automation.AutomationStatusTracking; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; @@ -91,6 +96,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.basepull.Basepul import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; +import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -118,10 +124,12 @@ public class TestUtils public static final String PROCESS_NAME_INCREASE_BIRTHDATE = "increaseBirthdate"; public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge"; public static final String PROCESS_NAME_BASEPULL = "basepullTest"; + public static final String PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport"; public static final String TABLE_NAME_PERSON_FILE = "personFile"; public static final String TABLE_NAME_PERSON_MEMORY = "personMemory"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; public static final String TABLE_NAME_BASEPULL = "basepullTest"; + public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport"; public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type @@ -167,6 +175,9 @@ public class TestUtils qInstance.addProcess(defineProcessIncreasePersonBirthdate()); qInstance.addProcess(defineProcessBasepull()); + qInstance.addReport(defineShapesPersonsReport()); + qInstance.addProcess(defineShapesPersonReportProcess()); + qInstance.addAutomationProvider(definePollingAutomationProvider()); qInstance.addQueueProvider(defineSqsProvider()); @@ -951,4 +962,52 @@ public class TestUtils .withProcessName(PROCESS_NAME_INCREASE_BIRTHDATE)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QReportMetaData defineShapesPersonsReport() + { + return new QReportMetaData() + .withName(REPORT_NAME_SHAPES_PERSON) + .withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT) + .withInputFields(List.of( + new QFieldMetaData(RunReportForRecordProcess.FIELD_RECORD_ID, QFieldType.INTEGER).withIsRequired(true) + )) + .withDataSources(List.of( + new QReportDataSource() + .withName("persons") + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("favoriteShapeId", QCriteriaOperator.EQUALS, List.of("${input." + RunReportForRecordProcess.FIELD_RECORD_ID + "}"))) + ) + )) + .withViews(List.of( + new QReportView() + .withName("person") + .withDataSourceName("persons") + .withType(ReportType.TABLE) + .withColumns(List.of( + new QReportField().withName("id"), + new QReportField().withName("firstName"), + new QReportField().withName("lastName") + )) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData defineShapesPersonReportProcess() + { + return RunReportForRecordProcess.processMetaDataBuilder() + .withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT) + .withReportName(REPORT_NAME_SHAPES_PERSON) + .withTableName(TestUtils.TABLE_NAME_SHAPE) + .getProcessMetaData(); + } + } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java index c90b7b20..773b61f8 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java @@ -24,8 +24,10 @@ package com.kingsrook.qqq.backend.module.api.model.metadata; import java.io.Serializable; import java.util.HashMap; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.api.APIBackendModule; import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; @@ -379,4 +381,14 @@ public class APIBackendMetaData extends QBackendMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void performValidation(QInstanceValidator qInstanceValidator) + { + qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName()); + } } diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java index 81e4427a..16d98fc9 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java @@ -32,9 +32,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -43,7 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* ** *******************************************************************************/ -@DisabledOnOs(OS.LINUX) +@Disabled // OnOs(OS.LINUX) public class EasyPostApiTest { diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java new file mode 100644 index 00000000..4ac4d52d --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java @@ -0,0 +1,51 @@ +/* + * 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.module.api.model.metadata; + + +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for APIBackendMetaData + *******************************************************************************/ +class APIBackendMetaDataTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + APIBackendMetaData apiBackendMetaData = new APIBackendMetaData() + .withName("test"); + QInstanceValidator qInstanceValidator = new QInstanceValidator(); + apiBackendMetaData.performValidation(qInstanceValidator); + assertEquals(1, qInstanceValidator.getErrors().size()); + assertThat(qInstanceValidator.getErrors()).anyMatch(e -> e.contains("Missing baseUrl")); + } + +} \ No newline at end of file