From caaf7e3d9311a30a4b7fd0ecea1c7dff5d31f9a4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Jul 2022 12:49:18 -0500 Subject: [PATCH 1/9] QQQ-26 add table exports --- .circleci/config.yml | 2 + .gitignore | 1 + pom.xml | 12 +- .../javalin/QJavalinImplementation.java | 229 +++++++++++++++--- .../javalin/QJavalinProcessHandler.java | 8 +- .../javalin/QJavalinImplementationTest.java | 102 +++++++- 6 files changed, 314 insertions(+), 40 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fe4c371c..658f75d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,8 @@ commands: name: Run Maven command: | mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >> + - store_artifacts: + path: target/site/jacoco - run: name: Save test results command: | diff --git a/.gitignore b/.gitignore index 39736a21..b65cb041 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +.DS_Store diff --git a/pom.xml b/pom.xml index 70e32c14..037a81e1 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220719.154219-3 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220719.155012-5 test @@ -66,7 +66,7 @@ io.javalin javalin - 3.13.13 + 4.6.1 com.konghq @@ -85,6 +85,12 @@ slf4j-simple 1.7.36 + + org.assertj + assertj-core + 3.23.1 + test + diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 392a58ff..0e5ce3c9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -24,14 +24,21 @@ package com.kingsrook.qqq.backend.javalin; import java.io.File; import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -49,6 +56,8 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInpu import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +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.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; @@ -182,36 +191,46 @@ public class QJavalinImplementation { return (() -> { + ///////////////////// + // metadata routes // + ///////////////////// path("/metaData", () -> { get("/", QJavalinImplementation::metaData); - path("/table/:table", () -> + path("/table/{table}", () -> { get("", QJavalinImplementation::tableMetaData); }); - path("/process/:processName", () -> + path("/process/{processName}", () -> { get("", QJavalinImplementation::processMetaData); }); }); - path("/data", () -> - { - path("/:table", () -> - { - get("/", QJavalinImplementation::dataQuery); - post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. - get("/count", QJavalinImplementation::dataCount); - // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records. - path("/:primaryKey", () -> - { - get("", QJavalinImplementation::dataGet); - patch("", QJavalinImplementation::dataUpdate); - put("", QJavalinImplementation::dataUpdate); // todo - want different semantics?? - delete("", QJavalinImplementation::dataDelete); - }); + ///////////////////////// + // table (data) routes // + ///////////////////////// + path("/data/{table}", () -> + { + get("/", QJavalinImplementation::dataQuery); + post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. + get("/count", QJavalinImplementation::dataCount); + get("/export", QJavalinImplementation::dataExportWithoutFilename); + get("/export/{filename}", QJavalinImplementation::dataExportWithFilename); + + // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records. + path("/{primaryKey}", () -> + { + get("", QJavalinImplementation::dataGet); + patch("", QJavalinImplementation::dataUpdate); + put("", QJavalinImplementation::dataUpdate); // todo - want different semantics?? + delete("", QJavalinImplementation::dataDelete); }); }); + + //////////////////// + // process routes // + //////////////////// path("", QJavalinProcessHandler.getRoutes()); }); } @@ -221,16 +240,16 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - static void setupSession(Context context, AbstractActionInput request) throws QModuleDispatchException + static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); // todo - does this need some per-provider logic actually? mmm... Map authenticationContext = new HashMap<>(); authenticationContext.put("sessionId", context.cookie("sessionId")); QSession session = authenticationModule.createSession(authenticationContext); - request.setSession(session); + input.setSession(session); context.cookie("sessionId", session.getIdReference(), SESSION_COOKIE_AGE); } @@ -357,10 +376,10 @@ public class QJavalinImplementation { try { - String tableName = context.pathParam("table"); - QTableMetaData table = qInstance.getTable(tableName); - String primaryKey = context.pathParam("primaryKey"); - QueryInput queryInput = new QueryInput(qInstance); + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); + QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); queryInput.setTableName(tableName); @@ -549,31 +568,181 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataExportWithFilename(Context context) + { + String filename = context.pathParam("filename"); + dataExport(context, Optional.of(filename)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataExportWithoutFilename(Context context) + { + dataExport(context, Optional.empty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataExport(Context context, Optional optionalFilename) + { + try + { + ////////////////////////////////////////// + // read params from the request context // + ////////////////////////////////////////// + String tableName = context.pathParam("table"); + String format = context.queryParam("format"); + String filter = context.queryParam("filter"); + Integer limit = integerQueryParam(context, "limit"); + + ///////////////////////////////////////////////////////////////////////////////////////// + // if a format query param wasn't given, then try to get file extension from file name // + ///////////////////////////////////////////////////////////////////////////////////////// + if(!StringUtils.hasContent(format) && optionalFilename.isPresent() && StringUtils.hasContent(optionalFilename.get())) + { + String filename = optionalFilename.get(); + if(filename.contains(".")) + { + format = filename.substring(filename.lastIndexOf(".") + 1); + } + } + + ReportFormat reportFormat; + try + { + reportFormat = ReportFormat.fromString(format); + } + catch(QUserFacingException e) + { + handleException(HttpStatus.Code.BAD_REQUEST, context, e); + return; + } + + String filename = optionalFilename.orElse(tableName + "." + reportFormat.toString().toLowerCase(Locale.ROOT)); + + ///////////////////////////////////////////// + // set up the report action's input object // + ///////////////////////////////////////////// + ReportInput reportInput = new ReportInput(qInstance); + setupSession(context, reportInput); + reportInput.setTableName(tableName); + reportInput.setReportFormat(reportFormat); + reportInput.setFilename(filename); + reportInput.setLimit(limit); + + String fields = stringQueryParam(context, "fields"); + if(StringUtils.hasContent(fields)) + { + reportInput.setFieldNames(List.of(fields.split(","))); + } + + if(filter != null) + { + reportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // set up the I/O pipe streams. // + // Critically, we must NOT open the outputStream in a try-with-resources. The thread that writes to // + // the stream must close it when it's done writing. // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + PipedOutputStream pipedOutputStream = new PipedOutputStream(); + PipedInputStream pipedInputStream = new PipedInputStream(); + pipedOutputStream.connect(pipedInputStream); + reportInput.setReportOutputStream(pipedOutputStream); + + ReportAction reportAction = new ReportAction(); + reportAction.preExecute(reportInput); + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // start the async job. // + // Critically, this must happen before the pipedInputStream is passed to the javalin result method // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + new AsyncJobManager().startJob("Javalin>ReportAction", (o) -> + { + try + { + reportAction.execute(reportInput); + return (true); + } + catch(Exception e) + { + pipedOutputStream.write(("Error generating report: " + e.getMessage()).getBytes()); + pipedOutputStream.close(); + return (false); + } + }); + + //////////////////////////////////////////// + // set the response content type & stream // + //////////////////////////////////////////// + context.contentType(reportFormat.getMimeType()); + context.header("Content-Disposition", "filename=" + filename); + context.result(pipedInputStream); + + //////////////////////////////////////////////////////////////////////////////////////////// + // we'd like to check to see if the job failed, and if so, to give the user an error... // + // but if we "block" here, then piped streams seem to never flush, so we deadlock things. // + //////////////////////////////////////////////////////////////////////////////////////////// + // AsyncJobStatus asyncJobStatus = asyncJobManager.waitForJob(jobUUID); + // if(asyncJobStatus.getState().equals(AsyncJobState.ERROR)) + // { + // System.out.println("Well, here we are..."); + // throw (new QUserFacingException("Error running report: " + asyncJobStatus.getCaughtException().getMessage())); + // } + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ public static void handleException(Context context, Exception e) + { + handleException(null, context, e); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void handleException(HttpStatus.Code statusCode, Context context, Exception e) { QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); if(userFacingException != null) { if(userFacingException instanceof QNotFoundException) { - context.status(HttpStatus.NOT_FOUND_404) - .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND).getCode(); + context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); } else { LOG.info("User-facing exception", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode(); + context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); } } else { LOG.warn("Exception in javalin request", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); + int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode(); + context.status(code).result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } } diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 308634a9..1c0775e9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -87,15 +87,15 @@ public class QJavalinProcessHandler { path("/processes", () -> { - path("/:processName", () -> + path("/{processName}", () -> { get("/init", QJavalinProcessHandler::processInit); post("/init", QJavalinProcessHandler::processInit); - path("/:processUUID", () -> + path("/{processUUID}", () -> { - post("/step/:step", QJavalinProcessHandler::processStep); - get("/status/:jobUUID", QJavalinProcessHandler::processStatus); + post("/step/{step}", QJavalinProcessHandler::processStep); + get("/status/{jobUUID}", QJavalinProcessHandler::processStatus); get("/records", QJavalinProcessHandler::processRecords); }); }); diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 459ff2bb..26604c8c 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -27,6 +27,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; @@ -34,6 +35,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -52,8 +54,6 @@ class QJavalinImplementationTest extends QJavalinTestBase { - - /******************************************************************************* ** test the top-level meta-data endpoint ** @@ -269,7 +269,7 @@ class QJavalinImplementationTest extends QJavalinTestBase @Test public void test_dataQueryWithFilter() { - String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; + String filterJson = getFirstNameEqualsTimFilterJSON(); HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); assertEquals(200, response.getStatus()); @@ -284,6 +284,17 @@ class QJavalinImplementationTest extends QJavalinTestBase + /******************************************************************************* + ** + *******************************************************************************/ + private String getFirstNameEqualsTimFilterJSON() + { + return """ + {"criteria":[{"fieldName":"firstName","operator":"EQUALS","values":["Tim"]}]}"""; + } + + + /******************************************************************************* ** test an insert ** @@ -375,4 +386,89 @@ class QJavalinImplementationTest extends QJavalinTestBase })); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportCsvPerFileName() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/MyPersonExport.csv").asString(); + assertEquals(200, response.getStatus()); + assertEquals("text/csv", response.getHeaders().get("Content-Type").get(0)); + assertEquals("filename=MyPersonExport.csv", response.getHeaders().get("Content-Disposition").get(0)); + String[] csvLines = response.getBody().split("\n"); + assertEquals(6, csvLines.length); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportNoFormat() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/").asString(); + assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus()); + assertThat(response.getBody()).contains("Report format was not specified"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportExcelPerFormatQueryParam() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/?format=xlsx").asString(); + assertEquals(200, response.getStatus()); + assertEquals(ReportFormat.XLSX.getMimeType(), response.getHeaders().get("Content-Type").get(0)); + assertEquals("filename=person.xlsx", response.getHeaders().get("Content-Disposition").get(0)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportFilterQueryParam() + { + String filterJson = getFirstNameEqualsTimFilterJSON(); + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/Favorite People.csv?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); + assertEquals("filename=Favorite People.csv", response.getHeaders().get("Content-Disposition").get(0)); + String[] csvLines = response.getBody().split("\n"); + assertEquals(2, csvLines.length); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportFieldsQueryParam() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString(); + String[] csvLines = response.getBody().split("\n"); + assertEquals(""" + "Id","Birth Date\"""", csvLines[0]); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportSupportedFormat() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/?format=docx").asString(); + assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus()); + assertThat(response.getBody()).contains("Unsupported report format"); + } + } From 8010036905d2510df7448b35d2776cc3b95b9993 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 19 Jul 2022 18:24:47 -0500 Subject: [PATCH 2/9] QQQ-27: updates to allow Auth0 to be an authentication model in javalin --- .../javalin/QJavalinImplementation.java | 63 +++++++++++++------ .../qqq/backend/javalin/TestUtils.java | 5 +- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 392a58ff..aaf30649 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; @@ -66,6 +67,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; @@ -97,6 +99,7 @@ public class QJavalinImplementation private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; + private static final String SESSION_ID_COOKIE_NAME = "sessionId"; static QInstance qInstance; @@ -224,15 +227,27 @@ public class QJavalinImplementation static void setupSession(Context context, AbstractActionInput request) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); - // todo - does this need some per-provider logic actually? mmm... - Map authenticationContext = new HashMap<>(); - authenticationContext.put("sessionId", context.cookie("sessionId")); - QSession session = authenticationModule.createSession(authenticationContext); - request.setSession(session); + try + { + Map authenticationContext = new HashMap<>(); + authenticationContext.put(SESSION_ID_COOKIE_NAME, context.cookie(SESSION_ID_COOKIE_NAME)); + QSession session = authenticationModule.createSession(qInstance, authenticationContext); + request.setSession(session); - context.cookie("sessionId", session.getIdReference(), SESSION_COOKIE_AGE); + context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE); + } + catch(QAuthenticationException qae) + { + //////////////////////////////////////////////////////////////////////////////// + // if exception caught, clear out the cookie so the frontend will reauthorize // + //////////////////////////////////////////////////////////////////////////////// + if(authenticationModule instanceof Auth0AuthenticationModule) + { + context.removeCookie(SESSION_ID_COOKIE_NAME); + } + } } @@ -244,7 +259,7 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); @@ -273,9 +288,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -317,9 +332,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -357,9 +372,9 @@ public class QJavalinImplementation { try { - String tableName = context.pathParam("table"); - QTableMetaData table = qInstance.getTable(tableName); - String primaryKey = context.pathParam("primaryKey"); + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); @@ -561,20 +576,32 @@ public class QJavalinImplementation { context.status(HttpStatus.NOT_FOUND_404) .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + return; } else { LOG.info("User-facing exception", e); context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + return; } } else { - LOG.warn("Exception in javalin request", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); + if(e instanceof QAuthenticationException) + { + context.status(HttpStatus.UNAUTHORIZED_401) + .result("{\"error\":\"" + e.getMessage() + "\"}"); + return; + } } + + //////////////////////////////// + // default exception handling // + //////////////////////////////// + LOG.warn("Exception in javalin request", e); + context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) + .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index ecede5f9..818c2b53 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -28,10 +28,11 @@ import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; 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.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; @@ -134,7 +135,7 @@ public class TestUtils { return new QAuthenticationMetaData() .withName("mock") - .withType("mock"); + .withType(QAuthenticationType.MOCK); } From feb01a8b234abe278dfa089f57fdaeaace221fc0 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 21 Jul 2022 11:54:02 -0500 Subject: [PATCH 3/9] QQQ-27: upped qqq-backend-core / rdbms versions --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 70e32c14..96c67854 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 test From b83bac653126622ca4e8c5e86c82b77bb0a0de8c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 22 Jul 2022 18:40:35 -0500 Subject: [PATCH 4/9] QQQ-28 updates for bulk actions --- pom.xml | 4 +- .../javalin/QJavalinProcessHandler.java | 49 +++++++++++++++++-- .../javalin/QJavalinImplementationTest.java | 3 +- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 70e32c14..f8d3c898 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220722.233134-9 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220722.233524-9 test diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 308634a9..60de405e 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; +import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; 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.tables.query.QCriteriaOperator; @@ -53,12 +54,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.state.StateType; +import com.kingsrook.qqq.backend.core.state.TempFileStateProvider; +import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; +import io.javalin.http.UploadedFile; import org.apache.commons.lang.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -216,7 +221,7 @@ public class QJavalinProcessHandler { Throwable rootException = ExceptionUtils.getRootException(exception); LOG.warn("Uncaught Exception in process", exception); - resultForCaller.put("error", "Original error message: " + rootException.getMessage()); + resultForCaller.put("error", "Error message: " + rootException.getMessage()); } } @@ -224,11 +229,14 @@ public class QJavalinProcessHandler /******************************************************************************* ** take values from query-string params, and put them into the run process request - ** todo - better from POST body, or with a "field-" type of prefix?? + ** todo - make query params have a "field-" type of prefix?? ** *******************************************************************************/ private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessInput runProcessInput) throws IOException { + ////////////////////////// + // process query string // + ////////////////////////// for(Map.Entry> queryParam : context.queryParamMap().entrySet()) { String fieldName = queryParam.getKey(); @@ -239,6 +247,37 @@ public class QJavalinProcessHandler } } + //////////////////////////// + // process form/post body // + //////////////////////////// + for(Map.Entry> formParam : context.formParamMap().entrySet()) + { + String fieldName = formParam.getKey(); + List values = formParam.getValue(); + if(CollectionUtils.nullSafeHasContents(values)) + { + runProcessInput.addValue(fieldName, values.get(0)); + } + } + + //////////////////////////// + // process uploaded files // + //////////////////////////// + for(UploadedFile uploadedFile : context.uploadedFiles()) + { + QUploadedFile qUploadedFile = new QUploadedFile(); + qUploadedFile.setBytes(uploadedFile.getContent().readAllBytes()); + qUploadedFile.setFilename(uploadedFile.getFilename()); + + UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); + TempFileStateProvider.getInstance().put(key, qUploadedFile); + LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key); + runProcessInput.addValue("uploadedFileKey", key); + } + + ///////////////////////////////////////////////////////////// + // deal with params that specify an initial-records filter // + ///////////////////////////////////////////////////////////// QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessInput); if(initialRecordsFilter != null) { @@ -331,11 +370,12 @@ public class QJavalinProcessHandler *******************************************************************************/ public static void processStatus(Context context) { - Map resultForCaller = new HashMap<>(); - String processUUID = context.pathParam("processUUID"); String jobUUID = context.pathParam("jobUUID"); + Map resultForCaller = new HashMap<>(); + resultForCaller.put("processUUID", processUUID); + LOG.info("Request for status of process " + processUUID + ", job " + jobUUID); Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); if(optionalJobStatus.isEmpty()) @@ -412,6 +452,7 @@ public class QJavalinProcessHandler Map resultForCaller = new HashMap<>(); List recordPage = CollectionUtils.safelyGetPage(records, skip, limit); resultForCaller.put("records", recordPage); + resultForCaller.put("totalRecords", records.size()); context.result(JsonUtils.toJson(resultForCaller)); } catch(Exception e) diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 459ff2bb..d0f7b78d 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -362,8 +362,7 @@ class QJavalinImplementationTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); - assertEquals(1, jsonObject.getJSONArray("records").length()); - assertEquals(3, jsonObject.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); + assertEquals(1, jsonObject.getInt("deletedRecordCount")); TestUtils.runTestSql("SELECT id FROM person", (rs -> { int rowsFound = 0; while(rs.next()) From fb5dcb6d825b6a15f1888f09d1b09bb32468f30e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 25 Jul 2022 13:52:56 -0500 Subject: [PATCH 5/9] Update core version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 571204d2..a888a933 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220725.183211-13 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220725.183409-11 test From a24c8e6237874172c9fd96dc8931f2d2bbdf8821 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 25 Jul 2022 13:54:42 -0500 Subject: [PATCH 6/9] Fixed exception handling --- .../qqq/backend/javalin/QJavalinImplementation.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index e932fe4a..2dddf8d6 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -761,17 +761,13 @@ public class QJavalinImplementation return; } + //////////////////////////////// + // default exception handling // + //////////////////////////////// LOG.warn("Exception in javalin request", e); int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode(); context.status(code).result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } - - //////////////////////////////// - // default exception handling // - //////////////////////////////// - LOG.warn("Exception in javalin request", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } From fc0a227ce4fe5d9e79f0f873befc84f955e7b2f3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 26 Jul 2022 16:40:32 -0500 Subject: [PATCH 7/9] Feedback from code reviews --- .../kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 6eb021a0..08175cdb 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -272,7 +272,7 @@ public class QJavalinProcessHandler UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); TempFileStateProvider.getInstance().put(key, qUploadedFile); LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key); - runProcessInput.addValue("uploadedFileKey", key); + runProcessInput.addValue(QUploadedFile.DEFAULT_UPLOADED_FILE_FIELD_NAME, key); } ///////////////////////////////////////////////////////////// From be48b07f0b0b1d51d75142b05131d4efd224fd3b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 26 Jul 2022 16:50:02 -0500 Subject: [PATCH 8/9] Update deps --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a888a933..b72529db 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-20220725.183211-13 + 0.2.0-20220726.214150-15 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-20220725.183409-11 + 0.2.0-20220726.214633-12 test From e5ceedd3362a9ca0b732e5e17cf1a0a2c36b0d73 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 27 Jul 2022 23:44:39 -0500 Subject: [PATCH 9/9] QQQ-27: gitignore .env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b65cb041..2c7054c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ *.iml +.env #############################################