diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiCustomizer.java
new file mode 100644
index 00000000..65eeff6c
--- /dev/null
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiCustomizer.java
@@ -0,0 +1,60 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.api.implementations.savedreports;
+
+
+import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer;
+import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+
+
+/*******************************************************************************
+ ** API-Customizer for the RenderSavedReport process
+ *******************************************************************************/
+public class RenderSavedReportProcessApiCustomizer implements PreRunApiProcessCustomizer
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void preApiRun(RunProcessInput runProcessInput) throws QException
+ {
+ Integer reportId = runProcessInput.getValueInteger("reportId");
+ if(reportId != null)
+ {
+ QRecord record = new GetAction().executeForRecord(new GetInput(SavedReport.TABLE_NAME).withPrimaryKey(reportId));
+ if(record == null)
+ {
+ throw (new QNotFoundException("Report Id " + reportId + " was not found."));
+ }
+
+ runProcessInput.setCallback(QProcessCallbackFactory.forPrimaryKey("id", reportId));
+ }
+ }
+
+}
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java
new file mode 100644
index 00000000..528adfe8
--- /dev/null
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java
@@ -0,0 +1,105 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.api.implementations.savedreports;
+
+
+import java.io.Serializable;
+import java.util.List;
+import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
+import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
+import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessCustomizers;
+import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput;
+import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer;
+import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData;
+import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer;
+import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue;
+import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue;
+import com.kingsrook.qqq.api.model.openapi.HttpMethod;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
+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.processes.QProcessMetaData;
+
+
+/*******************************************************************************
+ ** Class that helps prepare the RenderSavedReport process for use in an API
+ *******************************************************************************/
+public class RenderSavedReportProcessApiMetaDataEnricher
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static ApiProcessMetaData setupProcessForApi(QProcessMetaData process, String apiName, String initialApiVersion)
+ {
+ ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.ofOrWithNew(process);
+
+ ApiProcessInput input = new ApiProcessInput()
+ .withPathParams(new ApiProcessInputFieldsContainer()
+ .withField(new QFieldMetaData("reportId", QFieldType.INTEGER)
+ .withIsRequired(true)
+ .withSupplementalMetaData(newDefaultApiFieldMetaData("Saved Report Id", 1701))))
+ .withQueryStringParams(new ApiProcessInputFieldsContainer()
+ .withField(new QFieldMetaData("reportFormat", QFieldType.STRING)
+ .withIsRequired(true)
+ .withPossibleValueSourceName(ReportFormatPossibleValueEnum.NAME)
+ .withSupplementalMetaData(newDefaultApiFieldMetaData("Requested file format", "XLSX"))));
+ // todo (when implemented) - probably a JSON doc w/ input values.
+
+ RenderSavedReportProcessApiProcessOutput output = new RenderSavedReportProcessApiProcessOutput();
+
+ ApiProcessMetaData apiProcessMetaData = new ApiProcessMetaData()
+ .withInitialVersion(initialApiVersion)
+ .withCustomizer(ApiProcessCustomizers.PRE_RUN.getRole(), new QCodeReference(RenderSavedReportProcessApiCustomizer.class))
+ .withAsyncMode(ApiProcessMetaData.AsyncMode.OPTIONAL)
+ .withMethod(HttpMethod.GET)
+ .withInput(input)
+ .withOutput(output);
+
+ apiProcessMetaDataContainer.withApiProcessMetaData(apiName, apiProcessMetaData);
+
+ return (apiProcessMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** todo - move to higher-level utility
+ *******************************************************************************/
+ public static ApiFieldMetaDataContainer newDefaultApiFieldMetaData(String description, Serializable example)
+ {
+ ApiFieldMetaData defaultApiFieldMetaData = new ApiFieldMetaData().withDescription(description);
+ ApiFieldMetaDataContainer apiFieldMetaDataContainer = new ApiFieldMetaDataContainer().withDefaultApiFieldMetaData(defaultApiFieldMetaData);
+ if(example instanceof List)
+ {
+ defaultApiFieldMetaData.withExample(new ExampleWithListValue().withValue((List) example));
+ }
+ else
+ {
+ defaultApiFieldMetaData.withExample(new ExampleWithSingleValue().withValue(example));
+ }
+
+ return (apiFieldMetaDataContainer);
+ }
+
+}
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java
new file mode 100644
index 00000000..e8d698b0
--- /dev/null
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java
@@ -0,0 +1,144 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.api.implementations.savedreports;
+
+
+import java.io.File;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.Map;
+import com.kingsrook.qqq.api.model.actions.HttpApiResponse;
+import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface;
+import com.kingsrook.qqq.api.model.openapi.Content;
+import com.kingsrook.qqq.api.model.openapi.Response;
+import com.kingsrook.qqq.api.model.openapi.Schema;
+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.actions.reporting.ReportFormat;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jetty.http.HttpStatus;
+
+
+/*******************************************************************************
+ ** api process output specifier for the RenderSavedReport process
+ *******************************************************************************/
+public class RenderSavedReportProcessApiProcessOutput implements ApiProcessOutputInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException
+ {
+ try
+ {
+ ReportFormat reportFormat = ReportFormat.fromString(runProcessOutput.getValueString("reportFormat"));
+
+ String filePath = runProcessOutput.getValueString("serverFilePath");
+ File file = new File(filePath);
+ if(reportFormat.getIsBinary())
+ {
+ return FileUtils.readFileToByteArray(file);
+ }
+ else
+ {
+ return FileUtils.readFileToString(file, Charset.defaultCharset());
+ }
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error streaming report contents", e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void customizeHttpApiResponse(HttpApiResponse httpApiResponse, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // we don't need anyone else to format our response - assume that we've done so ourselves. //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ httpApiResponse.setNeedsFormattedAsJson(false);
+
+ ReportFormat reportFormat = ReportFormat.fromString(runProcessOutput.getValueString("reportFormat"));
+ httpApiResponse.setContentType(reportFormat.getMimeType());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput)
+ {
+ return (HttpStatus.Code.OK);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Map getSpecResponses(String apiName)
+ {
+ return Map.of(HttpStatus.Code.OK.getCode(), new Response()
+ .withDescription("Report contents in the requested format.")
+ .withContent(Map.of(
+ ReportFormat.JSON.getMimeType(), new Content()
+ .withSchema(new Schema()
+ .withDescription("JSON Report contents")
+ .withExample("""
+ [
+ {"id": 1, "name": "James"},
+ {"id": 2, "name": "Jean-Luc"}
+ ]
+ """)
+ .withType("string")
+ .withFormat("text")),
+ ReportFormat.CSV.getMimeType(), new Content()
+ .withSchema(new Schema()
+ .withDescription("CSV Report contents")
+ .withExample("""
+ "id","name"
+ 1,"James"
+ 2,"Jean-Luc"
+ """)
+ .withType("string")
+ .withFormat("text")),
+ ReportFormat.XLSX.getMimeType(), new Content()
+ .withSchema(new Schema()
+ .withDescription("Excel Report contents")
+ .withType("string")
+ .withFormat("binary"))
+ ))
+ );
+ }
+
+}
diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java
index 1cc32785..4dc34bf4 100644
--- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java
+++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.api;
import java.util.List;
import java.util.function.Consumer;
+import com.kingsrook.qqq.api.implementations.savedreports.RenderSavedReportProcessApiMetaDataEnricher;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer;
@@ -45,6 +46,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
@@ -71,7 +73,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMeta
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
@@ -79,6 +84,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.Mem
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
+import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RenderSavedReportMetaDataProducer;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -112,7 +118,7 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
- public static QInstance defineInstance()
+ public static QInstance defineInstance() throws QException
{
QInstance qInstance = new QInstance();
@@ -133,6 +139,8 @@ public class TestUtils
qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous"));
+ addSavedReports(qInstance);
+
qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer()
.withApiInstanceMetaData(new ApiInstanceMetaData()
.withName(API_NAME)
@@ -161,6 +169,18 @@ public class TestUtils
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void addSavedReports(QInstance qInstance) throws QException
+ {
+ qInstance.add(TablesPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(qInstance));
+ new SavedReportsMetaDataProvider().defineAll(qInstance, MEMORY_BACKEND_NAME, null);
+ RenderSavedReportProcessApiMetaDataEnricher.setupProcessForApi(qInstance.getProcess(RenderSavedReportMetaDataProducer.NAME), API_NAME, V2022_Q4);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -531,6 +551,19 @@ public class TestUtils
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Integer insertSavedReport(SavedReport savedReport) throws QException
+ {
+ InsertInput insertInput = new InsertInput();
+ insertInput.setTableName(SavedReport.TABLE_NAME);
+ insertInput.setRecords(List.of(savedReport.toQRecord()));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ return insertOutput.getRecords().get(0).getValueInteger("id");
+ }
+
+
/*******************************************************************************
**