From 52b64ffbc028737a39090d1aaf672527d6316ae8 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 29 Mar 2024 08:46:41 -0500 Subject: [PATCH] CE-881 - Bridge code for RenderSavedReportProcess in qqq-middleware-api --- ...RenderSavedReportProcessApiCustomizer.java | 60 ++++++++ ...SavedReportProcessApiMetaDataEnricher.java | 105 +++++++++++++ ...derSavedReportProcessApiProcessOutput.java | 144 ++++++++++++++++++ .../java/com/kingsrook/qqq/api/TestUtils.java | 35 ++++- 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiCustomizer.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java 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"); + } + + /******************************************************************************* **