From c8acda591933c1bed4a1d241c0312714f277bf7b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 1 Jul 2022 11:49:26 -0500 Subject: [PATCH 01/13] Update version to 0.1.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 928773ae..e9e67df9 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-javalin - 0.0.0 + 0.1.0-SNAPSHOT scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git From de3eabb1cf443cb0b2254197d0162bc25eea1c0c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 6 Jul 2022 13:55:50 -0500 Subject: [PATCH 02/13] QQQ-21 adding process metaData, single-record GET --- pom.xml | 2 +- .../javalin/QJavalinImplementation.java | 88 ++++++++++++- .../javalin/QJavalinImplementationTest.java | 117 ++++++++++++++++-- .../qqq/backend/javalin/TestUtils.java | 63 ++++++++-- 4 files changed, 244 insertions(+), 26 deletions(-) diff --git a/pom.xml b/pom.xml index e9e67df9..174fa7b9 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ com.kingsrook.qqq qqq-backend-core - 0.0.0 + 0.1.0-20220706.184937-2 com.kingsrook.qqq 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 6c68564d..de2d2dff 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -36,6 +36,7 @@ import java.util.concurrent.TimeoutException; import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; +import com.kingsrook.qqq.backend.core.actions.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.QueryAction; import com.kingsrook.qqq.backend.core.actions.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; @@ -43,6 +44,7 @@ import com.kingsrook.qqq.backend.core.actions.UpdateAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest; @@ -51,10 +53,14 @@ import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest; import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataResult; +import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataRequest; +import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataResult; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; +import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest; import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult; @@ -162,10 +168,13 @@ public class QJavalinImplementation path("/metaData", () -> { get("/", QJavalinImplementation::metaData); - path("/:table", () -> + path("/table/:table", () -> { get("", QJavalinImplementation::tableMetaData); - // todo - process meta data - just under tables? or top-level too? maybe move tables to be under /tables/? + }); + path("/process/:process", () -> + { + get("", QJavalinImplementation::processMetaData); }); }); path("/data", () -> @@ -334,7 +343,43 @@ public class QJavalinImplementation ********************************************************************************/ private static void dataGet(Context context) { - context.result("{\"todo\":\"not-done\",\"getResult\":{}}"); + try + { + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); + QueryRequest queryRequest = new QueryRequest(qInstance); + + setupSession(context, queryRequest); + queryRequest.setTableName(tableName); + + /////////////////////////////////////////////////////// + // setup a filter for the primaryKey = the path-pram // + /////////////////////////////////////////////////////// + queryRequest.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName(table.getPrimaryKeyField()) + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(primaryKey)))); + + QueryAction queryAction = new QueryAction(); + QueryResult queryResult = queryAction.execute(queryRequest); + + /////////////////////////////////////////////////////// + // throw a not found error if the record isn't found // + /////////////////////////////////////////////////////// + if(queryResult.getRecords().isEmpty()) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + + context.result(JsonUtils.toJson(queryResult.getRecords().get(0))); + } + catch(Exception e) + { + handleException(context, e); + } } @@ -427,6 +472,29 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void processMetaData(Context context) + { + try + { + ProcessMetaDataRequest processMetaDataRequest = new ProcessMetaDataRequest(qInstance); + setupSession(context, processMetaDataRequest); + processMetaDataRequest.setProcessName(context.pathParam("process")); + ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction(); + ProcessMetaDataResult processMetaDataResult = processMetaDataAction.execute(processMetaDataRequest); + + context.result(JsonUtils.toJson(processMetaDataResult)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -435,9 +503,17 @@ public class QJavalinImplementation QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); if(userFacingException != null) { - LOG.info("User-facing exception", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + if(userFacingException instanceof QNotFoundException) + { + context.status(HttpStatus.NOT_FOUND_404) + .result("{\"error\":\"" + e.getMessage() + "\"}"); + } + else + { + LOG.info("User-facing exception", e); + context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) + .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + } } else { 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 f8376c5c..88011374 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -51,7 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; *******************************************************************************/ class QJavalinImplementationTest { - private static final int PORT = 6262; + private static final int PORT = 6262; private static final String BASE_URL = "http://localhost:" + PORT; @@ -61,7 +61,7 @@ class QJavalinImplementationTest ** *******************************************************************************/ @BeforeAll - public static void beforeAll() throws Exception + public static void beforeAll() { QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); qJavalinImplementation.startJavalinServer(PORT); @@ -111,7 +111,7 @@ class QJavalinImplementationTest @Test public void test_tableMetaData() { - HttpResponse response = Unirest.get(BASE_URL + "/metaData/person").asString(); + HttpResponse response = Unirest.get(BASE_URL + "/metaData/table/person").asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); @@ -136,9 +136,9 @@ class QJavalinImplementationTest @Test public void test_tableMetaData_notFound() { - HttpResponse response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").asString(); + HttpResponse response = Unirest.get(BASE_URL + "/metaData/table/notAnActualTable").asString(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); // todo 404? + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); String error = jsonObject.getString("error"); @@ -147,6 +147,104 @@ class QJavalinImplementationTest + /******************************************************************************* + ** test the process-level meta-data endpoint + ** + *******************************************************************************/ + @Test + public void test_processMetaData() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData/process/greetInteractive").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + JSONObject process = jsonObject.getJSONObject("process"); + assertEquals(4, process.keySet().size(), "Number of mid-level keys"); + assertEquals("greetInteractive", process.getString("name")); + assertEquals("Greet Interactive", process.getString("label")); + assertEquals("person", process.getString("tableName")); + + JSONArray frontendSteps = process.getJSONArray("frontendSteps"); + JSONObject setupStep = frontendSteps.getJSONObject(0); + assertEquals("Setup", setupStep.getString("label")); + JSONArray setupFields = setupStep.getJSONArray("formFields"); + assertEquals(2, setupFields.length()); + assertTrue(setupFields.toList().stream().anyMatch(field -> "greetingPrefix".equals(((Map) field).get("name")))); + } + + + + /******************************************************************************* + ** test the process-level meta-data endpoint for a non-real name + ** + *******************************************************************************/ + @Test + public void test_processMetaData_notFound() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData/process/notAnActualProcess").asString(); + + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + String error = jsonObject.getString("error"); + assertTrue(error.contains("not found")); + } + + + + /******************************************************************************* + ** test a table get (single record) + ** + *******************************************************************************/ + @Test + public void test_dataGet() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/1").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("values")); + assertEquals("person", jsonObject.getString("tableName")); + JSONObject values = jsonObject.getJSONObject("values"); + assertTrue(values.has("firstName")); + assertTrue(values.has("id")); + } + + + + /******************************************************************************* + ** test a table get (single record) for an id that isn't found + ** + *******************************************************************************/ + @Test + public void test_dataGetNotFound() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/98765").asString(); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + String error = jsonObject.getString("error"); + assertEquals("Could not find Person with Id of 98765", error); + } + + + + /******************************************************************************* + ** test a table get (single record) for an id that isn't the expected type + ** + *******************************************************************************/ + @Test + public void test_dataGetWrongIdType() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/not-an-integer").asString(); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + } + + + /******************************************************************************* ** test a table query ** @@ -178,8 +276,8 @@ class QJavalinImplementationTest @Test public void test_dataQueryWithFilter() { - String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; - HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); + String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; + HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); @@ -226,6 +324,7 @@ class QJavalinImplementationTest } + /******************************************************************************* ** test an update ** @@ -293,7 +392,7 @@ class QJavalinImplementationTest ** *******************************************************************************/ @Test - public void test_processGreetInit() throws Exception + public void test_processGreetInit() { HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init") .header("Content-Type", "application/json") @@ -312,7 +411,7 @@ class QJavalinImplementationTest ** *******************************************************************************/ @Test - public void test_processGreetInitWithQueryValues() throws Exception + public void test_processGreetInitWithQueryValues() { HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude") .header("Content-Type", "application/json") 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 a25a5104..f6dcf37a 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; import java.util.List; -import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody; +import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; @@ -34,16 +34,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +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.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; -import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; import static junit.framework.Assert.assertNotNull; @@ -54,6 +53,9 @@ import static junit.framework.Assert.assertNotNull; *******************************************************************************/ public class TestUtils { + public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; + + /******************************************************************************* ** Prime a test database (e.g., h2, in-memory) @@ -101,6 +103,7 @@ public class TestUtils qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); qInstance.addProcess(defineProcessGreetPeople()); + qInstance.addProcess(defineProcessGreetPeopleInteractive()); return (qInstance); } @@ -167,10 +170,10 @@ public class TestUtils return new QProcessMetaData() .withName("greet") .withTableName("person") - .addFunction(new QFunctionMetaData() + .addStep(new QBackendStepMetaData() .withName("prepare") .withCode(new QCodeReference() - .withName(MockFunctionBody.class.getName()) + .withName(MockBackendStep.class.getName()) .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() @@ -185,9 +188,49 @@ public class TestUtils .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) - .withOutputView(new QOutputView() - .withMessageField("outputMessage") - .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) + ); + } + + + + /******************************************************************************* + ** Define an interactive version of the 'greet people' process + *******************************************************************************/ + private static QProcessMetaData defineProcessGreetPeopleInteractive() + { + return new QProcessMetaData() + .withName(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE) + .withTableName("person") + + .addStep(new QFrontendStepMetaData() + .withName("setup") + .withFormField(new QFieldMetaData("greetingPrefix", QFieldType.STRING)) + .withFormField(new QFieldMetaData("greetingSuffix", QFieldType.STRING)) + ) + + .addStep(new QBackendStepMetaData() + .withName("doWork") + .withCode(new QCodeReference() + .withName(MockBackendStep.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withFieldList(List.of( + new QFieldMetaData("greetingPrefix", QFieldType.STRING), + new QFieldMetaData("greetingSuffix", QFieldType.STRING) + ))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName("person") + .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) + ) + .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) + ) + + .addStep(new QFrontendStepMetaData() + .withName("results") + .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)) ); } From 0f2766283e438f2061a26c21d305f3fccb34c172 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 6 Jul 2022 16:07:51 -0500 Subject: [PATCH 03/13] QQQ-21: Updates due to backend core changes --- .../javalin/QJavalinImplementationTest.java | 15 ++++++++++++--- .../qqq/backend/javalin/TestUtils.java | 19 +++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) 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 f8376c5c..f8210573 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -116,15 +117,23 @@ class QJavalinImplementationTest assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + JSONObject table = jsonObject.getJSONObject("table"); assertEquals(4, table.keySet().size(), "Number of mid-level keys"); assertEquals("person", table.getString("name")); assertEquals("Person", table.getString("label")); assertEquals("id", table.getString("primaryKeyField")); + JSONObject fields = table.getJSONObject("fields"); - JSONObject field0 = fields.getJSONObject("id"); - assertEquals("id", field0.getString("name")); - assertEquals("INTEGER", field0.getString("type")); + JSONObject idField = fields.getJSONObject("id"); + assertEquals("id", idField.getString("name")); + assertEquals("INTEGER", idField.getString("type")); + + JSONObject firstNameField = fields.getJSONObject("firstName"); + assertTrue(firstNameField.getBoolean("isRequired"), "First name is required"); + + JSONObject birthDateField = fields.getJSONObject("birthDate"); + assertFalse(birthDateField.getBoolean("isRequired"), "Birth date is not required"); } 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 a25a5104..8f9a4d49 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; import java.util.List; -import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody; +import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; @@ -34,16 +34,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; -import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; import static junit.framework.Assert.assertNotNull; @@ -151,8 +149,8 @@ public class TestUtils .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date")) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date")) - .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name")) - .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name")) + .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name").withIsRequired(true)) + .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) .withField(new QFieldMetaData("email", QFieldType.STRING)); } @@ -167,10 +165,10 @@ public class TestUtils return new QProcessMetaData() .withName("greet") .withTableName("person") - .addFunction(new QFunctionMetaData() + .addStep(new QBackendStepMetaData() .withName("prepare") .withCode(new QCodeReference() - .withName(MockFunctionBody.class.getName()) + .withName(MockBackendStep.class.getName()) .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() @@ -185,9 +183,6 @@ public class TestUtils .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) - .withOutputView(new QOutputView() - .withMessageField("outputMessage") - .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) ); } From ed27a192e8b95e0f255fe1aa5ed64f0613eca1a3 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 6 Jul 2022 16:26:30 -0500 Subject: [PATCH 04/13] Revert "QQQ-21: Updates due to backend core changes" This reverts commit 0f2766283e438f2061a26c21d305f3fccb34c172. --- .../javalin/QJavalinImplementationTest.java | 15 +++------------ .../qqq/backend/javalin/TestUtils.java | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 19 deletions(-) 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 f8210573..f8376c5c 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -37,7 +37,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -117,23 +116,15 @@ class QJavalinImplementationTest assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); - JSONObject table = jsonObject.getJSONObject("table"); assertEquals(4, table.keySet().size(), "Number of mid-level keys"); assertEquals("person", table.getString("name")); assertEquals("Person", table.getString("label")); assertEquals("id", table.getString("primaryKeyField")); - JSONObject fields = table.getJSONObject("fields"); - JSONObject idField = fields.getJSONObject("id"); - assertEquals("id", idField.getString("name")); - assertEquals("INTEGER", idField.getString("type")); - - JSONObject firstNameField = fields.getJSONObject("firstName"); - assertTrue(firstNameField.getBoolean("isRequired"), "First name is required"); - - JSONObject birthDateField = fields.getJSONObject("birthDate"); - assertFalse(birthDateField.getBoolean("isRequired"), "Birth date is not required"); + JSONObject field0 = fields.getJSONObject("id"); + assertEquals("id", field0.getString("name")); + assertEquals("INTEGER", field0.getString("type")); } 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 8f9a4d49..a25a5104 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; import java.util.List; -import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep; +import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; @@ -34,14 +34,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; -import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; import static junit.framework.Assert.assertNotNull; @@ -149,8 +151,8 @@ public class TestUtils .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date")) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date")) - .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name").withIsRequired(true)) - .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true)) + .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name")) + .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name")) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) .withField(new QFieldMetaData("email", QFieldType.STRING)); } @@ -165,10 +167,10 @@ public class TestUtils return new QProcessMetaData() .withName("greet") .withTableName("person") - .addStep(new QBackendStepMetaData() + .addFunction(new QFunctionMetaData() .withName("prepare") .withCode(new QCodeReference() - .withName(MockBackendStep.class.getName()) + .withName(MockFunctionBody.class.getName()) .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() @@ -183,6 +185,9 @@ public class TestUtils .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) + .withOutputView(new QOutputView() + .withMessageField("outputMessage") + .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) ); } From aa001292b80c566a2b2ad3822460bd9f04e5153a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 8 Jul 2022 10:28:46 -0500 Subject: [PATCH 05/13] QQQ-21 methods to run async process stuff --- .circleci/config.yml | 2 +- checkstyle.xml | 2 +- pom.xml | 85 ++++- .../javalin/QJavalinImplementation.java | 306 ++++++++++++++---- .../javalin/QJavalinImplementationTest.java | 278 +++++++++++++++- .../qqq/backend/javalin/TestUtils.java | 168 +++++++++- 6 files changed, 757 insertions(+), 84 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ef02745..fe4c371c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: executor: java17 steps: - run_maven: - maven_subcommand: test + maven_subcommand: verify - slack/notify: event: fail diff --git a/checkstyle.xml b/checkstyle.xml index 76f872ed..f5e7412d 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -181,8 +181,8 @@ --> - + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + pre-unit-test + + prepare-agent + + + jaCoCoArgLine + + + + unit-test-check + + check + + + + ${coverage.haltOnFailure} + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + ${coverage.instructionCoveredRatioMinimum} + + + + + + + + post-unit-test + verify + + report + + + + + + exec-maven-plugin + org.codehaus.mojo + 3.0.0 + + + test-coverage-summary + verify + + exec + + + sh + + -c + + /tmp/$$.headers +xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values +echo +echo "Jacoco coverage summary report:" +echo " See also target/site/jacoco/index.html" +echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html" +echo "------------------------------------------------------------" +paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}' +rm /tmp/$$.headers /tmp/$$.values + ]]> + + + + + + 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 de2d2dff..83e1f65d 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -29,10 +29,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; @@ -41,11 +40,16 @@ import com.kingsrook.qqq.backend.core.actions.QueryAction; import com.kingsrook.qqq.backend.core.actions.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.UpdateAction; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; +import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult; @@ -57,6 +61,7 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMeta import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataResult; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; @@ -73,10 +78,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.state.StateType; +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 com.kingsrook.qqq.backend.core.utils.ValueUtils; import io.javalin.Javalin; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; @@ -105,7 +113,9 @@ public class QJavalinImplementation private static QInstance qInstance; - private static int PORT = 8001; + private static int DEFAULT_PORT = 8001; + + private static int ASYNC_STEP_TIMEOUT_MILLIS = 3_000; @@ -118,7 +128,7 @@ public class QJavalinImplementation // todo - parse args to look up metaData and prime instance // qInstance.addBackend(QMetaDataProvider.getQBackend()); - new QJavalinImplementation(qInstance).startJavalinServer(PORT); + new QJavalinImplementation(qInstance).startJavalinServer(DEFAULT_PORT); } @@ -158,6 +168,26 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + public static void setDefaultPort(int port) + { + QJavalinImplementation.DEFAULT_PORT = port; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void setAsyncStepTimeoutMillis(int asyncStepTimeoutMillis) + { + QJavalinImplementation.ASYNC_STEP_TIMEOUT_MILLIS = asyncStepTimeoutMillis; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -172,7 +202,7 @@ public class QJavalinImplementation { get("", QJavalinImplementation::tableMetaData); }); - path("/process/:process", () -> + path("/process/:processName", () -> { get("", QJavalinImplementation::processMetaData); }); @@ -195,10 +225,16 @@ public class QJavalinImplementation }); path("/processes", () -> { - path("/:process", () -> + path("/:processName", () -> { get("/init", QJavalinImplementation::processInit); - get("/step", QJavalinImplementation::processStep); + post("/init", QJavalinImplementation::processInit); + + path("/:processUUID", () -> + { + post("/step/:step", QJavalinImplementation::processStep); + get("/status/:jobUUID", QJavalinImplementation::processStatus); + }); }); }); }); @@ -481,7 +517,7 @@ public class QJavalinImplementation { ProcessMetaDataRequest processMetaDataRequest = new ProcessMetaDataRequest(qInstance); setupSession(context, processMetaDataRequest); - processMetaDataRequest.setProcessName(context.pathParam("process")); + processMetaDataRequest.setProcessName(context.pathParam("processName")); ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction(); ProcessMetaDataResult processMetaDataResult = processMetaDataAction.execute(processMetaDataRequest); @@ -506,7 +542,7 @@ public class QJavalinImplementation if(userFacingException instanceof QNotFoundException) { context.status(HttpStatus.NOT_FOUND_404) - .result("{\"error\":\"" + e.getMessage() + "\"}"); + .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); } else { @@ -527,15 +563,15 @@ public class QJavalinImplementation /******************************************************************************* ** Returns Integer if context has a valid int query parameter by the given name, - * Returns null if no param (or empty value). - * Throws NumberFormatException for malformed numbers. + ** Returns null if no param (or empty value). + ** Throws QValueException for malformed numbers. *******************************************************************************/ - private static Integer integerQueryParam(Context context, String name) throws NumberFormatException + private static Integer integerQueryParam(Context context, String name) throws QValueException { String value = context.queryParam(name); if(StringUtils.hasContent(value)) { - return (Integer.parseInt(value)); + return (ValueUtils.getValueAsInteger(value)); } return (null); @@ -547,7 +583,7 @@ public class QJavalinImplementation ** Returns String if context has a valid query parameter by the given name, * Returns null if no param (or empty value). *******************************************************************************/ - private static String stringQueryParam(Context context, String name) throws NumberFormatException + private static String stringQueryParam(Context context, String name) { String value = context.queryParam(name); if(StringUtils.hasContent(value)) @@ -566,15 +602,138 @@ public class QJavalinImplementation *******************************************************************************/ private static void processInit(Context context) throws QException { + doProcessInitOrStep(context, null, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep) throws QModuleDispatchException + { + if(processUUID == null) + { + processUUID = UUID.randomUUID().toString(); + } + RunProcessRequest runProcessRequest = new RunProcessRequest(qInstance); setupSession(context, runProcessRequest); - runProcessRequest.setProcessName(context.pathParam("process")); + runProcessRequest.setProcessName(context.pathParam("processName")); runProcessRequest.setCallback(new QJavalinProcessCallback()); + runProcessRequest.setBackendOnly(true); + runProcessRequest.setProcessUUID(processUUID); + runProcessRequest.setStartAfterStep(startAfterStep); + populateRunProcessRequestWithValuesFromContext(context, runProcessRequest); - ///////////////////////////////////////////////////////////////////////////////////// - // 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?? // - ///////////////////////////////////////////////////////////////////////////////////// + LOG.info(startAfterStep == null ? "Initiating process [" + runProcessRequest.getProcessName() + "] [" + processUUID + "]" + : "Resuming process [" + runProcessRequest.getProcessName() + "] [" + processUUID + "] after step [" + startAfterStep + "]"); + + Map resultForCaller = new HashMap<>(); + resultForCaller.put("processUUID", processUUID); + + try + { + //////////////////////////////////////// + // run the process as an async action // + //////////////////////////////////////// + Integer timeout = getTimeoutMillis(context); + RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> + { + runProcessRequest.setAsyncJobCallback(callback); + return (new RunProcessAction().execute(runProcessRequest)); + }); + + LOG.info("Process result error? " + runProcessResult.getException()); + for(QFieldMetaData outputField : qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) + { + LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); + } + serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + } + catch(JobGoingAsyncException jgae) + { + resultForCaller.put("jobUUID", jgae.getJobUUID()); + } + catch(Exception e) + { + ////////////////////////////////////////////////////////////////////////////// + // our other actions in here would do: handleException(context, e); // + // which would return a 500 to the client. // + // but - other process-step actions, they always return a 200, just with an // + // optional error message - so - keep all of the processes consistent. // + ////////////////////////////////////////////////////////////////////////////// + serializeRunProcessExceptionForCaller(resultForCaller, e); + } + + context.result(JsonUtils.toJson(resultForCaller)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Integer getTimeoutMillis(Context context) + { + Integer timeout = integerQueryParam(context, "_qStepTimeoutMillis"); + if(timeout == null) + { + timeout = ASYNC_STEP_TIMEOUT_MILLIS; + } + return timeout; + } + + + + /******************************************************************************* + ** Whether a step finished synchronously or asynchronously, return its data + ** to the caller the same way. + *******************************************************************************/ + private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult) + { + if(runProcessResult.getException().isPresent()) + { + //////////////////////////////////////////////////////////////// + // per code coverage, this path may never actually get hit... // + //////////////////////////////////////////////////////////////// + serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get()); + } + resultForCaller.put("values", runProcessResult.getValues()); + runProcessResult.getProcessState().getNextStepName().ifPresent(lastStep -> resultForCaller.put("nextStep", lastStep)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void serializeRunProcessExceptionForCaller(Map resultForCaller, Exception exception) + { + QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class); + + if(userFacingException != null) + { + LOG.info("User-facing exception in process", userFacingException); + resultForCaller.put("error", userFacingException.getMessage()); // todo - put this somewhere else (make error an object w/ user-facing and/or other error?) + } + else + { + Throwable rootException = ExceptionUtils.getRootException(exception); + LOG.warn("Uncaught Exception in process", exception); + resultForCaller.put("error", "Original error message: " + rootException.getMessage()); + } + } + + + + /******************************************************************************* + ** 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?? + ** + *******************************************************************************/ + private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) + { for(Map.Entry> queryParam : context.queryParamMap().entrySet()) { String fieldName = queryParam.getKey(); @@ -584,61 +743,74 @@ public class QJavalinImplementation runProcessRequest.addValue(fieldName, values.get(0)); } } - - try - { - //////////////////////////////////////////////// - // run the process // - // todo - some "job id" to return to caller? // - //////////////////////////////////////////////// - CompletableFuture future = CompletableFuture.supplyAsync(() -> - { - try - { - LOG.info("Running process [" + runProcessRequest.getProcessName() + "]"); - RunProcessResult runProcessResult = new RunProcessAction().execute(runProcessRequest); - LOG.info("Process result error? " + runProcessResult.getError()); - for(QFieldMetaData outputField : qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) - { - LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); - } - return (runProcessResult); - } - catch(Exception e) - { - LOG.error("Error running future for process", e); - throw (new CompletionException(e)); - } - }); - - Map resultForCaller = new HashMap<>(); - try - { - RunProcessResult runProcessResult = future.get(3, TimeUnit.SECONDS); - resultForCaller.put("error", runProcessResult.getError()); - resultForCaller.put("values", runProcessResult.getValues()); - } - catch(TimeoutException te) - { - resultForCaller.put("jobId", "Job is running asynchronously... job id available in a later version."); - } - context.result(JsonUtils.toJson(resultForCaller)); - } - catch(Exception e) - { - handleException(context, e); - } } /******************************************************************************* - ** Run a step in a process (named in path param :process) + ** Run a step in a process (named in path param :processName) ** *******************************************************************************/ - private static void processStep(Context context) + private static void processStep(Context context) throws QModuleDispatchException { - + String processUUID = context.pathParam("processUUID"); + String lastStep = context.pathParam("step"); + doProcessInitOrStep(context, processUUID, lastStep); } + + + /******************************************************************************* + ** Get status for a currently running process (step) + *******************************************************************************/ + private static void processStatus(Context context) + { + String processUUID = context.pathParam("processUUID"); + String jobUUID = context.pathParam("jobUUID"); + + LOG.info("Request for status of job " + jobUUID); + Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); + if(optionalJobStatus.isEmpty()) + { + handleException(context, new RuntimeException("Could not find status of process step job")); + } + else + { + Map resultForCaller = new HashMap<>(); + AsyncJobStatus jobStatus = optionalJobStatus.get(); + + resultForCaller.put("jobStatus", jobStatus); + LOG.info("Job status is " + jobStatus.getState() + " for " + jobUUID); + + if(jobStatus.getState().equals(AsyncJobState.COMPLETE)) + { + /////////////////////////////////////////////////////////////////////////////////////// + // if the job is complete, get the process result from state provider, and return it // + // this output should look like it did if the job finished synchronously!! // + /////////////////////////////////////////////////////////////////////////////////////// + Optional processState = RunProcessAction.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS)); + if(processState.isPresent()) + { + RunProcessResult runProcessResult = new RunProcessResult(processState.get()); + serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + } + else + { + handleException(context, new RuntimeException("Could not find process results")); + } + } + else if(jobStatus.getState().equals(AsyncJobState.ERROR)) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(jobStatus.getCaughtException() != null) + { + serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException()); + } + } + + context.result(JsonUtils.toJson(resultForCaller)); + } + } } 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 88011374..860d607c 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.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; @@ -37,6 +38,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -54,6 +56,9 @@ class QJavalinImplementationTest private static final int PORT = 6262; private static final String BASE_URL = "http://localhost:" + PORT; + private static final int MORE_THAN_TIMEOUT = 500; + private static final int LESS_THAN_TIMEOUT = 50; + /******************************************************************************* @@ -64,6 +69,7 @@ class QJavalinImplementationTest public static void beforeAll() { QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); + QJavalinImplementation.setAsyncStepTimeoutMillis(250); qJavalinImplementation.startJavalinServer(PORT); } @@ -364,10 +370,7 @@ class QJavalinImplementationTest @Test public void test_dataDelete() throws Exception { - HttpResponse response = Unirest.delete(BASE_URL + "/data/person/3") - .header("Content-Type", "application/json") - .asString(); - + HttpResponse response = Unirest.delete(BASE_URL + "/data/person/3").asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); @@ -394,10 +397,7 @@ class QJavalinImplementationTest @Test public void test_processGreetInit() { - HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init") - .header("Content-Type", "application/json") - .asString(); - + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init").asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); @@ -413,14 +413,268 @@ class QJavalinImplementationTest @Test public void test_processGreetInitWithQueryValues() { - HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude") - .header("Content-Type", "application/json") - .asString(); - + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude").asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage")); } + + + /******************************************************************************* + ** test init'ing a process that goes async + ** + *******************************************************************************/ + @Test + public void test_processInitGoingAsync() throws InterruptedException + { + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP; + HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString(); + + JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String jobUUID = jsonObject.getString("jobUUID"); + assertNotNull(processUUID, "Process UUID should not be null."); + assertNotNull(jobUUID, "Job UUID should not be null"); + + ///////////////////////////////////////////// + // request job status before sleep is done // + ///////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepRunningResponse(response); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + //////////////////////////////////////////////////////// + // request job status again, get back results instead // + //////////////////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepCompleteResponse(response); + } + + + + /******************************************************************************* + ** test init'ing a process that does NOT goes async + ** + *******************************************************************************/ + @Test + public void test_processInitNotGoingAsync() + { + HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + assertProcessStepCompleteResponse(response); + } + + + + /******************************************************************************* + ** test running a step a process that goes async + ** + *******************************************************************************/ + @Test + public void test_processStepGoingAsync() throws InterruptedException + { + ///////////////////////////////////////////// + // first init the process, to get its UUID // + ///////////////////////////////////////////// + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; + HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + + JSONObject jsonObject = assertProcessStepCompleteResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String nextStep = jsonObject.getString("nextStep"); + assertNotNull(processUUID, "Process UUID should not be null."); + assertNotNull(nextStep, "There should be a next step"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) + .header("Content-Type", "application/json").asString(); + + jsonObject = assertProcessStepWentAsyncResponse(response); + String jobUUID = jsonObject.getString("jobUUID"); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + /////////////////////////////// + // third, request job status // + /////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + + jsonObject = assertProcessStepCompleteResponse(response); + String nextStep2 = jsonObject.getString("nextStep"); + assertNotNull(nextStep2, "There be one more next step"); + assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); + } + + + + /******************************************************************************* + ** test running a step a process that does NOT goes async + ** + *******************************************************************************/ + @Test + public void test_processStepNotGoingAsync() + { + ///////////////////////////////////////////// + // first init the process, to get its UUID // + ///////////////////////////////////////////// + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; + HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + + JSONObject jsonObject = assertProcessStepCompleteResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String nextStep = jsonObject.getString("nextStep"); + assertNotNull(nextStep, "There should be a next step"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // second, run the 'nextStep' (the backend step, that sleeps). run it with a short enough sleep so that it won't go async // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) + .header("Content-Type", "application/json").asString(); + + jsonObject = assertProcessStepCompleteResponse(response); + String nextStep2 = jsonObject.getString("nextStep"); + assertNotNull(nextStep2, "There be one more next step"); + assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); + } + + + + /******************************************************************************* + ** test init'ing a process that goes async and then throws + ** + *******************************************************************************/ + @Test + public void test_processInitGoingAsyncThenThrowing() throws InterruptedException + { + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW; + HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString(); + + JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String jobUUID = jsonObject.getString("jobUUID"); + + ///////////////////////////////////////////// + // request job status before sleep is done // + ///////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepRunningResponse(response); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + ///////////////////////////////////////////////////////////// + // request job status again, get back error status instead // + ///////////////////////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepErrorResponse(response); + } + + + + /******************************************************************************* + ** test init'ing a process that does NOT goes async, but throws. + ** + *******************************************************************************/ + @Test + public void test_processInitNotGoingAsyncButThrowing() + { + HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + assertProcessStepErrorResponse(response); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) has gone async, expect what the + ** response should look like + *******************************************************************************/ + private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID"); + assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID"); + + assertFalse(jsonObject.has("values"), "Async-started response should NOT have values"); + assertFalse(jsonObject.has("error"), "Async-started response should NOT have error"); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) is still running, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepRunningResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("jobStatus"), "Step Running response should have a jobStatus"); + + assertFalse(jsonObject.has("values"), "Step Running response should NOT have values"); + assertFalse(jsonObject.has("error"), "Step Running response should NOT have error"); + + assertEquals(AsyncJobState.RUNNING.name(), jsonObject.getJSONObject("jobStatus").getString("state")); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) completes, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepCompleteResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("values"), "Step Complete response should have values"); + + assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID"); + assertFalse(jsonObject.has("error"), "Step Complete response should not have an error"); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) has an error, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepErrorResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("error"), "Step Error response should have an error"); + + assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID"); + assertFalse(jsonObject.has("values"), "Step Error response should not have values"); + + return (jsonObject); + } + } 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 f6dcf37a..5fccd572 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,7 +25,12 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; 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.interfaces.BackendStep; import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepRequest; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepResult; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; @@ -54,6 +59,15 @@ import static junit.framework.Assert.assertNotNull; public class TestUtils { public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; + public static final String PROCESS_NAME_SIMPLE_SLEEP = "simpleSleep"; + public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow"; + public static final String PROCESS_NAME_SLEEP_INTERACTIVE = "sleepInteractive"; + + public static final String STEP_NAME_SLEEPER = "sleeper"; + public static final String STEP_NAME_THROWER = "thrower"; + + public static final String SCREEN_0 = "screen0"; + public static final String SCREEN_1 = "screen1"; @@ -104,6 +118,9 @@ public class TestUtils qInstance.addTable(defineTablePerson()); qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); + qInstance.addProcess(defineProcessSimpleSleep()); + qInstance.addProcess(defineProcessScreenThenSleep()); + qInstance.addProcess(defineProcessSimpleThrow()); return (qInstance); } @@ -175,7 +192,7 @@ public class TestUtils .withCode(new QCodeReference() .withName(MockBackendStep.class.getName()) .withCodeType(QCodeType.JAVA) - .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) .withFieldList(List.of( @@ -213,7 +230,7 @@ public class TestUtils .withCode(new QCodeReference() .withName(MockBackendStep.class.getName()) .withCodeType(QCodeType.JAVA) - .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) .withFieldList(List.of( @@ -234,4 +251,151 @@ public class TestUtils ); } + + + /******************************************************************************* + ** Define a process with just one step that sleeps + *******************************************************************************/ + private static QProcessMetaData defineProcessSimpleSleep() + { + return new QProcessMetaData() + .withName(PROCESS_NAME_SIMPLE_SLEEP) + .addStep(SleeperStep.getMetaData()); + } + + + + /******************************************************************************* + ** Define a process with a screen, then a sleep step + *******************************************************************************/ + private static QProcessMetaData defineProcessScreenThenSleep() + { + return new QProcessMetaData() + .withName(PROCESS_NAME_SLEEP_INTERACTIVE) + .addStep(new QFrontendStepMetaData() + .withName(SCREEN_0) + .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING))) + .addStep(SleeperStep.getMetaData()) + .addStep(new QFrontendStepMetaData() + .withName(SCREEN_1) + .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING))); + } + + + + /******************************************************************************* + ** Define a process with just one step that sleeps and then throws + *******************************************************************************/ + private static QProcessMetaData defineProcessSimpleThrow() + { + return new QProcessMetaData() + .withName(PROCESS_NAME_SIMPLE_THROW) + .addStep(ThrowerStep.getMetaData()); + } + + + + /******************************************************************************* + ** Testing backend step - just sleeps however long you ask it to (or, throws if + ** you don't provide a number of seconds to sleep). + *******************************************************************************/ + public static class SleeperStep implements BackendStep + { + public static final String FIELD_SLEEP_MILLIS = "sleepMillis"; + + + + /******************************************************************************* + ** Execute the backend step - using the request as input, and the result as output. + ** + ******************************************************************************/ + @Override + public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException + { + try + { + Thread.sleep(runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS)); + } + catch(InterruptedException e) + { + throw (new QException("Interrupted while sleeping...")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QBackendStepMetaData getMetaData() + { + return (new QBackendStepMetaData() + .withName(STEP_NAME_SLEEPER) + .withCode(new QCodeReference() + .withName(SleeperStep.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.BACKEND_STEP)) + .withInputData(new QFunctionInputMetaData() + .addField(new QFieldMetaData(SleeperStep.FIELD_SLEEP_MILLIS, QFieldType.INTEGER)))); + } + } + + + + /******************************************************************************* + ** Testing backend step - just throws an exception after however long you ask it to sleep. + *******************************************************************************/ + public static class ThrowerStep implements BackendStep + { + public static final String FIELD_SLEEP_MILLIS = "sleepMillis"; + + + + /******************************************************************************* + ** Execute the backend step - using the request as input, and the result as output. + ** + ******************************************************************************/ + @Override + public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException + { + int sleepMillis; + try + { + sleepMillis = runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS); + } + catch(QValueException qve) + { + sleepMillis = 50; + } + + try + { + Thread.sleep(sleepMillis); + } + catch(InterruptedException e) + { + throw (new QException("Interrupted while sleeping...")); + } + + throw (new QException("I always throw.")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QBackendStepMetaData getMetaData() + { + return (new QBackendStepMetaData() + .withName(STEP_NAME_THROWER) + .withCode(new QCodeReference() + .withName(ThrowerStep.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.BACKEND_STEP)) + .withInputData(new QFunctionInputMetaData() + .addField(new QFieldMetaData(ThrowerStep.FIELD_SLEEP_MILLIS, QFieldType.INTEGER)))); + } + } + } From 69fbc8147fd1c168c68e4fdb77541b569d08d585 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Fri, 8 Jul 2022 15:27:44 -0500 Subject: [PATCH 06/13] QQQ-21: added 'count' action --- pom.xml | 4 +- .../javalin/QJavalinImplementation.java | 44 +++++++++++++++++++ .../javalin/QJavalinImplementationTest.java | 18 ++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 52aa4ef6..f4d71592 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0-20220708.152048-3 + 0.1.0-20220708.195335-5 com.kingsrook.qqq qqq-backend-module-rdbms - 0.0.0 + 0.1.0-20220708.202041-3 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 83e1f65d..ce80c7d4 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.CountAction; import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; @@ -51,6 +52,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest; +import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest; +import com.kingsrook.qqq.backend.core.model.actions.count.CountResult; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult; import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest; @@ -213,6 +216,9 @@ public class QJavalinImplementation { get("/", QJavalinImplementation::dataQuery); post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. + path("/count", () -> { + get("", 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", () -> { @@ -420,6 +426,44 @@ public class QJavalinImplementation + /******************************************************************************* + * + * Filter parameter is a serialized QQueryFilter object, that is to say: + *
+    *   filter=
+    *    {"criteria":[
+    *       {"fieldName":"id","operator":"EQUALS","values":[1]},
+    *       {"fieldName":"name","operator":"IN","values":["Darin","James"]}
+    *     ]
+    *    }
+    * 
+ *******************************************************************************/ + static void dataCount(Context context) + { + try + { + CountRequest countRequest = new CountRequest(qInstance); + setupSession(context, countRequest); + countRequest.setTableName(context.pathParam("table")); + + String filter = stringQueryParam(context, "filter"); + if(filter != null) + { + countRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + } + + CountAction countAction = new CountAction(); + CountResult countResult = countAction.execute(countRequest); + + context.result(JsonUtils.toJson(countResult)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + /******************************************************************************* * * Filter parameter is a serialized QQueryFilter object, that is to say: 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 860d607c..9fcc095e 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -251,6 +251,24 @@ class QJavalinImplementationTest + /******************************************************************************* + ** test a table count + ** + *******************************************************************************/ + @Test + public void test_dataCount() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/count").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("count")); + int count = jsonObject.getInt("count"); + assertEquals(5, count); + } + + + /******************************************************************************* ** test a table query ** From c7aa8292c0112dc46e2541e09835b28b84357ba4 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Fri, 8 Jul 2022 15:30:59 -0500 Subject: [PATCH 07/13] QQQ-21: fixed curly in wrong place --- .../kingsrook/qqq/backend/javalin/QJavalinImplementation.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ce80c7d4..97998401 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -216,7 +216,8 @@ public class QJavalinImplementation { get("/", QJavalinImplementation::dataQuery); post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. - path("/count", () -> { + path("/count", () -> + { get("", 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. From 30770c05b241d5a0bccc50b6db2abb59155253cd Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 8 Jul 2022 16:08:22 -0500 Subject: [PATCH 08/13] QQQ-21 update to take recordIds or filter for initing process; refactor processes into their own javalin class & test --- pom.xml | 2 +- .../javalin/QJavalinImplementation.java | 283 +----------- .../javalin/QJavalinProcessHandler.java | 409 ++++++++++++++++++ .../javalin/QJavalinImplementationTest.java | 325 +------------- .../javalin/QJavalinProcessHandlerTest.java | 398 +++++++++++++++++ .../qqq/backend/javalin/QJavalinTestBase.java | 78 ++++ 6 files changed, 904 insertions(+), 591 deletions(-) create mode 100644 src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java diff --git a/pom.xml b/pom.xml index f4d71592..8f5d6e69 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0-20220708.195335-5 + 0.1.0-20220708.203555-6 com.kingsrook.qqq 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 97998401..ec05e05c 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -29,24 +29,15 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.actions.CountAction; import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.QueryAction; -import com.kingsrook.qqq.backend.core.actions.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.UpdateAction; -import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; -import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; -import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; -import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; -import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; @@ -64,9 +55,6 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMeta import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataResult; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; @@ -75,15 +63,11 @@ import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult; import com.kingsrook.qqq.backend.core.model.actions.update.UpdateRequest; import com.kingsrook.qqq.backend.core.model.actions.update.UpdateResult; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface; -import com.kingsrook.qqq.backend.core.state.StateType; -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; @@ -114,12 +98,11 @@ public class QJavalinImplementation private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; - private static QInstance qInstance; + protected static QInstance qInstance; private static int DEFAULT_PORT = 8001; - private static int ASYNC_STEP_TIMEOUT_MILLIS = 3_000; - + private static Javalin service; /******************************************************************************* @@ -165,11 +148,20 @@ public class QJavalinImplementation { // todo port from arg // todo base path from arg? - Javalin service = Javalin.create().start(port); + service = Javalin.create().start(port); service.routes(getRoutes()); } + /******************************************************************************* + ** + *******************************************************************************/ + void stopJavalinServer() + { + service.stop(); + } + + /******************************************************************************* ** @@ -181,16 +173,6 @@ public class QJavalinImplementation - /******************************************************************************* - ** - *******************************************************************************/ - public static void setAsyncStepTimeoutMillis(int asyncStepTimeoutMillis) - { - QJavalinImplementation.ASYNC_STEP_TIMEOUT_MILLIS = asyncStepTimeoutMillis; - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -230,20 +212,7 @@ public class QJavalinImplementation }); }); }); - path("/processes", () -> - { - path("/:processName", () -> - { - get("/init", QJavalinImplementation::processInit); - post("/init", QJavalinImplementation::processInit); - - path("/:processUUID", () -> - { - post("/step/:step", QJavalinImplementation::processStep); - get("/status/:jobUUID", QJavalinImplementation::processStatus); - }); - }); - }); + path("", QJavalinProcessHandler.getRoutes()); }); } @@ -252,7 +221,7 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - private static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException + static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); @@ -465,6 +434,7 @@ public class QJavalinImplementation } + /******************************************************************************* * * Filter parameter is a serialized QQueryFilter object, that is to say: @@ -579,7 +549,7 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - private static void handleException(Context context, Exception e) + public static void handleException(Context context, Exception e) { QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); if(userFacingException != null) @@ -611,7 +581,7 @@ public class QJavalinImplementation ** Returns null if no param (or empty value). ** Throws QValueException for malformed numbers. *******************************************************************************/ - private static Integer integerQueryParam(Context context, String name) throws QValueException + public static Integer integerQueryParam(Context context, String name) throws QValueException { String value = context.queryParam(name); if(StringUtils.hasContent(value)) @@ -639,223 +609,4 @@ public class QJavalinImplementation return (null); } - - - /******************************************************************************* - ** Init a process (named in path param :process) - ** - *******************************************************************************/ - private static void processInit(Context context) throws QException - { - doProcessInitOrStep(context, null, null); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep) throws QModuleDispatchException - { - if(processUUID == null) - { - processUUID = UUID.randomUUID().toString(); - } - - RunProcessRequest runProcessRequest = new RunProcessRequest(qInstance); - setupSession(context, runProcessRequest); - runProcessRequest.setProcessName(context.pathParam("processName")); - runProcessRequest.setCallback(new QJavalinProcessCallback()); - runProcessRequest.setBackendOnly(true); - runProcessRequest.setProcessUUID(processUUID); - runProcessRequest.setStartAfterStep(startAfterStep); - populateRunProcessRequestWithValuesFromContext(context, runProcessRequest); - - LOG.info(startAfterStep == null ? "Initiating process [" + runProcessRequest.getProcessName() + "] [" + processUUID + "]" - : "Resuming process [" + runProcessRequest.getProcessName() + "] [" + processUUID + "] after step [" + startAfterStep + "]"); - - Map resultForCaller = new HashMap<>(); - resultForCaller.put("processUUID", processUUID); - - try - { - //////////////////////////////////////// - // run the process as an async action // - //////////////////////////////////////// - Integer timeout = getTimeoutMillis(context); - RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> - { - runProcessRequest.setAsyncJobCallback(callback); - return (new RunProcessAction().execute(runProcessRequest)); - }); - - LOG.info("Process result error? " + runProcessResult.getException()); - for(QFieldMetaData outputField : qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) - { - LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); - } - serializeRunProcessResultForCaller(resultForCaller, runProcessResult); - } - catch(JobGoingAsyncException jgae) - { - resultForCaller.put("jobUUID", jgae.getJobUUID()); - } - catch(Exception e) - { - ////////////////////////////////////////////////////////////////////////////// - // our other actions in here would do: handleException(context, e); // - // which would return a 500 to the client. // - // but - other process-step actions, they always return a 200, just with an // - // optional error message - so - keep all of the processes consistent. // - ////////////////////////////////////////////////////////////////////////////// - serializeRunProcessExceptionForCaller(resultForCaller, e); - } - - context.result(JsonUtils.toJson(resultForCaller)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static Integer getTimeoutMillis(Context context) - { - Integer timeout = integerQueryParam(context, "_qStepTimeoutMillis"); - if(timeout == null) - { - timeout = ASYNC_STEP_TIMEOUT_MILLIS; - } - return timeout; - } - - - - /******************************************************************************* - ** Whether a step finished synchronously or asynchronously, return its data - ** to the caller the same way. - *******************************************************************************/ - private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult) - { - if(runProcessResult.getException().isPresent()) - { - //////////////////////////////////////////////////////////////// - // per code coverage, this path may never actually get hit... // - //////////////////////////////////////////////////////////////// - serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get()); - } - resultForCaller.put("values", runProcessResult.getValues()); - runProcessResult.getProcessState().getNextStepName().ifPresent(lastStep -> resultForCaller.put("nextStep", lastStep)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static void serializeRunProcessExceptionForCaller(Map resultForCaller, Exception exception) - { - QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class); - - if(userFacingException != null) - { - LOG.info("User-facing exception in process", userFacingException); - resultForCaller.put("error", userFacingException.getMessage()); // todo - put this somewhere else (make error an object w/ user-facing and/or other error?) - } - else - { - Throwable rootException = ExceptionUtils.getRootException(exception); - LOG.warn("Uncaught Exception in process", exception); - resultForCaller.put("error", "Original error message: " + rootException.getMessage()); - } - } - - - - /******************************************************************************* - ** 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?? - ** - *******************************************************************************/ - private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) - { - for(Map.Entry> queryParam : context.queryParamMap().entrySet()) - { - String fieldName = queryParam.getKey(); - List values = queryParam.getValue(); - if(CollectionUtils.nullSafeHasContents(values)) - { - runProcessRequest.addValue(fieldName, values.get(0)); - } - } - } - - - - /******************************************************************************* - ** Run a step in a process (named in path param :processName) - ** - *******************************************************************************/ - private static void processStep(Context context) throws QModuleDispatchException - { - String processUUID = context.pathParam("processUUID"); - String lastStep = context.pathParam("step"); - doProcessInitOrStep(context, processUUID, lastStep); - } - - - - /******************************************************************************* - ** Get status for a currently running process (step) - *******************************************************************************/ - private static void processStatus(Context context) - { - String processUUID = context.pathParam("processUUID"); - String jobUUID = context.pathParam("jobUUID"); - - LOG.info("Request for status of job " + jobUUID); - Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); - if(optionalJobStatus.isEmpty()) - { - handleException(context, new RuntimeException("Could not find status of process step job")); - } - else - { - Map resultForCaller = new HashMap<>(); - AsyncJobStatus jobStatus = optionalJobStatus.get(); - - resultForCaller.put("jobStatus", jobStatus); - LOG.info("Job status is " + jobStatus.getState() + " for " + jobUUID); - - if(jobStatus.getState().equals(AsyncJobState.COMPLETE)) - { - /////////////////////////////////////////////////////////////////////////////////////// - // if the job is complete, get the process result from state provider, and return it // - // this output should look like it did if the job finished synchronously!! // - /////////////////////////////////////////////////////////////////////////////////////// - Optional processState = RunProcessAction.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS)); - if(processState.isPresent()) - { - RunProcessResult runProcessResult = new RunProcessResult(processState.get()); - serializeRunProcessResultForCaller(resultForCaller, runProcessResult); - } - else - { - handleException(context, new RuntimeException("Could not find process results")); - } - } - else if(jobStatus.getState().equals(AsyncJobState.ERROR)) - { - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller // - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(jobStatus.getCaughtException() != null) - { - serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException()); - } - } - - context.result(JsonUtils.toJson(resultForCaller)); - } - } } diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java new file mode 100644 index 00000000..b2e007ee --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -0,0 +1,409 @@ +/* + * 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.javalin; + + +import java.io.IOException; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.RunProcessAction; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; +import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException; +import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback; +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.RunProcessRequest; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; +import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.state.StateType; +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 org.apache.commons.lang.NotImplementedException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.path; +import static io.javalin.apibuilder.ApiBuilder.post; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QJavalinProcessHandler +{ + private static final Logger LOG = LogManager.getLogger(QJavalinProcessHandler.class); + + private static int ASYNC_STEP_TIMEOUT_MILLIS = 3_000; + + + + /******************************************************************************* + ** Define the routes + *******************************************************************************/ + public static EndpointGroup getRoutes() + { + return (() -> + { + path("/processes", () -> + { + path("/:processName", () -> + { + get("/init", QJavalinProcessHandler::processInit); + post("/init", QJavalinProcessHandler::processInit); + + path("/:processUUID", () -> + { + post("/step/:step", QJavalinProcessHandler::processStep); + get("/status/:jobUUID", QJavalinProcessHandler::processStatus); + }); + }); + }); + }); + } + + + + /******************************************************************************* + ** Init a process (named in path param :process) + ** + *******************************************************************************/ + public static void processInit(Context context) throws QException + { + doProcessInitOrStep(context, null, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep) + { + Map resultForCaller = new HashMap<>(); + + try + { + if(processUUID == null) + { + processUUID = UUID.randomUUID().toString(); + } + resultForCaller.put("processUUID", processUUID); + + String processName = context.pathParam("processName"); + LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]" + : "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]"); + + RunProcessRequest runProcessRequest = new RunProcessRequest(QJavalinImplementation.qInstance); + QJavalinImplementation.setupSession(context, runProcessRequest); + runProcessRequest.setProcessName(processName); + runProcessRequest.setFrontendStepBehavior(RunProcessRequest.FrontendStepBehavior.BREAK); + runProcessRequest.setProcessUUID(processUUID); + runProcessRequest.setStartAfterStep(startAfterStep); + populateRunProcessRequestWithValuesFromContext(context, runProcessRequest); + + try + { + //////////////////////////////////////// + // run the process as an async action // + //////////////////////////////////////// + Integer timeout = getTimeoutMillis(context); + RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> + { + runProcessRequest.setAsyncJobCallback(callback); + return (new RunProcessAction().execute(runProcessRequest)); + }); + + LOG.info("Process result error? " + runProcessResult.getException()); + for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) + { + LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); + } + serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + } + catch(JobGoingAsyncException jgae) + { + resultForCaller.put("jobUUID", jgae.getJobUUID()); + } + } + catch(Exception e) + { + ////////////////////////////////////////////////////////////////////////////// + // our other actions in here would do: handleException(context, e); // + // which would return a 500 to the client. // + // but - other process-step actions, they always return a 200, just with an // + // optional error message - so - keep all of the processes consistent. // + ////////////////////////////////////////////////////////////////////////////// + serializeRunProcessExceptionForCaller(resultForCaller, e); + } + + context.result(JsonUtils.toJson(resultForCaller)); + } + + + + /******************************************************************************* + ** Whether a step finished synchronously or asynchronously, return its data + ** to the caller the same way. + *******************************************************************************/ + private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult) + { + if(runProcessResult.getException().isPresent()) + { + //////////////////////////////////////////////////////////////// + // per code coverage, this path may never actually get hit... // + //////////////////////////////////////////////////////////////// + serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get()); + } + resultForCaller.put("values", runProcessResult.getValues()); + runProcessResult.getProcessState().getNextStepName().ifPresent(lastStep -> resultForCaller.put("nextStep", lastStep)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void serializeRunProcessExceptionForCaller(Map resultForCaller, Exception exception) + { + QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class); + + if(userFacingException != null) + { + LOG.info("User-facing exception in process", userFacingException); + resultForCaller.put("error", userFacingException.getMessage()); // todo - put this somewhere else (make error an object w/ user-facing and/or other error?) + } + else + { + Throwable rootException = ExceptionUtils.getRootException(exception); + LOG.warn("Uncaught Exception in process", exception); + resultForCaller.put("error", "Original error message: " + rootException.getMessage()); + } + } + + + + /******************************************************************************* + ** 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?? + ** + *******************************************************************************/ + private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) throws IOException + { + for(Map.Entry> queryParam : context.queryParamMap().entrySet()) + { + String fieldName = queryParam.getKey(); + List values = queryParam.getValue(); + if(CollectionUtils.nullSafeHasContents(values)) + { + runProcessRequest.addValue(fieldName, values.get(0)); + } + } + + QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessRequest); + if(initialRecordsFilter != null) + { + runProcessRequest.setCallback(new QProcessCallback() + { + @Override + public QQueryFilter getQueryFilter() + { + return (initialRecordsFilter); + } + + + + @Override + public Map getFieldValues(List fields) + { + return (Collections.emptyMap()); + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessRequest runProcessRequest) throws IOException + { + QInstance instance = runProcessRequest.getInstance(); + QProcessMetaData process = instance.getProcess(runProcessRequest.getProcessName()); + QTableMetaData table = instance.getTable(process.getTableName()); + + if(table == null) + { + LOG.info("No table found in process - so not building an init records filter."); + return (null); + } + String primaryKeyField = table.getPrimaryKeyField(); + + String recordsParam = context.queryParam("recordsParam"); + if(StringUtils.hasContent(recordsParam)) + { + @SuppressWarnings("ConstantConditions") + String paramValue = context.queryParam(recordsParam); + if(!StringUtils.hasContent(paramValue)) + { + throw (new IllegalArgumentException("Missing value in query parameter: " + recordsParam + " (which was specified as the recordsParam)")); + } + + switch(recordsParam) + { + case "recordIds": + @SuppressWarnings("ConstantConditions") + Serializable[] idStrings = context.queryParam(recordsParam).split(","); + return (new QQueryFilter().withCriteria(new QFilterCriteria() + .withFieldName(primaryKeyField) + .withOperator(QCriteriaOperator.IN) + .withValues(Arrays.stream(idStrings).toList()))); + case "filterJSON": + return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class)); + case "filterId": + // return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class)); + throw (new NotImplementedException("Saved filters are not yet implemented.")); + default: + throw (new IllegalArgumentException("Unrecognized value [" + recordsParam + "] for query parameter: recordsParam")); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Run a step in a process (named in path param :processName) + ** + *******************************************************************************/ + public static void processStep(Context context) throws QModuleDispatchException + { + String processUUID = context.pathParam("processUUID"); + String lastStep = context.pathParam("step"); + doProcessInitOrStep(context, processUUID, lastStep); + } + + + + /******************************************************************************* + ** Get status for a currently running process (step) + *******************************************************************************/ + public static void processStatus(Context context) + { + String processUUID = context.pathParam("processUUID"); + String jobUUID = context.pathParam("jobUUID"); + + LOG.info("Request for status of job " + jobUUID); + Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); + if(optionalJobStatus.isEmpty()) + { + QJavalinImplementation.handleException(context, new RuntimeException("Could not find status of process step job")); + } + else + { + Map resultForCaller = new HashMap<>(); + AsyncJobStatus jobStatus = optionalJobStatus.get(); + + resultForCaller.put("jobStatus", jobStatus); + LOG.info("Job status is " + jobStatus.getState() + " for " + jobUUID); + + if(jobStatus.getState().equals(AsyncJobState.COMPLETE)) + { + /////////////////////////////////////////////////////////////////////////////////////// + // if the job is complete, get the process result from state provider, and return it // + // this output should look like it did if the job finished synchronously!! // + /////////////////////////////////////////////////////////////////////////////////////// + Optional processState = RunProcessAction.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS)); + if(processState.isPresent()) + { + RunProcessResult runProcessResult = new RunProcessResult(processState.get()); + serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + } + else + { + QJavalinImplementation.handleException(context, new RuntimeException("Could not find process results")); + } + } + else if(jobStatus.getState().equals(AsyncJobState.ERROR)) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(jobStatus.getCaughtException() != null) + { + serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException()); + } + } + + context.result(JsonUtils.toJson(resultForCaller)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void setAsyncStepTimeoutMillis(int asyncStepTimeoutMillis) + { + ASYNC_STEP_TIMEOUT_MILLIS = asyncStepTimeoutMillis; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Integer getTimeoutMillis(Context context) + { + Integer timeout = QJavalinImplementation.integerQueryParam(context, "_qStepTimeoutMillis"); + if(timeout == null) + { + timeout = ASYNC_STEP_TIMEOUT_MILLIS; + } + return timeout; + } + +} 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 9fcc095e..6d002780 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -27,18 +27,14 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; import org.eclipse.jetty.http.HttpStatus; import org.json.JSONArray; import org.json.JSONObject; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -51,39 +47,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; ** and actually makes http requests into it. ** *******************************************************************************/ -class QJavalinImplementationTest +class QJavalinImplementationTest extends QJavalinTestBase { - private static final int PORT = 6262; - private static final String BASE_URL = "http://localhost:" + PORT; - private static final int MORE_THAN_TIMEOUT = 500; - private static final int LESS_THAN_TIMEOUT = 50; - - - - /******************************************************************************* - ** Before the class (all) runs, start a javalin server. - ** - *******************************************************************************/ - @BeforeAll - public static void beforeAll() - { - QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); - QJavalinImplementation.setAsyncStepTimeoutMillis(250); - qJavalinImplementation.startJavalinServer(PORT); - } - - - - /******************************************************************************* - ** Fully rebuild the test-database before each test runs, for completely known state. - ** - *******************************************************************************/ - @BeforeEach - public void beforeEach() throws Exception - { - TestUtils.primeTestDatabase(); - } @@ -406,293 +372,4 @@ class QJavalinImplementationTest })); } - - - /******************************************************************************* - ** test running a process - ** - *******************************************************************************/ - @Test - public void test_processGreetInit() - { - HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init").asString(); - assertEquals(200, response.getStatus()); - JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); - assertNotNull(jsonObject); - assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage")); - } - - - - /******************************************************************************* - ** test running a process with field values on the query string - ** - *******************************************************************************/ - @Test - public void test_processGreetInitWithQueryValues() - { - HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude").asString(); - assertEquals(200, response.getStatus()); - JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); - assertNotNull(jsonObject); - assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage")); - } - - - - /******************************************************************************* - ** test init'ing a process that goes async - ** - *******************************************************************************/ - @Test - public void test_processInitGoingAsync() throws InterruptedException - { - String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP; - HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString(); - - JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); - String processUUID = jsonObject.getString("processUUID"); - String jobUUID = jsonObject.getString("jobUUID"); - assertNotNull(processUUID, "Process UUID should not be null."); - assertNotNull(jobUUID, "Job UUID should not be null"); - - ///////////////////////////////////////////// - // request job status before sleep is done // - ///////////////////////////////////////////// - response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); - jsonObject = assertProcessStepRunningResponse(response); - - /////////////////////////////////// - // sleep, to let that job finish // - /////////////////////////////////// - Thread.sleep(MORE_THAN_TIMEOUT); - - //////////////////////////////////////////////////////// - // request job status again, get back results instead // - //////////////////////////////////////////////////////// - response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); - jsonObject = assertProcessStepCompleteResponse(response); - } - - - - /******************************************************************************* - ** test init'ing a process that does NOT goes async - ** - *******************************************************************************/ - @Test - public void test_processInitNotGoingAsync() - { - HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) - .header("Content-Type", "application/json").asString(); - assertProcessStepCompleteResponse(response); - } - - - - /******************************************************************************* - ** test running a step a process that goes async - ** - *******************************************************************************/ - @Test - public void test_processStepGoingAsync() throws InterruptedException - { - ///////////////////////////////////////////// - // first init the process, to get its UUID // - ///////////////////////////////////////////// - String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; - HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT) - .header("Content-Type", "application/json").asString(); - - JSONObject jsonObject = assertProcessStepCompleteResponse(response); - String processUUID = jsonObject.getString("processUUID"); - String nextStep = jsonObject.getString("nextStep"); - assertNotNull(processUUID, "Process UUID should not be null."); - assertNotNull(nextStep, "There should be a next step"); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) - .header("Content-Type", "application/json").asString(); - - jsonObject = assertProcessStepWentAsyncResponse(response); - String jobUUID = jsonObject.getString("jobUUID"); - - /////////////////////////////////// - // sleep, to let that job finish // - /////////////////////////////////// - Thread.sleep(MORE_THAN_TIMEOUT); - - /////////////////////////////// - // third, request job status // - /////////////////////////////// - response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); - - jsonObject = assertProcessStepCompleteResponse(response); - String nextStep2 = jsonObject.getString("nextStep"); - assertNotNull(nextStep2, "There be one more next step"); - assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); - } - - - - /******************************************************************************* - ** test running a step a process that does NOT goes async - ** - *******************************************************************************/ - @Test - public void test_processStepNotGoingAsync() - { - ///////////////////////////////////////////// - // first init the process, to get its UUID // - ///////////////////////////////////////////// - String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; - HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) - .header("Content-Type", "application/json").asString(); - - JSONObject jsonObject = assertProcessStepCompleteResponse(response); - String processUUID = jsonObject.getString("processUUID"); - String nextStep = jsonObject.getString("nextStep"); - assertNotNull(nextStep, "There should be a next step"); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // second, run the 'nextStep' (the backend step, that sleeps). run it with a short enough sleep so that it won't go async // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) - .header("Content-Type", "application/json").asString(); - - jsonObject = assertProcessStepCompleteResponse(response); - String nextStep2 = jsonObject.getString("nextStep"); - assertNotNull(nextStep2, "There be one more next step"); - assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); - } - - - - /******************************************************************************* - ** test init'ing a process that goes async and then throws - ** - *******************************************************************************/ - @Test - public void test_processInitGoingAsyncThenThrowing() throws InterruptedException - { - String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW; - HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString(); - - JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); - String processUUID = jsonObject.getString("processUUID"); - String jobUUID = jsonObject.getString("jobUUID"); - - ///////////////////////////////////////////// - // request job status before sleep is done // - ///////////////////////////////////////////// - response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); - jsonObject = assertProcessStepRunningResponse(response); - - /////////////////////////////////// - // sleep, to let that job finish // - /////////////////////////////////// - Thread.sleep(MORE_THAN_TIMEOUT); - - ///////////////////////////////////////////////////////////// - // request job status again, get back error status instead // - ///////////////////////////////////////////////////////////// - response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); - jsonObject = assertProcessStepErrorResponse(response); - } - - - - /******************************************************************************* - ** test init'ing a process that does NOT goes async, but throws. - ** - *******************************************************************************/ - @Test - public void test_processInitNotGoingAsyncButThrowing() - { - HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) - .header("Content-Type", "application/json").asString(); - assertProcessStepErrorResponse(response); - } - - - - /******************************************************************************* - ** every time a process step (sync or async) has gone async, expect what the - ** response should look like - *******************************************************************************/ - private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response) - { - assertEquals(200, response.getStatus()); - JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); - - assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID"); - assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID"); - - assertFalse(jsonObject.has("values"), "Async-started response should NOT have values"); - assertFalse(jsonObject.has("error"), "Async-started response should NOT have error"); - - return (jsonObject); - } - - - - /******************************************************************************* - ** every time a process step (sync or async) is still running, expect certain things - ** to be (and not to be) in the json response. - *******************************************************************************/ - private JSONObject assertProcessStepRunningResponse(HttpResponse response) - { - assertEquals(200, response.getStatus()); - JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); - - assertTrue(jsonObject.has("jobStatus"), "Step Running response should have a jobStatus"); - - assertFalse(jsonObject.has("values"), "Step Running response should NOT have values"); - assertFalse(jsonObject.has("error"), "Step Running response should NOT have error"); - - assertEquals(AsyncJobState.RUNNING.name(), jsonObject.getJSONObject("jobStatus").getString("state")); - - return (jsonObject); - } - - - - /******************************************************************************* - ** every time a process step (sync or async) completes, expect certain things - ** to be (and not to be) in the json response. - *******************************************************************************/ - private JSONObject assertProcessStepCompleteResponse(HttpResponse response) - { - assertEquals(200, response.getStatus()); - JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); - - assertTrue(jsonObject.has("values"), "Step Complete response should have values"); - - assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID"); - assertFalse(jsonObject.has("error"), "Step Complete response should not have an error"); - - return (jsonObject); - } - - - - /******************************************************************************* - ** every time a process step (sync or async) has an error, expect certain things - ** to be (and not to be) in the json response. - *******************************************************************************/ - private JSONObject assertProcessStepErrorResponse(HttpResponse response) - { - assertEquals(200, response.getStatus()); - JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); - - assertTrue(jsonObject.has("error"), "Step Error response should have an error"); - - assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID"); - assertFalse(jsonObject.has("values"), "Step Error response should not have values"); - - return (jsonObject); - } - } diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java new file mode 100644 index 00000000..b1644e49 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java @@ -0,0 +1,398 @@ +/* + * 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.javalin; + + +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; +import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import kong.unirest.HttpResponse; +import kong.unirest.Unirest; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for the javalin process handler methods. + *******************************************************************************/ +class QJavalinProcessHandlerTest extends QJavalinTestBase +{ + private static final int MORE_THAN_TIMEOUT = 500; + private static final int LESS_THAN_TIMEOUT = 50; + + + + /******************************************************************************* + ** test running a process + ** + *******************************************************************************/ + @Test + public void test_processGreetInit() + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage")); + } + + + + /******************************************************************************* + ** test running a process that requires rows, but we didn't tell it how to get them. + ** + *******************************************************************************/ + @Test + public void test_processRequiresRowsButNotSpecified() + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertTrue(jsonObject.has("error")); + assertTrue(jsonObject.getString("error").contains("missing input records")); + } + + + + /******************************************************************************* + ** test running a process and telling it rows to load via recordIds param + ** + *******************************************************************************/ + @Test + public void test_processRequiresRowsWithRecordIdParam() + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + + // todo - once we know how to get records from a process, add that call + } + + + + /******************************************************************************* + ** test running a process and telling it rows to load via filter JSON + ** + *******************************************************************************/ + @Test + public void test_processRequiresRowsWithFilterJSON() + { + QQueryFilter queryFilter = new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.IN) + .withValues(List.of(3, 4, 5))); + String filterJSON = JsonUtils.toJson(queryFilter); + + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(filterJSON, Charset.defaultCharset())).asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + + // todo - once we know how to get records from a process, add that call + } + + + + /******************************************************************************* + ** test running a process with field values on the query string + ** + *******************************************************************************/ + @Test + public void test_processGreetInitWithQueryValues() + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage")); + } + + + + /******************************************************************************* + ** test init'ing a process that goes async + ** + *******************************************************************************/ + @Test + public void test_processInitGoingAsync() throws InterruptedException + { + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP; + HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString(); + + JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String jobUUID = jsonObject.getString("jobUUID"); + assertNotNull(processUUID, "Process UUID should not be null."); + assertNotNull(jobUUID, "Job UUID should not be null"); + + ///////////////////////////////////////////// + // request job status before sleep is done // + ///////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepRunningResponse(response); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + //////////////////////////////////////////////////////// + // request job status again, get back results instead // + //////////////////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepCompleteResponse(response); + } + + + + /******************************************************************************* + ** test init'ing a process that does NOT goes async + ** + *******************************************************************************/ + @Test + public void test_processInitNotGoingAsync() + { + HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + assertProcessStepCompleteResponse(response); + } + + + + /******************************************************************************* + ** test running a step a process that goes async + ** + *******************************************************************************/ + @Test + public void test_processStepGoingAsync() throws InterruptedException + { + ///////////////////////////////////////////// + // first init the process, to get its UUID // + ///////////////////////////////////////////// + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; + HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + + JSONObject jsonObject = assertProcessStepCompleteResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String nextStep = jsonObject.getString("nextStep"); + assertNotNull(processUUID, "Process UUID should not be null."); + assertNotNull(nextStep, "There should be a next step"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) + .header("Content-Type", "application/json").asString(); + + jsonObject = assertProcessStepWentAsyncResponse(response); + String jobUUID = jsonObject.getString("jobUUID"); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + /////////////////////////////// + // third, request job status // + /////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + + jsonObject = assertProcessStepCompleteResponse(response); + String nextStep2 = jsonObject.getString("nextStep"); + assertNotNull(nextStep2, "There be one more next step"); + assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); + } + + + + /******************************************************************************* + ** test running a step a process that does NOT goes async + ** + *******************************************************************************/ + @Test + public void test_processStepNotGoingAsync() + { + ///////////////////////////////////////////// + // first init the process, to get its UUID // + ///////////////////////////////////////////// + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; + HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + + JSONObject jsonObject = assertProcessStepCompleteResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String nextStep = jsonObject.getString("nextStep"); + assertNotNull(nextStep, "There should be a next step"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // second, run the 'nextStep' (the backend step, that sleeps). run it with a short enough sleep so that it won't go async // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) + .header("Content-Type", "application/json").asString(); + + jsonObject = assertProcessStepCompleteResponse(response); + String nextStep2 = jsonObject.getString("nextStep"); + assertNotNull(nextStep2, "There be one more next step"); + assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); + } + + + + /******************************************************************************* + ** test init'ing a process that goes async and then throws + ** + *******************************************************************************/ + @Test + public void test_processInitGoingAsyncThenThrowing() throws InterruptedException + { + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW; + HttpResponse response = Unirest.get(processBasePath + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString(); + + JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String jobUUID = jsonObject.getString("jobUUID"); + + ///////////////////////////////////////////// + // request job status before sleep is done // + ///////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepRunningResponse(response); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + ///////////////////////////////////////////////////////////// + // request job status again, get back error status instead // + ///////////////////////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepErrorResponse(response); + } + + + + /******************************************************************************* + ** test init'ing a process that does NOT goes async, but throws. + ** + *******************************************************************************/ + @Test + public void test_processInitNotGoingAsyncButThrowing() + { + HttpResponse response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + assertProcessStepErrorResponse(response); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) has gone async, expect what the + ** response should look like + *******************************************************************************/ + private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID"); + assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID"); + + assertFalse(jsonObject.has("values"), "Async-started response should NOT have values"); + assertFalse(jsonObject.has("error"), "Async-started response should NOT have error"); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) is still running, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepRunningResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("jobStatus"), "Step Running response should have a jobStatus"); + + assertFalse(jsonObject.has("values"), "Step Running response should NOT have values"); + assertFalse(jsonObject.has("error"), "Step Running response should NOT have error"); + + assertEquals(AsyncJobState.RUNNING.name(), jsonObject.getJSONObject("jobStatus").getString("state")); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) completes, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepCompleteResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("values"), "Step Complete response should have values"); + + assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID"); + assertFalse(jsonObject.has("error"), "Step Complete response should not have an error"); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) has an error, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepErrorResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertTrue(jsonObject.has("error"), "Step Error response should have an error"); + + assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID"); + assertFalse(jsonObject.has("values"), "Step Error response should not have values"); + + return (jsonObject); + } +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java new file mode 100644 index 00000000..168174c2 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java @@ -0,0 +1,78 @@ +/* + * 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.javalin; + + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + + +/******************************************************************************* + ** base class for javalin implementation tests. + *******************************************************************************/ +public class QJavalinTestBase +{ + private static final int PORT = 6262; + protected static final String BASE_URL = "http://localhost:" + PORT; + + private static QJavalinImplementation qJavalinImplementation; + + + + /******************************************************************************* + ** Before the class (all) runs, start a javalin server. + ** + *******************************************************************************/ + @BeforeAll + public static void beforeAll() + { + qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); + QJavalinProcessHandler.setAsyncStepTimeoutMillis(250); + qJavalinImplementation.startJavalinServer(PORT); + } + + + + /******************************************************************************* + ** Before the class (all) runs, start a javalin server. + ** + *******************************************************************************/ + @AfterAll + public static void afterAll() + { + qJavalinImplementation.stopJavalinServer(); + } + + + + /******************************************************************************* + ** Fully rebuild the test-database before each test runs, for completely known state. + ** + *******************************************************************************/ + @BeforeEach + public void beforeEach() throws Exception + { + TestUtils.primeTestDatabase(); + } + +} From 492ee321a1106a936ce13a5903e07774ffe741d8 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 11 Jul 2022 09:10:57 -0500 Subject: [PATCH 09/13] QQQ-21 adding processRecords --- pom.xml | 2 +- .../javalin/QJavalinImplementation.java | 8 +-- .../javalin/QJavalinProcessCallback.java | 66 ------------------- .../javalin/QJavalinProcessHandler.java | 42 +++++++++++- .../javalin/QJavalinProcessHandlerTest.java | 33 +++++++++- .../qqq/backend/javalin/QJavalinTestBase.java | 2 +- 6 files changed, 77 insertions(+), 76 deletions(-) delete mode 100644 src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java diff --git a/pom.xml b/pom.xml index 8f5d6e69..06def7f3 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0-20220708.203555-6 + 0.1.0-20220711.141150-7 com.kingsrook.qqq 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 ec05e05c..56830ab3 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -147,7 +147,7 @@ public class QJavalinImplementation void startJavalinServer(int port) { // todo port from arg - // todo base path from arg? + // todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??) service = Javalin.create().start(port); service.routes(getRoutes()); } @@ -198,10 +198,8 @@ public class QJavalinImplementation { get("/", QJavalinImplementation::dataQuery); post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. - path("/count", () -> - { - get("", QJavalinImplementation::dataCount); - }); + 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", () -> { diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java deleted file mode 100644 index 328a66b2..00000000 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.javalin; - - -import java.io.Serializable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback; -import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class QJavalinProcessCallback implements QProcessCallback -{ - private static final Logger LOG = LogManager.getLogger(QJavalinProcessCallback.class); - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public QQueryFilter getQueryFilter() - { - LOG.warn("Getting a query filter in javalin is NOT yet implemented"); - return (new QQueryFilter()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public Map getFieldValues(List fields) - { - LOG.warn("Getting field values in javalin is NOT yet implemented"); - return (new HashMap<>()); - } -} 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 b2e007ee..f14f7302 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -47,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; @@ -68,7 +70,7 @@ import static io.javalin.apibuilder.ApiBuilder.post; /******************************************************************************* - ** + ** methods for handling qqq processes in javalin. *******************************************************************************/ public class QJavalinProcessHandler { @@ -96,6 +98,7 @@ public class QJavalinProcessHandler { post("/step/:step", QJavalinProcessHandler::processStep); get("/status/:jobUUID", QJavalinProcessHandler::processStatus); + get("/records", QJavalinProcessHandler::processRecords); }); }); }); @@ -383,6 +386,43 @@ public class QJavalinProcessHandler + /******************************************************************************* + ** + *******************************************************************************/ + private static void processRecords(Context context) + { + try + { + String processUUID = context.pathParam("processUUID"); + Integer skip = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "skip"), 0); + Integer limit = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "limit"), 20); + + Optional optionalProcessState = RunProcessAction.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS)); + if(optionalProcessState.isEmpty()) + { + throw (new Exception("Could not find process results.")); + } + ProcessState processState = optionalProcessState.get(); + + List records = processState.getRecords(); + if(CollectionUtils.nullSafeIsEmpty(records)) + { + throw (new Exception("No records were found for the process.")); + } + + Map resultForCaller = new HashMap<>(); + List recordPage = CollectionUtils.safelyGetPage(records, skip, limit); + resultForCaller.put("records", recordPage); + context.result(JsonUtils.toJson(resultForCaller)); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java index b1644e49..548b4f39 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java @@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -46,7 +47,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; *******************************************************************************/ class QJavalinProcessHandlerTest extends QJavalinTestBase { - private static final int MORE_THAN_TIMEOUT = 500; + private static final int MORE_THAN_TIMEOUT = 1000; private static final int LESS_THAN_TIMEOUT = 50; @@ -79,7 +80,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); assertTrue(jsonObject.has("error")); - assertTrue(jsonObject.getString("error").contains("missing input records")); + assertTrue(jsonObject.getString("error").contains("Missing input records")); } @@ -395,4 +396,32 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase return (jsonObject); } + + + + /******************************************************************************* + ** test getting records back from a process + ** + *******************************************************************************/ + @Test + public void test_processRecords() + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + String processUUID = jsonObject.getString("processUUID"); + + response = Unirest.get(BASE_URL + "/processes/greet/" + processUUID + "/records").asString(); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(2, records.length()); + JSONObject record0 = records.getJSONObject(0); + JSONObject values = record0.getJSONObject("values"); + assertTrue(values.has("id")); + assertTrue(values.has("firstName")); + } + } \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java index 168174c2..54dc47a5 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java @@ -47,7 +47,7 @@ public class QJavalinTestBase public static void beforeAll() { qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); - QJavalinProcessHandler.setAsyncStepTimeoutMillis(250); + QJavalinProcessHandler.setAsyncStepTimeoutMillis(500); qJavalinImplementation.startJavalinServer(PORT); } From 2cb7eef6795af3cb51389057c9d2d4362dabf03b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 12 Jul 2022 09:18:01 -0500 Subject: [PATCH 10/13] QQQ-21 feedback from code review --- pom.xml | 2 +- .../javalin/QJavalinImplementation.java | 7 +- .../javalin/QJavalinProcessHandler.java | 67 +++++++++-------- .../javalin/QJavalinProcessHandlerTest.java | 74 ++++++++++++++++--- .../qqq/backend/javalin/QJavalinTestBase.java | 2 +- 5 files changed, 104 insertions(+), 48 deletions(-) diff --git a/pom.xml b/pom.xml index 06def7f3..40b33f13 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0-20220711.141150-7 + 0.1.0-20220712.140206-8 com.kingsrook.qqq 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 56830ab3..437f982f 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -98,13 +98,14 @@ public class QJavalinImplementation private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; - protected static QInstance qInstance; + static QInstance qInstance; private static int DEFAULT_PORT = 8001; private static Javalin service; + /******************************************************************************* ** *******************************************************************************/ @@ -153,6 +154,7 @@ public class QJavalinImplementation } + /******************************************************************************* ** *******************************************************************************/ @@ -363,6 +365,9 @@ public class QJavalinImplementation setupSession(context, queryRequest); queryRequest.setTableName(tableName); + // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) + // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) + /////////////////////////////////////////////////////// // setup a filter for the primaryKey = the path-pram // /////////////////////////////////////////////////////// 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 f14f7302..aaac1a36 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -53,8 +53,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.state.StateType; -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; @@ -145,29 +143,27 @@ public class QJavalinProcessHandler runProcessRequest.setStartAfterStep(startAfterStep); populateRunProcessRequestWithValuesFromContext(context, runProcessRequest); - try + //////////////////////////////////////// + // run the process as an async action // + //////////////////////////////////////// + Integer timeout = getTimeoutMillis(context); + RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> { - //////////////////////////////////////// - // run the process as an async action // - //////////////////////////////////////// - Integer timeout = getTimeoutMillis(context); - RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> - { - runProcessRequest.setAsyncJobCallback(callback); - return (new RunProcessAction().execute(runProcessRequest)); - }); + runProcessRequest.setAsyncJobCallback(callback); + return (new RunProcessAction().execute(runProcessRequest)); + }); - LOG.info("Process result error? " + runProcessResult.getException()); - for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) - { - LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); - } - serializeRunProcessResultForCaller(resultForCaller, runProcessResult); - } - catch(JobGoingAsyncException jgae) + LOG.info("Process result error? " + runProcessResult.getException()); + for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) { - resultForCaller.put("jobUUID", jgae.getJobUUID()); + LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); } + + serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + } + catch(JobGoingAsyncException jgae) + { + resultForCaller.put("jobUUID", jgae.getJobUUID()); } catch(Exception e) { @@ -199,7 +195,7 @@ public class QJavalinProcessHandler serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get()); } resultForCaller.put("values", runProcessResult.getValues()); - runProcessResult.getProcessState().getNextStepName().ifPresent(lastStep -> resultForCaller.put("nextStep", lastStep)); + runProcessResult.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep)); } @@ -297,13 +293,13 @@ public class QJavalinProcessHandler { case "recordIds": @SuppressWarnings("ConstantConditions") - Serializable[] idStrings = context.queryParam(recordsParam).split(","); + Serializable[] idStrings = paramValue.split(","); return (new QQueryFilter().withCriteria(new QFilterCriteria() .withFieldName(primaryKeyField) .withOperator(QCriteriaOperator.IN) .withValues(Arrays.stream(idStrings).toList()))); case "filterJSON": - return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class)); + return (JsonUtils.toObject(paramValue, QQueryFilter.class)); case "filterId": // return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class)); throw (new NotImplementedException("Saved filters are not yet implemented.")); @@ -335,19 +331,20 @@ public class QJavalinProcessHandler *******************************************************************************/ public static void processStatus(Context context) { + Map resultForCaller = new HashMap<>(); + String processUUID = context.pathParam("processUUID"); String jobUUID = context.pathParam("jobUUID"); - LOG.info("Request for status of job " + jobUUID); + LOG.info("Request for status of process " + processUUID + ", job " + jobUUID); Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); if(optionalJobStatus.isEmpty()) { - QJavalinImplementation.handleException(context, new RuntimeException("Could not find status of process step job")); + serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job")); } else { - Map resultForCaller = new HashMap<>(); - AsyncJobStatus jobStatus = optionalJobStatus.get(); + AsyncJobStatus jobStatus = optionalJobStatus.get(); resultForCaller.put("jobStatus", jobStatus); LOG.info("Job status is " + jobStatus.getState() + " for " + jobUUID); @@ -358,7 +355,7 @@ public class QJavalinProcessHandler // if the job is complete, get the process result from state provider, and return it // // this output should look like it did if the job finished synchronously!! // /////////////////////////////////////////////////////////////////////////////////////// - Optional processState = RunProcessAction.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS)); + Optional processState = RunProcessAction.getState(processUUID); if(processState.isPresent()) { RunProcessResult runProcessResult = new RunProcessResult(processState.get()); @@ -366,7 +363,7 @@ public class QJavalinProcessHandler } else { - QJavalinImplementation.handleException(context, new RuntimeException("Could not find process results")); + serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID)); } } else if(jobStatus.getState().equals(AsyncJobState.ERROR)) @@ -379,9 +376,9 @@ public class QJavalinProcessHandler serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException()); } } - - context.result(JsonUtils.toJson(resultForCaller)); } + + context.result(JsonUtils.toJson(resultForCaller)); } @@ -397,7 +394,9 @@ public class QJavalinProcessHandler Integer skip = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "skip"), 0); Integer limit = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "limit"), 20); - Optional optionalProcessState = RunProcessAction.getStateProvider().get(ProcessState.class, new UUIDAndTypeStateKey(UUID.fromString(processUUID), StateType.PROCESS_STATUS)); + // todo - potential optimization - if a future state provider could take advantage of it, + // we might pass the skip & limit in to a method that fetch just those 'n' rows from state, rather than the whole thing? + Optional optionalProcessState = RunProcessAction.getState(processUUID); if(optionalProcessState.isEmpty()) { throw (new Exception("Could not find process results.")); @@ -411,7 +410,7 @@ public class QJavalinProcessHandler } Map resultForCaller = new HashMap<>(); - List recordPage = CollectionUtils.safelyGetPage(records, skip, limit); + List recordPage = CollectionUtils.safelyGetPage(records, skip, limit); resultForCaller.put("records", recordPage); context.result(JsonUtils.toJson(resultForCaller)); } diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java index 548b4f39..91cc0479 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java @@ -47,7 +47,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; *******************************************************************************/ class QJavalinProcessHandlerTest extends QJavalinTestBase { - private static final int MORE_THAN_TIMEOUT = 1000; + private static final int MORE_THAN_TIMEOUT = 500; private static final int LESS_THAN_TIMEOUT = 50; @@ -96,8 +96,9 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); + String processUUID = jsonObject.getString("processUUID"); - // todo - once we know how to get records from a process, add that call + getProcessRecords(processUUID, 2); } @@ -120,8 +121,45 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); + String processUUID = jsonObject.getString("processUUID"); - // todo - once we know how to get records from a process, add that call + getProcessRecords(processUUID, 3); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private JSONObject getProcessRecords(String processUUID, int expectedNoOfRecords) + { + return (getProcessRecords(processUUID, expectedNoOfRecords, 0, 20)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private JSONObject getProcessRecords(String processUUID, int expectedNoOfRecords, int skip, int limit) + { + HttpResponse response; + JSONObject jsonObject; + response = Unirest.get(BASE_URL + "/processes/greet/" + processUUID + "/records?skip=" + skip + "&limit=" + limit).asString(); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + + if(expectedNoOfRecords == 0) + { + assertFalse(jsonObject.has("records")); + } + else + { + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(expectedNoOfRecords, records.length()); + } + return (jsonObject); } @@ -133,7 +171,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase @Test public void test_processGreetInitWithQueryValues() { - HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString(); + HttpResponse response = Unirest.post(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); @@ -321,7 +359,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* - ** every time a process step (sync or async) has gone async, expect what the + ** every time a process step (or init) has gone async, expect what the ** response should look like *******************************************************************************/ private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response) @@ -412,16 +450,30 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase assertNotNull(jsonObject); String processUUID = jsonObject.getString("processUUID"); - response = Unirest.get(BASE_URL + "/processes/greet/" + processUUID + "/records").asString(); - jsonObject = JsonUtils.toJSONObject(response.getBody()); - assertNotNull(jsonObject); - assertTrue(jsonObject.has("records")); - JSONArray records = jsonObject.getJSONArray("records"); - assertEquals(2, records.length()); + jsonObject = getProcessRecords(processUUID, 2); + JSONArray records = jsonObject.getJSONArray("records"); JSONObject record0 = records.getJSONObject(0); JSONObject values = record0.getJSONObject("values"); assertTrue(values.has("id")); assertTrue(values.has("firstName")); } + + + /******************************************************************************* + ** test getting records back from a process with skip & Limit + ** + *******************************************************************************/ + @Test + public void test_processRecordsSkipAndLimit() + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=1,2,3,4,5").asString(); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + String processUUID = jsonObject.getString("processUUID"); + + getProcessRecords(processUUID, 5); + getProcessRecords(processUUID, 1, 4, 5); + getProcessRecords(processUUID, 0, 5, 5); + } + } \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java index 54dc47a5..168174c2 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java @@ -47,7 +47,7 @@ public class QJavalinTestBase public static void beforeAll() { qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); - QJavalinProcessHandler.setAsyncStepTimeoutMillis(500); + QJavalinProcessHandler.setAsyncStepTimeoutMillis(250); qJavalinImplementation.startJavalinServer(PORT); } From f0ae369a93d95dc60ed77016282d0c1b5525c925 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 13 Jul 2022 10:42:05 -0500 Subject: [PATCH 11/13] QQQ-21 Adding isHidden to tables & processes --- pom.xml | 2 +- .../javalin/QJavalinImplementationTest.java | 17 ++++++++++------- .../qqq/backend/javalin/TestUtils.java | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 40b33f13..c7298e89 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0-20220712.140206-8 + 0.1.0-20220713.153920-10 com.kingsrook.qqq 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 6d002780..459ff2bb 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -35,6 +35,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -67,11 +68,15 @@ class QJavalinImplementationTest extends QJavalinTestBase assertTrue(jsonObject.has("tables")); JSONObject tables = jsonObject.getJSONObject("tables"); assertEquals(1, tables.length()); - JSONObject table0 = tables.getJSONObject("person"); - assertTrue(table0.has("name")); - assertEquals("person", table0.getString("name")); - assertTrue(table0.has("label")); - assertEquals("Person", table0.getString("label")); + JSONObject personTable = tables.getJSONObject("person"); + assertTrue(personTable.has("name")); + assertEquals("person", personTable.getString("name")); + assertTrue(personTable.has("label")); + assertEquals("Person", personTable.getString("label")); + assertFalse(personTable.getBoolean("isHidden")); + + JSONObject processes = jsonObject.getJSONObject("processes"); + assertTrue(processes.getJSONObject("simpleSleep").getBoolean("isHidden")); } @@ -89,7 +94,6 @@ class QJavalinImplementationTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); JSONObject table = jsonObject.getJSONObject("table"); - assertEquals(4, table.keySet().size(), "Number of mid-level keys"); assertEquals("person", table.getString("name")); assertEquals("Person", table.getString("label")); assertEquals("id", table.getString("primaryKeyField")); @@ -132,7 +136,6 @@ class QJavalinImplementationTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); JSONObject process = jsonObject.getJSONObject("process"); - assertEquals(4, process.keySet().size(), "Number of mid-level keys"); assertEquals("greetInteractive", process.getString("name")); assertEquals("Greet Interactive", process.getString("label")); assertEquals("person", process.getString("tableName")); 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 5fccd572..133d1c07 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -260,6 +260,7 @@ public class TestUtils { return new QProcessMetaData() .withName(PROCESS_NAME_SIMPLE_SLEEP) + .withIsHidden(true) .addStep(SleeperStep.getMetaData()); } From 13f7291c2a94994a5a3d2fe866c7a15f36dcd99e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 10:21:23 -0500 Subject: [PATCH 12/13] Update qqq-backend-core and rdbms to 0.1.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c7298e89..65b3ab9a 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0-20220713.153920-10 + 0.1.0 com.kingsrook.qqq qqq-backend-module-rdbms - 0.1.0-20220708.202041-3 + 0.1.0 test From 930423e940ef79079fc346aad1ea5bdfcb0d9fd4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 10:21:51 -0500 Subject: [PATCH 13/13] Update versions for release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 65b3ab9a..3a7158aa 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-javalin - 0.1.0-SNAPSHOT + 0.1.0 scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git