From 2fe4ef5b2448d143007a2099a7383fada2754ecb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff <93011140+darin-kelkhoff@users.noreply.github.com> Date: Mon, 8 Nov 2021 21:17:15 -0600 Subject: [PATCH 01/60] Initial commit --- .gitignore | 23 +++++++++++++++++++++++ README.md | 1 + 2 files changed, 24 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a1c2a238 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/README.md b/README.md new file mode 100644 index 00000000..f73d2f81 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# qqq-middleware-javalin \ No newline at end of file From aa56e52024290d15f76facaadac22bfe2543b0f4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 8 Nov 2021 21:19:06 -0600 Subject: [PATCH 02/60] Initial code checkin --- .gitignore | 7 + checkstyle.xml | 242 ++++++++++++++++ pom.xml | 141 +++++++++ .../javalin/QJavalinImplementation.java | 272 ++++++++++++++++++ .../javalin/QJavalinImplementationTest.java | 133 +++++++++ .../qqq/backend/javalin/TestUtils.java | 93 ++++++ src/test/resources/prime-test-database.sql | 18 ++ 7 files changed, 906 insertions(+) create mode 100644 checkstyle.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java create mode 100644 src/test/resources/prime-test-database.sql diff --git a/.gitignore b/.gitignore index a1c2a238..39736a21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +target/ +*.iml + + +############################################# +## Original contents from github template: ## +############################################# # Compiled class file *.class diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000..fadf2cda --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..e73e5525 --- /dev/null +++ b/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + + com.kingsrook.qqq + qqq-middleware-javalin + 0.0-SNAPSHOT + + + + + + + UTF-8 + UTF-8 + 17 + 17 + true + true + + + + + + com.kingsrook.qqq + qqq-backend-core + 0.0-SNAPSHOT + + + com.kingsrook.qqq + qqq-backend-module-rdbms + 0.0-SNAPSHOT + test + + + + + io.javalin + javalin + 3.13.10 + + + com.konghq + unirest-java + 3.4.00 + test + + + com.h2database + h2 + 1.4.197 + test + + + org.slf4j + slf4j-simple + 1.7.30 + test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + org.apache.logging.log4j + log4j-api + 2.14.1 + + + org.apache.logging.log4j + log4j-core + 2.14.1 + + + org.junit.jupiter + junit-jupiter-engine + 5.8.1 + test + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + -Xlint:unchecked + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + com.puppycrawl.tools + checkstyle + 9.0 + + + + + validate + validate + + checkstyle.xml + + UTF-8 + true + false + true + warning + **/target/generated-sources/*.* + + + + check + + + + + + + + diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java new file mode 100644 index 00000000..13efb344 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -0,0 +1,272 @@ +package com.kingsrook.qqq.backend.javalin; + + +import com.kingsrook.qqq.backend.core.actions.MetaDataAction; +import com.kingsrook.qqq.backend.core.actions.QueryAction; +import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.MetaDataRequest; +import com.kingsrook.qqq.backend.core.model.actions.MetaDataResult; +import com.kingsrook.qqq.backend.core.model.actions.QueryRequest; +import com.kingsrook.qqq.backend.core.model.actions.QueryResult; +import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataRequest; +import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataResult; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +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.Javalin; +import io.javalin.apibuilder.EndpointGroup; +import io.javalin.http.Context; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.http.HttpStatus; +import static io.javalin.apibuilder.ApiBuilder.delete; +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.patch; +import static io.javalin.apibuilder.ApiBuilder.path; +import static io.javalin.apibuilder.ApiBuilder.post; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QJavalinImplementation +{ + private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); + + private static QInstance qInstance; + + private static int PORT = 8001; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void main(String[] args) + { + QInstance qInstance = new QInstance(); + // todo - parse args to look up metaData and prime instance + // qInstance.addBackend(QMetaDataProvider.getQBackend()); + + new QJavalinImplementation(qInstance).startJavalinServer(PORT); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QJavalinImplementation(QInstance qInstance) + { + QJavalinImplementation.qInstance = qInstance; + } + + + + /******************************************************************************* + ** Setter for qInstance + ** + *******************************************************************************/ + public static void setQInstance(QInstance qInstance) + { + QJavalinImplementation.qInstance = qInstance; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + void startJavalinServer(int port) + { + // todo port from arg + // todo base path from arg? + Javalin service = Javalin.create().start(port); + service.routes(getRoutes()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public EndpointGroup getRoutes() + { + return (() -> + { + path("/metaData", () -> + { + get("/", QJavalinImplementation::metaData); + path("/:table", () -> + { + get("", QJavalinImplementation::tableMetaData); + }); + }); + path("/data", () -> + { + path("/:table", () -> + { + get("/", QJavalinImplementation::dataQuery); + post("/", QJavalinImplementation::dataInsert); + path("/:id", () -> + { + get("", QJavalinImplementation::dataGet); + patch("", QJavalinImplementation::dataUpdate); + delete("", QJavalinImplementation::dataDelete); + }); + }); + }); + }); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataDelete(Context context) + { + context.result("{\"deleteResult\":{}}"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataUpdate(Context context) + { + context.result("{\"updateResult\":{}}"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataInsert(Context context) + { + context.result("{\"insertResult\":{}}"); + } + + + + /******************************************************************************* + ** + ********************************************************************************/ + private static void dataGet(Context context) + { + context.result("{\"getResult\":{}}"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static void dataQuery(Context context) + { + try + { + QueryRequest queryRequest = new QueryRequest(qInstance); + queryRequest.setTableName(context.pathParam("table")); + queryRequest.setSkip(integerQueryParam(context, "skip")); + queryRequest.setLimit(integerQueryParam(context, "limit")); + + QueryAction queryAction = new QueryAction(); + QueryResult queryResult = queryAction.execute(queryRequest); + + context.result(JsonUtils.toJson(queryResult)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void metaData(Context context) + { + try + { + MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance); + MetaDataAction metaDataAction = new MetaDataAction(); + MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest); + + context.result(JsonUtils.toJson(metaDataResult)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void tableMetaData(Context context) + { + try + { + TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance); + tableMetaDataRequest.setTableName(context.pathParam("table")); + TableMetaDataAction tableMetaDataAction = new TableMetaDataAction(); + TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest); + + context.result(JsonUtils.toJson(tableMetaDataResult)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void handleException(Context context, Exception e) + { + 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() + "\"}"); + } + else + { + LOG.warn("Exception in javalin request", e); + e.printStackTrace(); + context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) + .result("{\"error\":\"" + e.getClass().getSimpleName() + "\"}"); + } + } + + + + /******************************************************************************* + ** 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. + *******************************************************************************/ + private static Integer integerQueryParam(Context context, String name) throws NumberFormatException + { + String value = context.queryParam(name); + if(StringUtils.hasContent(value)) + { + return (Integer.parseInt(value)); + } + + return (null); + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java new file mode 100644 index 00000000..426a01f4 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -0,0 +1,133 @@ +package com.kingsrook.qqq.backend.javalin; + + +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.assertTrue; + + +/******************************************************************************* + ** based on https://javalin.io/tutorials/testing + ** + *******************************************************************************/ +class QJavalinImplementationTest +{ + private static final int PORT = 6262; + private static final String BASE_URL = "http://localhost:" + PORT; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeAll + public static void beforeAll() throws Exception + { + QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); + qJavalinImplementation.startJavalinServer(PORT); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + public void beforeEach() throws Exception + { + TestUtils.primeTestDatabase(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_metaData() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + 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")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_tableMetaData() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData/person").asString(); + + 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")); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_tableMetaData_notFound() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").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"); + String error = jsonObject.getString("error"); + assertTrue(error.contains("not found")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_dataQuery() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(5, records.length()); + JSONObject record0 = records.getJSONObject(0); + assertTrue(record0.has("values")); + assertEquals("person", record0.getString("tableName")); + assertTrue(record0.has("primaryKey")); + JSONObject values0 = record0.getJSONObject("values"); + assertTrue(values0.has("firstName")); + } +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java new file mode 100644 index 00000000..5074661b --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -0,0 +1,93 @@ +package com.kingsrook.qqq.backend.javalin; + + +import java.io.InputStream; +import java.sql.Connection; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +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.QTableMetaData; +import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import org.apache.commons.io.IOUtils; +import static junit.framework.Assert.assertNotNull; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestUtils +{ + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static void primeTestDatabase() throws Exception + { + ConnectionManager connectionManager = new ConnectionManager(); + Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(TestUtils.defineBackend())); + InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); + assertNotNull(primeTestDatabaseSqlStream); + List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); + String joinedSQL = String.join("\n", lines); + for(String sql : joinedSQL.split(";")) + { + QueryManager.executeUpdate(connection, sql); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QInstance defineInstance() + { + QInstance qInstance = new QInstance(); + qInstance.addBackend(defineBackend()); + qInstance.addTable(defineTablePerson()); + return (qInstance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QBackendMetaData defineBackend() + { + return new QBackendMetaData() + .withName("default") + .withType("rdbms") + .withValue("vendor", "h2") + .withValue("hostName", "mem") + .withValue("databaseName", "test_database") + .withValue("username", "sa") + .withValue("password", ""); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QTableMetaData defineTablePerson() + { + return new QTableMetaData() + .withName("person") + .withLabel("Person") + .withBackendName(defineBackend().getName()) + .withPrimaryKeyField("id") + .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("birthDate", QFieldType.DATE).withBackendName("birth_date")) + .withField(new QFieldMetaData("email", QFieldType.STRING)); + } + +} diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql new file mode 100644 index 00000000..e5adab34 --- /dev/null +++ b/src/test/resources/prime-test-database.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS person; +CREATE TABLE person +( + id SERIAL, + create_date TIMESTAMP DEFAULT now(), + modify_date TIMESTAMP DEFAULT now(), + + first_name VARCHAR(80) NOT NULL, + last_name VARCHAR(80) NOT NULL, + birth_date DATE, + email VARCHAR(250) NOT NULL +); + +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (4, 'Tyler', 'Samples', '1990-01-01', 'tsamples@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com'); From c0c1454f954e81db52fafdf7d22203e587207072 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff <93011140+darin-kelkhoff@users.noreply.github.com> Date: Mon, 8 Nov 2021 22:10:44 -0600 Subject: [PATCH 03/60] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f73d2f81..9ec49901 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# qqq-middleware-javalin \ No newline at end of file +# qqq-middleware-javalin +This is a qqq middleware module, providing [javalin](https://javalin.io) access to the qqq-backend. From 6fbf3563dd4cee5197da23923f081dd8b2eb4b3e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 28 Nov 2021 21:41:43 -0600 Subject: [PATCH 04/60] Implemented insert; added filter to query --- .../javalin/QJavalinImplementation.java | 77 ++++++++++++++++++- .../javalin/QJavalinImplementationTest.java | 56 ++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 13efb344..96f58901 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -1,16 +1,25 @@ package com.kingsrook.qqq.backend.javalin; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.QueryAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.InsertRequest; +import com.kingsrook.qqq.backend.core.model.actions.InsertResult; import com.kingsrook.qqq.backend.core.model.actions.MetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.MetaDataResult; +import com.kingsrook.qqq.backend.core.model.actions.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.QueryRequest; import com.kingsrook.qqq.backend.core.model.actions.QueryResult; import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataResult; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -148,7 +157,36 @@ public class QJavalinImplementation *******************************************************************************/ private static void dataInsert(Context context) { - context.result("{\"insertResult\":{}}"); + try + { + String table = context.pathParam("table"); + List recordList = new ArrayList<>(); + QRecord record = new QRecord(); + record.setTableName(table); + recordList.add(record); + + Map map = context.bodyAsClass(Map.class); + for(Map.Entry entry : map.entrySet()) + { + if(StringUtils.hasContent(String.valueOf(entry.getValue()))) + { + record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue()); + } + } + + InsertRequest insertRequest = new InsertRequest(qInstance); + insertRequest.setTableName(table); + insertRequest.setRecords(recordList); + + InsertAction insertAction = new InsertAction(); + InsertResult insertResult = insertAction.execute(insertRequest); + + context.result(JsonUtils.toJson(insertResult)); + } + catch(Exception e) + { + handleException(context, e); + } } @@ -164,7 +202,19 @@ 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"]}
+    *     ],
+    *     "orderBys":[
+    *       {"fieldName":"age","isAscending":true}
+    *     ]}
+    * 
*******************************************************************************/ static void dataQuery(Context context) { @@ -175,6 +225,12 @@ public class QJavalinImplementation queryRequest.setSkip(integerQueryParam(context, "skip")); queryRequest.setLimit(integerQueryParam(context, "limit")); + String filter = stringQueryParam(context, "filter"); + if(filter != null) + { + queryRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + } + QueryAction queryAction = new QueryAction(); QueryResult queryResult = queryAction.execute(queryRequest); @@ -269,4 +325,21 @@ public class QJavalinImplementation return (null); } + + + + /******************************************************************************* + ** 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 + { + String value = context.queryParam(name); + if(StringUtils.hasContent(value)) + { + return (value); + } + + return (null); + } } 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 426a01f4..83bc87fe 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -1,6 +1,11 @@ package com.kingsrook.qqq.backend.javalin; +import java.io.Serializable; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; @@ -130,4 +135,55 @@ class QJavalinImplementationTest JSONObject values0 = record0.getJSONObject("values"); assertTrue(values0.has("firstName")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @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(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(1, records.length()); + JSONObject record0 = records.getJSONObject(0); + JSONObject values0 = record0.getJSONObject("values"); + assertEquals("Tim", values0.getString("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_dataInsert() + { + Map body = new HashMap<>(); + body.put("firstName", "Bobby"); + body.put("lastName", "Hull"); + + HttpResponse response = Unirest.post(BASE_URL + "/data/person") + .header("Content-Type", "application/json") + .body(body) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(1, records.length()); + JSONObject record0 = records.getJSONObject(0); + assertTrue(record0.has("values")); + assertEquals("person", record0.getString("tableName")); + assertTrue(record0.has("primaryKey")); + JSONObject values0 = record0.getJSONObject("values"); + assertTrue(values0.has("firstName")); + assertEquals("Bobby", values0.getString("firstName")); + } } \ No newline at end of file From c2fbca18a76bd6c3d07dd8fc12dda35e9a9778dd Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 30 Nov 2021 18:09:53 -0600 Subject: [PATCH 05/60] Added delete --- .../javalin/QJavalinImplementation.java | 23 +++++++++++- .../javalin/QJavalinImplementationTest.java | 35 ++++++++++++++++++- .../qqq/backend/javalin/TestUtils.java | 12 +++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 96f58901..c450c2b6 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -5,11 +5,14 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; +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.QueryAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.DeleteRequest; +import com.kingsrook.qqq.backend.core.model.actions.DeleteResult; import com.kingsrook.qqq.backend.core.model.actions.InsertRequest; import com.kingsrook.qqq.backend.core.model.actions.InsertResult; import com.kingsrook.qqq.backend.core.model.actions.MetaDataRequest; @@ -137,7 +140,25 @@ public class QJavalinImplementation *******************************************************************************/ private static void dataDelete(Context context) { - context.result("{\"deleteResult\":{}}"); + try + { + String table = context.pathParam("table"); + List primaryKeys = new ArrayList<>(); + primaryKeys.add(context.pathParam("id")); + + DeleteRequest deleteRequest = new DeleteRequest(qInstance); + deleteRequest.setTableName(table); + deleteRequest.setPrimaryKeys(primaryKeys); + + DeleteAction deleteAction = new DeleteAction(); + DeleteResult deleteResult = deleteAction.execute(deleteRequest); + + context.result(JsonUtils.toJson(deleteResult)); + } + catch(Exception e) + { + handleException(context, e); + } } diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 83bc87fe..52d0031d 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -16,6 +16,8 @@ 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -98,6 +100,7 @@ class QJavalinImplementationTest } + /******************************************************************************* ** *******************************************************************************/ @@ -106,7 +109,7 @@ class QJavalinImplementationTest { HttpResponse response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").asString(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); // todo 404? JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); String error = jsonObject.getString("error"); @@ -137,6 +140,7 @@ class QJavalinImplementationTest } + /******************************************************************************* ** *******************************************************************************/ @@ -186,4 +190,33 @@ class QJavalinImplementationTest assertTrue(values0.has("firstName")); assertEquals("Bobby", values0.getString("firstName")); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_dataDelete() throws Exception + { + HttpResponse response = Unirest.delete(BASE_URL + "/data/person/3") + .header("Content-Type", "application/json") + .asString(); + + assertEquals(200, response.getStatus()); + + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertEquals(1, jsonObject.getJSONArray("records").length()); + assertEquals(3, jsonObject.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); + TestUtils.runTestSql("SELECT id FROM person", (rs -> { + int rowsFound = 0; + while(rs.next()) + { + rowsFound++; + assertFalse(rs.getInt(1) == 3); + } + assertEquals(4, rowsFound); + })); + } } \ No newline at end of file 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 5074661b..6d8df0a8 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -41,6 +41,18 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception + { + ConnectionManager connectionManager = new ConnectionManager(); + Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(defineBackend())); + QueryManager.executeStatement(connection, sql, resultSetProcessor); + } + + + /******************************************************************************* ** *******************************************************************************/ From 84b432997c2ddd280370c251c9bec45be24c21eb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 1 Dec 2021 23:03:05 -0600 Subject: [PATCH 06/60] Commentses --- .../backend/javalin/QJavalinImplementation.java | 6 ++++-- .../javalin/QJavalinImplementationTest.java | 16 ++++++++++++++-- .../kingsrook/qqq/backend/javalin/TestUtils.java | 7 +++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index c450c2b6..fa2732bf 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -41,6 +41,8 @@ import static io.javalin.apibuilder.ApiBuilder.post; /******************************************************************************* + ** QQQ Javalin implementation. Given a QInstance, defines all routes needed + ** to respond to http requests and route down into the qqq backend. ** *******************************************************************************/ public class QJavalinImplementation @@ -168,7 +170,7 @@ public class QJavalinImplementation *******************************************************************************/ private static void dataUpdate(Context context) { - context.result("{\"updateResult\":{}}"); + context.result("{\"todo\":\"not-done\",\"updateResult\":{}}"); } @@ -217,7 +219,7 @@ public class QJavalinImplementation ********************************************************************************/ private static void dataGet(Context context) { - context.result("{\"getResult\":{}}"); + context.result("{\"todo\":\"not-done\",\"getResult\":{}}"); } 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 52d0031d..49374c4f 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -22,7 +22,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* - ** based on https://javalin.io/tutorials/testing + ** Unit terst for the QJavalinImplementation + ** + ** based on https://javalin.io/tutorials/testing - starts a javalin instance + ** and actually makes http requests into it. ** *******************************************************************************/ class QJavalinImplementationTest @@ -33,6 +36,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** Before the class (all) runs, start a javalin server. ** *******************************************************************************/ @BeforeAll @@ -45,6 +49,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** Fully rebuild the test-database before each test runs, for completely known state. ** *******************************************************************************/ @BeforeEach @@ -56,6 +61,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test the top-level meta-data endpoint ** *******************************************************************************/ @Test @@ -78,6 +84,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test the table-level meta-data endpoint ** *******************************************************************************/ @Test @@ -102,6 +109,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test the table-level meta-data endpoint for a non-real name ** *******************************************************************************/ @Test @@ -119,6 +127,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test a table query ** *******************************************************************************/ @Test @@ -142,6 +151,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test a table query using an actual filter. ** *******************************************************************************/ @Test @@ -163,6 +173,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test an insert ** *******************************************************************************/ @Test @@ -194,6 +205,7 @@ class QJavalinImplementationTest /******************************************************************************* + ** test a delete ** *******************************************************************************/ @Test @@ -219,4 +231,4 @@ class QJavalinImplementationTest assertEquals(4, rowsFound); })); } -} \ No newline at end of file +} 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 6d8df0a8..d9d73402 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -17,11 +17,14 @@ import static junit.framework.Assert.assertNotNull; /******************************************************************************* + ** Utility methods for unit tests. ** *******************************************************************************/ public class TestUtils { + /******************************************************************************* + ** Prime a test database (e.g., h2, in-memory) ** *******************************************************************************/ @SuppressWarnings("unchecked") @@ -42,6 +45,7 @@ public class TestUtils /******************************************************************************* + ** Run an SQL Query in the test database ** *******************************************************************************/ public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception @@ -54,6 +58,7 @@ public class TestUtils /******************************************************************************* + ** Define the q-instance for testing (h2 rdbms and 'person' table) ** *******************************************************************************/ public static QInstance defineInstance() @@ -67,6 +72,7 @@ public class TestUtils /******************************************************************************* + ** Define the h2 rdbms backend ** *******************************************************************************/ public static QBackendMetaData defineBackend() @@ -84,6 +90,7 @@ public class TestUtils /******************************************************************************* + ** Define the person table ** *******************************************************************************/ public static QTableMetaData defineTablePerson() From fcd5bc9ee63bfbd8dd841be52c092b3cabba6a95 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 13 Dec 2021 22:08:34 -0600 Subject: [PATCH 07/60] Updating to log4j 2.15.0 for security patch --- pom.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index e73e5525..88d81fda 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,16 @@ org.slf4j slf4j-simple 1.7.30 - test + + + org.slf4j + slf4j-api + 1.7.30 + + + org.slf4j + slf4j-simple + 1.7.30 @@ -69,12 +78,12 @@ org.apache.logging.log4j log4j-api - 2.14.1 + 2.15.0 org.apache.logging.log4j log4j-core - 2.14.1 + 2.15.0 org.junit.jupiter From 7b10abc1060b6e1fd31ca98b3bcffff40f6c11cf Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 14 Dec 2021 18:42:03 -0600 Subject: [PATCH 08/60] Added copyright --- checkstyle.xml | 4 ++++ pom.xml | 4 ++++ .../kingsrook/qqq/backend/javalin/QJavalinImplementation.java | 4 ++++ .../qqq/backend/javalin/QJavalinImplementationTest.java | 4 ++++ .../java/com/kingsrook/qqq/backend/javalin/TestUtils.java | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/checkstyle.xml b/checkstyle.xml index fadf2cda..56a37ba1 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,4 +1,8 @@ + + diff --git a/pom.xml b/pom.xml index 88d81fda..8f6f7dc2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,8 @@ + + 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 fa2732bf..ba14a285 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -1,3 +1,7 @@ +/* + * Copyright © 2021-2021. Kingsrook LLC . All Rights Reserved. + */ + package com.kingsrook.qqq.backend.javalin; 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 49374c4f..59cd314e 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -1,3 +1,7 @@ +/* + * Copyright © 2021-2021. Kingsrook LLC . All Rights Reserved. + */ + package com.kingsrook.qqq.backend.javalin; 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 d9d73402..4fd8d45f 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -1,3 +1,7 @@ +/* + * Copyright © 2021-2021. Kingsrook LLC . All Rights Reserved. + */ + package com.kingsrook.qqq.backend.javalin; From e7c2cf9b941b472b105cd33cd4b3361d2c92a1cc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 15 Feb 2022 18:20:13 -0600 Subject: [PATCH 09/60] Checkpoint: support for authentication; initial process tests --- .../javalin/QJavalinImplementation.java | 56 ++++++++++++++----- .../javalin/QJavalinImplementationTest.java | 2 +- .../qqq/backend/javalin/TestUtils.java | 21 ++++++- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index ba14a285..ccaefc50 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -7,6 +7,7 @@ package com.kingsrook.qqq.backend.javalin; import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.DeleteAction; @@ -14,20 +15,25 @@ import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.QueryAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; -import com.kingsrook.qqq.backend.core.model.actions.DeleteRequest; -import com.kingsrook.qqq.backend.core.model.actions.DeleteResult; -import com.kingsrook.qqq.backend.core.model.actions.InsertRequest; -import com.kingsrook.qqq.backend.core.model.actions.InsertResult; -import com.kingsrook.qqq.backend.core.model.actions.MetaDataRequest; -import com.kingsrook.qqq.backend.core.model.actions.MetaDataResult; -import com.kingsrook.qqq.backend.core.model.actions.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.actions.QueryRequest; -import com.kingsrook.qqq.backend.core.model.actions.QueryResult; -import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataRequest; -import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataResult; +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; +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.table.TableMetaDataRequest; +import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult; +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; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.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.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -52,6 +58,7 @@ import static io.javalin.apibuilder.ApiBuilder.post; public class QJavalinImplementation { private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); + private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; private static QInstance qInstance; @@ -141,6 +148,25 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException + { + QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); + + // todo - does this need some per-provider logic actually? mmm... + Map authenticationContext = new HashMap<>(); + authenticationContext.put("sessionId", context.cookie("sessionId")); + QSession session = authenticationModule.createSession(authenticationContext); + request.setSession(session); + + context.cookie("sessionId", session.getIdReference(), SESSION_COOKIE_AGE); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -153,6 +179,7 @@ public class QJavalinImplementation primaryKeys.add(context.pathParam("id")); DeleteRequest deleteRequest = new DeleteRequest(qInstance); + setupSession(context, deleteRequest); deleteRequest.setTableName(table); deleteRequest.setPrimaryKeys(primaryKeys); @@ -202,6 +229,7 @@ public class QJavalinImplementation } InsertRequest insertRequest = new InsertRequest(qInstance); + setupSession(context, insertRequest); insertRequest.setTableName(table); insertRequest.setRecords(recordList); @@ -231,7 +259,6 @@ public class QJavalinImplementation /******************************************************************************* * * Filter parameter is a serialized QQueryFilter object, that is to say: - * *
     *   filter=
     *    {"criteria":[
@@ -248,6 +275,7 @@ public class QJavalinImplementation
       try
       {
          QueryRequest queryRequest = new QueryRequest(qInstance);
+         setupSession(context, queryRequest);
          queryRequest.setTableName(context.pathParam("table"));
          queryRequest.setSkip(integerQueryParam(context, "skip"));
          queryRequest.setLimit(integerQueryParam(context, "limit"));
@@ -279,6 +307,7 @@ public class QJavalinImplementation
       try
       {
          MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance);
+         setupSession(context, metaDataRequest);
          MetaDataAction metaDataAction = new MetaDataAction();
          MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest);
 
@@ -300,6 +329,7 @@ public class QJavalinImplementation
       try
       {
          TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance);
+         setupSession(context, tableMetaDataRequest);
          tableMetaDataRequest.setTableName(context.pathParam("table"));
          TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
          TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest);
@@ -331,7 +361,7 @@ public class QJavalinImplementation
          LOG.warn("Exception in javalin request", e);
          e.printStackTrace();
          context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
-            .result("{\"error\":\"" + e.getClass().getSimpleName() + "\"}");
+            .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}");
       }
    }
 
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 59cd314e..b2266896 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -26,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 
 /*******************************************************************************
- ** Unit terst for the QJavalinImplementation
+ ** Unit test for the QJavalinImplementation
  **
  ** based on https://javalin.io/tutorials/testing - starts a javalin instance
  ** and actually makes http requests into it.
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 4fd8d45f..094538c3 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -8,12 +8,13 @@ package com.kingsrook.qqq.backend.javalin;
 import java.io.InputStream;
 import java.sql.Connection;
 import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
 import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
 import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
 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.QTableMetaData;
-import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendMetaData;
 import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
 import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
 import org.apache.commons.io.IOUtils;
@@ -35,7 +36,7 @@ public class TestUtils
    public static void primeTestDatabase() throws Exception
    {
       ConnectionManager connectionManager = new ConnectionManager();
-      Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(TestUtils.defineBackend()));
+      Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(TestUtils.defineBackend()));
       InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
       assertNotNull(primeTestDatabaseSqlStream);
       List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream);
@@ -55,7 +56,7 @@ public class TestUtils
    public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
    {
       ConnectionManager connectionManager = new ConnectionManager();
-      Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(defineBackend()));
+      Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(defineBackend()));
       QueryManager.executeStatement(connection, sql, resultSetProcessor);
    }
 
@@ -68,6 +69,7 @@ public class TestUtils
    public static QInstance defineInstance()
    {
       QInstance qInstance = new QInstance();
+      qInstance.setAuthentication(defineAuthentication());
       qInstance.addBackend(defineBackend());
       qInstance.addTable(defineTablePerson());
       return (qInstance);
@@ -75,6 +77,19 @@ public class TestUtils
 
 
 
+   /*******************************************************************************
+    ** Define the authentication used in standard tests - using 'mock' type.
+    **
+    *******************************************************************************/
+   private static QAuthenticationMetaData defineAuthentication()
+   {
+      return new QAuthenticationMetaData()
+         .withName("mock")
+         .withType("mock");
+   }
+
+
+
    /*******************************************************************************
     ** Define the h2 rdbms backend
     **

From d6557daa42bcb028883287bb707bd0e788308c38 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Tue, 1 Mar 2022 18:29:09 -0600
Subject: [PATCH 10/60] Checkpoint:  Added Update (edit) action

---
 .../javalin/QJavalinImplementation.java       | 60 ++++++++++++++++---
 .../javalin/QJavalinImplementationTest.java   | 42 +++++++++++--
 2 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
index ccaefc50..e4f53aad 100644
--- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -5,6 +5,8 @@
 package com.kingsrook.qqq.backend.javalin;
 
 
+import java.io.File;
+import java.io.IOException;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -15,6 +17,8 @@ import com.kingsrook.qqq.backend.core.actions.InsertAction;
 import com.kingsrook.qqq.backend.core.actions.MetaDataAction;
 import com.kingsrook.qqq.backend.core.actions.QueryAction;
 import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.UpdateAction;
+import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
 import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
 import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
 import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest;
@@ -29,8 +33,11 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaData
 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;
+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.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;
@@ -40,6 +47,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
 import io.javalin.Javalin;
 import io.javalin.apibuilder.EndpointGroup;
 import io.javalin.http.Context;
+import org.apache.commons.io.FileUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.eclipse.jetty.http.HttpStatus;
@@ -48,6 +56,7 @@ import static io.javalin.apibuilder.ApiBuilder.get;
 import static io.javalin.apibuilder.ApiBuilder.patch;
 import static io.javalin.apibuilder.ApiBuilder.path;
 import static io.javalin.apibuilder.ApiBuilder.post;
+import static io.javalin.apibuilder.ApiBuilder.put;
 
 
 /*******************************************************************************
@@ -91,12 +100,13 @@ public class QJavalinImplementation
 
 
    /*******************************************************************************
-    ** Setter for qInstance
     **
     *******************************************************************************/
-   public static void setQInstance(QInstance qInstance)
+   public QJavalinImplementation(String qInstanceFilePath) throws IOException
    {
-      QJavalinImplementation.qInstance = qInstance;
+      LOG.info("Loading qInstance from file (assuming json): " + qInstanceFilePath);
+      String qInstanceJson = FileUtils.readFileToString(new File(qInstanceFilePath));
+      QJavalinImplementation.qInstance = new QInstanceAdapter().jsonToQInstanceIncludingBackends(qInstanceJson);
    }
 
 
@@ -134,11 +144,13 @@ public class QJavalinImplementation
             path("/:table", () ->
             {
                get("/", QJavalinImplementation::dataQuery);
-               post("/", QJavalinImplementation::dataInsert);
-               path("/:id", () ->
+               post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single.
+               // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records.
+               path("/:primaryKey", () ->
                {
                   get("", QJavalinImplementation::dataGet);
                   patch("", QJavalinImplementation::dataUpdate);
+                  put("", QJavalinImplementation::dataUpdate); // todo - want different semantics??
                   delete("", QJavalinImplementation::dataDelete);
                });
             });
@@ -176,7 +188,7 @@ public class QJavalinImplementation
       {
          String table = context.pathParam("table");
          List primaryKeys = new ArrayList<>();
-         primaryKeys.add(context.pathParam("id"));
+         primaryKeys.add(context.pathParam("primaryKey"));
 
          DeleteRequest deleteRequest = new DeleteRequest(qInstance);
          setupSession(context, deleteRequest);
@@ -201,7 +213,41 @@ public class QJavalinImplementation
     *******************************************************************************/
    private static void dataUpdate(Context context)
    {
-      context.result("{\"todo\":\"not-done\",\"updateResult\":{}}");
+      try
+      {
+         String table = context.pathParam("table");
+         List recordList = new ArrayList<>();
+         QRecord record = new QRecord();
+         record.setTableName(table);
+         recordList.add(record);
+
+         Map map = context.bodyAsClass(Map.class);
+         for(Map.Entry entry : map.entrySet())
+         {
+            if(StringUtils.hasContent(String.valueOf(entry.getValue())))
+            {
+               record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue());
+            }
+         }
+
+         QTableMetaData tableMetaData = qInstance.getTable(table);
+
+         record.setValue(tableMetaData.getPrimaryKeyField(), context.pathParam("primaryKey"));
+
+         UpdateRequest updateRequest = new UpdateRequest(qInstance);
+         setupSession(context, updateRequest);
+         updateRequest.setTableName(table);
+         updateRequest.setRecords(recordList);
+
+         UpdateAction updateAction = new UpdateAction();
+         UpdateResult updateResult = updateAction.execute(updateRequest);
+
+         context.result(JsonUtils.toJson(updateResult));
+      }
+      catch(Exception e)
+      {
+         handleException(context, e);
+      }
    }
 
 
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
index b2266896..3d2a4ac6 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -20,7 +20,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;
 
@@ -147,9 +147,9 @@ class QJavalinImplementationTest
       JSONObject record0 = records.getJSONObject(0);
       assertTrue(record0.has("values"));
       assertEquals("person", record0.getString("tableName"));
-      assertTrue(record0.has("primaryKey"));
       JSONObject values0 = record0.getJSONObject("values");
       assertTrue(values0.has("firstName"));
+      assertTrue(values0.has("id"));
    }
 
 
@@ -200,10 +200,42 @@ class QJavalinImplementationTest
       JSONObject record0 = records.getJSONObject(0);
       assertTrue(record0.has("values"));
       assertEquals("person", record0.getString("tableName"));
-      assertTrue(record0.has("primaryKey"));
       JSONObject values0 = record0.getJSONObject("values");
       assertTrue(values0.has("firstName"));
       assertEquals("Bobby", values0.getString("firstName"));
+      assertTrue(values0.has("id"));
+      assertEquals(6, values0.getInt("id"));
+   }
+
+
+   /*******************************************************************************
+    ** test an update
+    **
+    *******************************************************************************/
+   @Test
+   public void test_dataUpdate()
+   {
+      Map body = new HashMap<>();
+      body.put("firstName", "Free");
+      //? body.put("id", 4);
+
+      HttpResponse response = Unirest.patch(BASE_URL + "/data/person/4")
+         .header("Content-Type", "application/json")
+         .body(body)
+         .asString();
+
+      assertEquals(200, response.getStatus());
+      JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+      assertTrue(jsonObject.has("records"));
+      JSONArray records = jsonObject.getJSONArray("records");
+      assertEquals(1, records.length());
+      JSONObject record0 = records.getJSONObject(0);
+      assertTrue(record0.has("values"));
+      assertEquals("person", record0.getString("tableName"));
+      JSONObject values0 = record0.getJSONObject("values");
+      assertEquals(4, values0.getInt("id"));
+      assertEquals("Free", values0.getString("firstName"));
+      // mmm, whole record isn't loaded.  should it be? assertEquals("Samples", values0.getString("lastName"));
    }
 
 
@@ -224,13 +256,13 @@ class QJavalinImplementationTest
       JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
       assertNotNull(jsonObject);
       assertEquals(1, jsonObject.getJSONArray("records").length());
-      assertEquals(3, jsonObject.getJSONArray("records").getJSONObject(0).getInt("primaryKey"));
+      assertEquals(3, jsonObject.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
       TestUtils.runTestSql("SELECT id FROM person", (rs -> {
          int rowsFound = 0;
          while(rs.next())
          {
             rowsFound++;
-            assertFalse(rs.getInt(1) == 3);
+            assertNotEquals(3, rs.getInt(1));
          }
          assertEquals(4, rowsFound);
       }));

From 588dfdbad8e6688e862f9631e9d939faaf36290c Mon Sep 17 00:00:00 2001
From: tsamplesKR <93012035+tsamplesKR@users.noreply.github.com>
Date: Wed, 27 Apr 2022 13:03:49 -0500
Subject: [PATCH 11/60] Increased javalin minor to fix an issue with hosting
 ReactJS application

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 8f6f7dc2..3355595a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,7 +43,7 @@
       
          io.javalin
          javalin
-         3.13.10
+         3.13.13
       
       
          com.konghq

From 0a60cfc0bea491f71aa6f43e017c030c1891b8ec Mon Sep 17 00:00:00 2001
From: tsamplesKR <93012035+tsamplesKR@users.noreply.github.com>
Date: Thu, 28 Apr 2022 12:34:35 -0500
Subject: [PATCH 12/60] Update workflow to publish jar in qqq-maven

---
 .github/workflows/maven.yml | 35 +++++++++++++++++++++++++++++++++++
 pom.xml                     | 17 +++++++++++++++++
 2 files changed, 52 insertions(+)
 create mode 100644 .github/workflows/maven.yml

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 00000000..80d00dfd
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,35 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: Java CI with Maven
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up JDK 17
+      uses: actions/setup-java@v2
+      with:
+        java-version: '17'
+        distribution: 'adopt'
+        cache: maven
+    - name: maven-settings-xml-action
+      uses: whelk-io/maven-settings-xml-action@v20
+      with:
+        servers: '[{ "id": "github-qqq-maven-registry", "username": "${{ secrets.QQQ_MAVEN_REGISTRY_USERNAME }}", "password": "${{ secrets.QQQ_MAVEN_REGISTRY_PASSWORD }}" }]'
+        repositories: '[{ "id": "github-qqq-maven-registry", "url": "https://maven.pkg.github.com/Kingsrook/qqq-maven-registry", "snapshots": { "enabled": "true" }}]'
+    - name: Build with Maven
+      run: mvn -B package --file pom.xml
+    - name: Publish to GitHub Packages Apache Maven
+      run: mvn deploy
+      env:
+        GITHUB_TOKEN: ${{ github.token }}
diff --git a/pom.xml b/pom.xml
index 3355595a..375fe3a0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -151,4 +151,21 @@
       
    
 
+
+   
+      
+         github
+         GitHub QQQ Maven Registry
+         https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
+      
+   
+
+   
+      
+         github-qqq-maven-registry
+         GitHub QQQ Maven Registry
+         https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
+      
+   
+
 

From 14aa98ffd9df4278e9df76d99d0a223f99e7d085 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Tue, 7 Jun 2022 16:51:30 -0500
Subject: [PATCH 13/60] QQQ-4 Applying AGPL Licence

---
 LICENSE                                       | 619 ++++++++++++++++++
 README.md                                     |  20 +
 checkstyle.xml                                |  19 +-
 pom.xml                                       |  19 +-
 .../javalin/QJavalinImplementation.java       |  19 +-
 .../javalin/QJavalinImplementationTest.java   |  19 +-
 .../qqq/backend/javalin/TestUtils.java        |  19 +-
 src/test/resources/prime-test-database.sql    |  21 +
 8 files changed, 750 insertions(+), 5 deletions(-)
 create mode 100644 LICENSE

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..ca9b0551
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,619 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. 
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md
index 9ec49901..cffb2382 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,22 @@
 # qqq-middleware-javalin
 This is a qqq middleware module, providing [javalin](https://javalin.io) access to the qqq-backend.
+
+## License
+QQQ - Low-code Application Framework for Engineers. \
+Copyright (C) 2022.  Kingsrook, LLC \
+651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States \
+contact@kingsrook.com
+https://github.com/Kingsrook/intellij-commentator-plugin
+
+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 .
diff --git a/checkstyle.xml b/checkstyle.xml
index 56a37ba1..a6a0d052 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -1,6 +1,23 @@
 
 
 
 
 
 
 .  All Rights Reserved.
+ * 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/intellij-commentator-plugin
+ *
+ * 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;
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 3d2a4ac6..c36fbaa7 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -1,5 +1,22 @@
 /*
- * Copyright © 2021-2021. Kingsrook LLC .  All Rights Reserved.
+ * 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/intellij-commentator-plugin
+ *
+ * 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;
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 094538c3..924cf2f9 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -1,5 +1,22 @@
 /*
- * Copyright © 2021-2021. Kingsrook LLC .  All Rights Reserved.
+ * 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/intellij-commentator-plugin
+ *
+ * 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;
diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql
index e5adab34..96a050d7 100644
--- a/src/test/resources/prime-test-database.sql
+++ b/src/test/resources/prime-test-database.sql
@@ -1,3 +1,24 @@
+/*
+ * 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/intellij-commentator-plugin
+ *
+ * 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 .
+ */
+
 DROP TABLE IF EXISTS person;
 CREATE TABLE person
 (

From e1a0281e4cab484aed54196e21de65b4558bd9d7 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Wed, 8 Jun 2022 09:07:32 -0500
Subject: [PATCH 14/60] Fixing github link in copyright

---
 checkstyle.xml                                                  | 2 +-
 pom.xml                                                         | 2 +-
 .../kingsrook/qqq/backend/javalin/QJavalinImplementation.java   | 2 +-
 .../qqq/backend/javalin/QJavalinImplementationTest.java         | 2 +-
 src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java  | 2 +-
 src/test/resources/prime-test-database.sql                      | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/checkstyle.xml b/checkstyle.xml
index a6a0d052..dbaa3479 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -4,7 +4,7 @@
   ~ 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/intellij-commentator-plugin
+  ~ 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
diff --git a/pom.xml b/pom.xml
index fb894d38..50839179 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   ~ 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/intellij-commentator-plugin
+  ~ 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
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 74326d82..ca33f553 100644
--- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -3,7 +3,7 @@
  * 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/intellij-commentator-plugin
+ * 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
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 c36fbaa7..5f17251a 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -3,7 +3,7 @@
  * 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/intellij-commentator-plugin
+ * 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
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 924cf2f9..6d3c4e8f 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -3,7 +3,7 @@
  * 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/intellij-commentator-plugin
+ * 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
diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql
index 96a050d7..6227f249 100644
--- a/src/test/resources/prime-test-database.sql
+++ b/src/test/resources/prime-test-database.sql
@@ -3,7 +3,7 @@
  * 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/intellij-commentator-plugin
+ * 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

From 3187eceb2214390fb26c9795ec644b4ea3ad226d Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Mon, 13 Jun 2022 14:47:51 -0500
Subject: [PATCH 15/60] Real-development-mode ready versions of pom and CI
 scripts

---
 .circleci/config.yml        | 77 +++++++++++++++++++++++++++++++++++++
 .circleci/mvn-settings.xml  |  9 +++++
 .github/workflows/maven.yml | 35 -----------------
 pom.xml                     | 35 +++++++++++++----
 4 files changed, 113 insertions(+), 43 deletions(-)
 create mode 100644 .circleci/config.yml
 create mode 100644 .circleci/mvn-settings.xml
 delete mode 100644 .github/workflows/maven.yml

diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000..7c158891
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,77 @@
+version: 2.1
+
+executors:
+  java17:
+    docker:
+      - image: 'cimg/openjdk:17.0'
+    resource_class: small
+
+orbs:
+  slack: circleci/slack@4.10.1
+
+commands:
+  run_maven:
+    parameters:
+      maven_subcommand:
+        default: test
+        type: string
+    steps:
+      - checkout
+      - restore_cache:
+          keys:
+            - v1-dependencies-{{ checksum "pom.xml" }}
+      - run:
+          name: Run Maven
+          command: |
+            mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >>
+      - run:
+          name: Save test results
+          command: |
+            mkdir -p ~/test-results/junit/
+            find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/test-results/junit/ \;
+          when: always
+      - store_test_results:
+          path: ~/test-results
+      - save_cache:
+          paths:
+            - ~/.m2
+          key: v1-dependencies-{{ checksum "pom.xml" }}
+
+jobs:
+  mvn_test:
+    executor: java17
+    steps:
+      - run_maven:
+          maven_subcommand: test
+      - slack/notify:
+          event: fail
+
+  mvn_deploy:
+    executor: java17
+    steps:
+      - run_maven:
+          maven_subcommand: deploy
+      - slack/notify:
+          event: always
+
+workflows:
+  test_only:
+    jobs:
+      - mvn_test:
+          context: [ qqq-maven-registry-credentials, kingsrook-slack ]
+          filters:
+            branches:
+              ignore: /dev/
+            tags:
+              ignore: /version-.*/
+
+  deploy:
+    jobs:
+      - mvn_deploy:
+          context: [ qqq-maven-registry-credentials, kingsrook-slack ]
+          filters:
+            branches:
+              only: /dev/
+            tags:
+              only: /version-.*/
+
diff --git a/.circleci/mvn-settings.xml b/.circleci/mvn-settings.xml
new file mode 100644
index 00000000..b2a345f0
--- /dev/null
+++ b/.circleci/mvn-settings.xml
@@ -0,0 +1,9 @@
+
+    
+        
+            github-qqq-maven-registry
+            ${env.QQQ_MAVEN_REGISTRY_USERNAME}
+            ${env.QQQ_MAVEN_REGISTRY_PASSWORD}
+        
+    
+
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
deleted file mode 100644
index 80d00dfd..00000000
--- a/.github/workflows/maven.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
-# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
-
-name: Java CI with Maven
-
-on:
-  push:
-    branches: [ main ]
-  pull_request:
-    branches: [ main ]
-
-jobs:
-  build:
-
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v2
-    - name: Set up JDK 17
-      uses: actions/setup-java@v2
-      with:
-        java-version: '17'
-        distribution: 'adopt'
-        cache: maven
-    - name: maven-settings-xml-action
-      uses: whelk-io/maven-settings-xml-action@v20
-      with:
-        servers: '[{ "id": "github-qqq-maven-registry", "username": "${{ secrets.QQQ_MAVEN_REGISTRY_USERNAME }}", "password": "${{ secrets.QQQ_MAVEN_REGISTRY_PASSWORD }}" }]'
-        repositories: '[{ "id": "github-qqq-maven-registry", "url": "https://maven.pkg.github.com/Kingsrook/qqq-maven-registry", "snapshots": { "enabled": "true" }}]'
-    - name: Build with Maven
-      run: mvn -B package --file pom.xml
-    - name: Publish to GitHub Packages Apache Maven
-      run: mvn deploy
-      env:
-        GITHUB_TOKEN: ${{ github.token }}
diff --git a/pom.xml b/pom.xml
index 50839179..6cc2d8ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,14 +20,18 @@
   ~ along with this program.  If not, see .
   -->
 
-
+
    4.0.0
 
    com.kingsrook.qqq
    qqq-middleware-javalin
-   0.0-SNAPSHOT
+   0.0.0-SNAPSHOT
+
+   
+     scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git
+     scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git
+     HEAD
+   
 
    
       
@@ -47,12 +51,12 @@
       
          com.kingsrook.qqq
          qqq-backend-core
-         0.0-SNAPSHOT
+         0.0.0-SNAPSHOT
       
       
          com.kingsrook.qqq
          qqq-backend-module-rdbms
-         0.0-SNAPSHOT
+         0.0.0-SNAPSHOT
          test
       
 
@@ -165,13 +169,28 @@
                
             
          
+         
+            com.amashchenko.maven.plugin
+            gitflow-maven-plugin
+            1.18.0
+            
+               
+                  main
+                  dev
+                  version-
+               
+               true 
+               install  
+               true
+               1 
+            
+         
       
    
 
-
    
       
-         github
+         github-qqq-maven-registry
          GitHub QQQ Maven Registry
          https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
       

From 0c18c377e6decea0b33ebe8a79cf19bf67ad8388 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Mon, 13 Jun 2022 14:50:01 -0500
Subject: [PATCH 16/60] Update to strip away comment lines (e.g., copyright)
 and use -- comments

---
 .../qqq/backend/javalin/TestUtils.java        |  1 +
 src/test/resources/prime-test-database.sql    | 40 +++++++++----------
 2 files changed, 21 insertions(+), 20 deletions(-)

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 6d3c4e8f..7b2e36ae 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -57,6 +57,7 @@ public class TestUtils
       InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
       assertNotNull(primeTestDatabaseSqlStream);
       List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream);
+      lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList();
       String joinedSQL = String.join("\n", lines);
       for(String sql : joinedSQL.split(";"))
       {
diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql
index 6227f249..0780239c 100644
--- a/src/test/resources/prime-test-database.sql
+++ b/src/test/resources/prime-test-database.sql
@@ -1,23 +1,23 @@
-/*
- * 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 .
- */
+--
+-- 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 .
+--
 
 DROP TABLE IF EXISTS person;
 CREATE TABLE person

From 3069003083d010f76b7bb4380b80dec51c4b43ac Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Jun 2022 20:04:08 +0000
Subject: [PATCH 17/60] Bump log4j-core from 2.15.0 to 2.17.1

Bumps log4j-core from 2.15.0 to 2.17.1.

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] 
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 6cc2d8ac..6c3e7db1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,7 +108,7 @@
       
          org.apache.logging.log4j
          log4j-core
-         2.15.0
+         2.17.1
       
       
          org.junit.jupiter

From f6fff7ca2517a08101306616ee4921702a7f47cb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Jun 2022 20:25:19 +0000
Subject: [PATCH 18/60] Bump h2 from 1.4.197 to 2.1.210

Bumps [h2](https://github.com/h2database/h2database) from 1.4.197 to 2.1.210.
- [Release notes](https://github.com/h2database/h2database/releases)
- [Commits](https://github.com/h2database/h2database/compare/version-1.4.197...version-2.1.210)

---
updated-dependencies:
- dependency-name: com.h2database:h2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] 
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 6c3e7db1..da3cd77e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,7 +75,7 @@
       
          com.h2database
          h2
-         1.4.197
+         2.1.210
          test
       
       

From e3a7105b9960dd4f2fac81d086afb4ce5495e72a Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Mon, 13 Jun 2022 15:27:01 -0500
Subject: [PATCH 19/60] Fixing test for updated version of h2

---
 .../qqq/backend/javalin/QJavalinImplementationTest.java         | 1 +
 src/test/resources/prime-test-database.sql                      | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

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 5f17251a..06189301 100644
--- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -203,6 +203,7 @@ class QJavalinImplementationTest
       Map body = new HashMap<>();
       body.put("firstName", "Bobby");
       body.put("lastName", "Hull");
+      body.put("email", "bobby@hull.com");
 
       HttpResponse response = Unirest.post(BASE_URL + "/data/person")
          .header("Content-Type", "application/json")
diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql
index 0780239c..be858987 100644
--- a/src/test/resources/prime-test-database.sql
+++ b/src/test/resources/prime-test-database.sql
@@ -22,7 +22,7 @@
 DROP TABLE IF EXISTS person;
 CREATE TABLE person
 (
-   id SERIAL,
+   id INT AUTO_INCREMENT,
    create_date TIMESTAMP DEFAULT now(),
    modify_date TIMESTAMP DEFAULT now(),
 

From 826455f79ec47a9ea760b865501cb48778b45310 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Jun 2022 20:30:58 +0000
Subject: [PATCH 20/60] Bump log4j-api from 2.15.0 to 2.17.1

Bumps log4j-api from 2.15.0 to 2.17.1.

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] 
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index da3cd77e..05c9b813 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,7 +103,7 @@
       
          org.apache.logging.log4j
          log4j-api
-         2.15.0
+         2.17.1
       
       
          org.apache.logging.log4j

From b453ab85dbbd470cc4a034318e411f2f9b718eed Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Mon, 13 Jun 2022 15:50:56 -0500
Subject: [PATCH 21/60] Fixed github link in License

---
 README.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/README.md b/README.md
index cffb2382..3920b9a6 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,7 @@ This is a qqq middleware module, providing [javalin](https://javalin.io) access
 QQQ - Low-code Application Framework for Engineers. \
 Copyright (C) 2022.  Kingsrook, LLC \
 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States \
-contact@kingsrook.com
-https://github.com/Kingsrook/intellij-commentator-plugin
+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

From 399733a2b0c56a6dc9295fad6eaf04d6c963eaf5 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Tue, 14 Jun 2022 12:19:47 -0500
Subject: [PATCH 22/60] Removing slf4j-api and redundant slf4j-simple; bumping
 log4j to 2.17.2 and junit to 5.8.2

---
 pom.xml | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

diff --git a/pom.xml b/pom.xml
index 05c9b813..ebb1132b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,17 +81,7 @@
       
          org.slf4j
          slf4j-simple
-         1.7.30
-      
-      
-         org.slf4j
-         slf4j-api
-         1.7.30
-      
-      
-         org.slf4j
-         slf4j-simple
-         1.7.30
+         1.7.36
       
 
       
@@ -103,17 +93,17 @@
       
          org.apache.logging.log4j
          log4j-api
-         2.17.1
+         2.17.2
       
       
          org.apache.logging.log4j
          log4j-core
-         2.17.1
+         2.17.2
       
       
          org.junit.jupiter
          junit-jupiter-engine
-         5.8.1
+         5.8.2
          test
       
    

From 9460a2133e599e7419a1799dca2eb80996c18972 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Tue, 28 Jun 2022 12:20:23 -0500
Subject: [PATCH 23/60] Update to build on snapshot tags

---
 .circleci/config.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 7c158891..0ef02745 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -63,7 +63,7 @@ workflows:
             branches:
               ignore: /dev/
             tags:
-              ignore: /version-.*/
+              ignore: /(version|snapshot)-.*/
 
   deploy:
     jobs:
@@ -73,5 +73,5 @@ workflows:
             branches:
               only: /dev/
             tags:
-              only: /version-.*/
+              only: /(version|snapshot)-.*/
 

From 5c9a12bf5f7a2600a1e800fcdb4222b7e3fafdb5 Mon Sep 17 00:00:00 2001
From: Darin Kelkhoff 
Date: Tue, 28 Jun 2022 12:20:29 -0500
Subject: [PATCH 24/60] Update to sync with other modules

---
 checkstyle.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/checkstyle.xml b/checkstyle.xml
index dbaa3479..76f872ed 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -46,6 +46,7 @@
    -->
 
    
+      
       
       
          
@@ -171,7 +172,7 @@
          
          
          
-         
+         
       
       
       
-      
       
             
          
+         
+            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 39/60] 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 40/60] 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 41/60] 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 42/60] 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 43/60] 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 44/60] 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 45/60] 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 46/60] 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 From 22b286c0202b5a6f7a588582ca65a3e7bfbdc053 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 10:21:52 -0500 Subject: [PATCH 47/60] Update for next development version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3a7158aa..5489a369 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-javalin - 0.1.0 + 0.2.0-SNAPSHOT scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git From be1482ce8fabc971697b922a515325e6e56000e8 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 13:05:55 -0500 Subject: [PATCH 48/60] Reorganize packages; rename Request to Input and Response to Output --- pom.xml | 4 +- .../javalin/QJavalinImplementation.java | 168 +++++++++--------- .../javalin/QJavalinProcessHandler.java | 74 ++++---- .../javalin/QJavalinProcessHandlerTest.java | 6 +- .../qqq/backend/javalin/TestUtils.java | 28 +-- 5 files changed, 140 insertions(+), 140 deletions(-) diff --git a/pom.xml b/pom.xml index 5489a369..70e32c14 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0 + 0.2.0-SNAPSHOT com.kingsrook.qqq qqq-backend-module-rdbms - 0.1.0 + 0.2.0-SNAPSHOT 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 437f982f..392a58ff 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -29,45 +29,45 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -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.TableMetaDataAction; -import com.kingsrook.qqq.backend.core.actions.UpdateAction; +import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; +import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.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.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; -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.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; -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.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher; -import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -221,7 +221,7 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException + static void setupSession(Context context, AbstractActionInput request) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); @@ -248,13 +248,13 @@ public class QJavalinImplementation List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); - DeleteRequest deleteRequest = new DeleteRequest(qInstance); - setupSession(context, deleteRequest); - deleteRequest.setTableName(table); - deleteRequest.setPrimaryKeys(primaryKeys); + DeleteInput deleteInput = new DeleteInput(qInstance); + setupSession(context, deleteInput); + deleteInput.setTableName(table); + deleteInput.setPrimaryKeys(primaryKeys); DeleteAction deleteAction = new DeleteAction(); - DeleteResult deleteResult = deleteAction.execute(deleteRequest); + DeleteOutput deleteResult = deleteAction.execute(deleteInput); context.result(JsonUtils.toJson(deleteResult)); } @@ -292,13 +292,13 @@ public class QJavalinImplementation record.setValue(tableMetaData.getPrimaryKeyField(), context.pathParam("primaryKey")); - UpdateRequest updateRequest = new UpdateRequest(qInstance); - setupSession(context, updateRequest); - updateRequest.setTableName(table); - updateRequest.setRecords(recordList); + UpdateInput updateInput = new UpdateInput(qInstance); + setupSession(context, updateInput); + updateInput.setTableName(table); + updateInput.setRecords(recordList); UpdateAction updateAction = new UpdateAction(); - UpdateResult updateResult = updateAction.execute(updateRequest); + UpdateOutput updateResult = updateAction.execute(updateInput); context.result(JsonUtils.toJson(updateResult)); } @@ -332,15 +332,15 @@ public class QJavalinImplementation } } - InsertRequest insertRequest = new InsertRequest(qInstance); - setupSession(context, insertRequest); - insertRequest.setTableName(table); - insertRequest.setRecords(recordList); + InsertInput insertInput = new InsertInput(qInstance); + setupSession(context, insertInput); + insertInput.setTableName(table); + insertInput.setRecords(recordList); InsertAction insertAction = new InsertAction(); - InsertResult insertResult = insertAction.execute(insertRequest); + InsertOutput insertOutput = insertAction.execute(insertInput); - context.result(JsonUtils.toJson(insertResult)); + context.result(JsonUtils.toJson(insertOutput)); } catch(Exception e) { @@ -359,11 +359,11 @@ public class QJavalinImplementation { String tableName = context.pathParam("table"); QTableMetaData table = qInstance.getTable(tableName); - String primaryKey = context.pathParam("primaryKey"); - QueryRequest queryRequest = new QueryRequest(qInstance); + String primaryKey = context.pathParam("primaryKey"); + QueryInput queryInput = new QueryInput(qInstance); - setupSession(context, queryRequest); - queryRequest.setTableName(tableName); + setupSession(context, queryInput); + queryInput.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) @@ -371,25 +371,25 @@ public class QJavalinImplementation /////////////////////////////////////////////////////// // setup a filter for the primaryKey = the path-pram // /////////////////////////////////////////////////////// - queryRequest.setFilter(new QQueryFilter() + queryInput.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); + QueryOutput queryOutput = queryAction.execute(queryInput); /////////////////////////////////////////////////////// // throw a not found error if the record isn't found // /////////////////////////////////////////////////////// - if(queryResult.getRecords().isEmpty()) + if(queryOutput.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))); + context.result(JsonUtils.toJson(queryOutput.getRecords().get(0))); } catch(Exception e) { @@ -415,20 +415,20 @@ public class QJavalinImplementation { try { - CountRequest countRequest = new CountRequest(qInstance); - setupSession(context, countRequest); - countRequest.setTableName(context.pathParam("table")); + CountInput countInput = new CountInput(qInstance); + setupSession(context, countInput); + countInput.setTableName(context.pathParam("table")); String filter = stringQueryParam(context, "filter"); if(filter != null) { - countRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + countInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); } CountAction countAction = new CountAction(); - CountResult countResult = countAction.execute(countRequest); + CountOutput countOutput = countAction.execute(countInput); - context.result(JsonUtils.toJson(countResult)); + context.result(JsonUtils.toJson(countOutput)); } catch(Exception e) { @@ -456,22 +456,22 @@ public class QJavalinImplementation { try { - QueryRequest queryRequest = new QueryRequest(qInstance); - setupSession(context, queryRequest); - queryRequest.setTableName(context.pathParam("table")); - queryRequest.setSkip(integerQueryParam(context, "skip")); - queryRequest.setLimit(integerQueryParam(context, "limit")); + QueryInput queryInput = new QueryInput(qInstance); + setupSession(context, queryInput); + queryInput.setTableName(context.pathParam("table")); + queryInput.setSkip(integerQueryParam(context, "skip")); + queryInput.setLimit(integerQueryParam(context, "limit")); String filter = stringQueryParam(context, "filter"); if(filter != null) { - queryRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + queryInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); } QueryAction queryAction = new QueryAction(); - QueryResult queryResult = queryAction.execute(queryRequest); + QueryOutput queryOutput = queryAction.execute(queryInput); - context.result(JsonUtils.toJson(queryResult)); + context.result(JsonUtils.toJson(queryOutput)); } catch(Exception e) { @@ -488,12 +488,12 @@ public class QJavalinImplementation { try { - MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance); - setupSession(context, metaDataRequest); + MetaDataInput metaDataInput = new MetaDataInput(qInstance); + setupSession(context, metaDataInput); MetaDataAction metaDataAction = new MetaDataAction(); - MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest); + MetaDataOutput metaDataOutput = metaDataAction.execute(metaDataInput); - context.result(JsonUtils.toJson(metaDataResult)); + context.result(JsonUtils.toJson(metaDataOutput)); } catch(Exception e) { @@ -510,13 +510,13 @@ public class QJavalinImplementation { try { - TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance); - setupSession(context, tableMetaDataRequest); - tableMetaDataRequest.setTableName(context.pathParam("table")); + TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(qInstance); + setupSession(context, tableMetaDataInput); + tableMetaDataInput.setTableName(context.pathParam("table")); TableMetaDataAction tableMetaDataAction = new TableMetaDataAction(); - TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest); + TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput); - context.result(JsonUtils.toJson(tableMetaDataResult)); + context.result(JsonUtils.toJson(tableMetaDataOutput)); } catch(Exception e) { @@ -533,13 +533,13 @@ public class QJavalinImplementation { try { - ProcessMetaDataRequest processMetaDataRequest = new ProcessMetaDataRequest(qInstance); - setupSession(context, processMetaDataRequest); - processMetaDataRequest.setProcessName(context.pathParam("processName")); + ProcessMetaDataInput processMetaDataInput = new ProcessMetaDataInput(qInstance); + setupSession(context, processMetaDataInput); + processMetaDataInput.setProcessName(context.pathParam("processName")); ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction(); - ProcessMetaDataResult processMetaDataResult = processMetaDataAction.execute(processMetaDataRequest); + ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput); - context.result(JsonUtils.toJson(processMetaDataResult)); + context.result(JsonUtils.toJson(processMetaDataOutput)); } catch(Exception e) { 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 aaac1a36..308634a9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -33,26 +33,26 @@ import java.util.Objects; 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.actions.processes.QProcessCallback; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; 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.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.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; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -135,31 +135,31 @@ public class QJavalinProcessHandler 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); + RunProcessInput runProcessInput = new RunProcessInput(QJavalinImplementation.qInstance); + QJavalinImplementation.setupSession(context, runProcessInput); + runProcessInput.setProcessName(processName); + runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + runProcessInput.setProcessUUID(processUUID); + runProcessInput.setStartAfterStep(startAfterStep); + populateRunProcessRequestWithValuesFromContext(context, runProcessInput); //////////////////////////////////////// // run the process as an async action // //////////////////////////////////////// Integer timeout = getTimeoutMillis(context); - RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> + RunProcessOutput runProcessOutput = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) -> { - runProcessRequest.setAsyncJobCallback(callback); - return (new RunProcessAction().execute(runProcessRequest)); + runProcessInput.setAsyncJobCallback(callback); + return (new RunProcessAction().execute(runProcessInput)); }); - LOG.info("Process result error? " + runProcessResult.getException()); - for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields()) + LOG.info("Process result error? " + runProcessOutput.getException()); + for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessInput.getProcessName()).getOutputFields()) { - LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName())); + LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessOutput.getValues().get(outputField.getName())); } - serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + serializeRunProcessResultForCaller(resultForCaller, runProcessOutput); } catch(JobGoingAsyncException jgae) { @@ -185,17 +185,17 @@ public class QJavalinProcessHandler ** Whether a step finished synchronously or asynchronously, return its data ** to the caller the same way. *******************************************************************************/ - private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessResult runProcessResult) + private static void serializeRunProcessResultForCaller(Map resultForCaller, RunProcessOutput runProcessOutput) { - if(runProcessResult.getException().isPresent()) + if(runProcessOutput.getException().isPresent()) { //////////////////////////////////////////////////////////////// // per code coverage, this path may never actually get hit... // //////////////////////////////////////////////////////////////// - serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get()); + serializeRunProcessExceptionForCaller(resultForCaller, runProcessOutput.getException().get()); } - resultForCaller.put("values", runProcessResult.getValues()); - runProcessResult.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep)); + resultForCaller.put("values", runProcessOutput.getValues()); + runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep)); } @@ -227,7 +227,7 @@ public class QJavalinProcessHandler ** todo - better from POST body, or with a "field-" type of prefix?? ** *******************************************************************************/ - private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) throws IOException + private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessInput runProcessInput) throws IOException { for(Map.Entry> queryParam : context.queryParamMap().entrySet()) { @@ -235,14 +235,14 @@ public class QJavalinProcessHandler List values = queryParam.getValue(); if(CollectionUtils.nullSafeHasContents(values)) { - runProcessRequest.addValue(fieldName, values.get(0)); + runProcessInput.addValue(fieldName, values.get(0)); } } - QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessRequest); + QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessInput); if(initialRecordsFilter != null) { - runProcessRequest.setCallback(new QProcessCallback() + runProcessInput.setCallback(new QProcessCallback() { @Override public QQueryFilter getQueryFilter() @@ -266,10 +266,10 @@ public class QJavalinProcessHandler /******************************************************************************* ** *******************************************************************************/ - private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessRequest runProcessRequest) throws IOException + private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessInput runProcessInput) throws IOException { - QInstance instance = runProcessRequest.getInstance(); - QProcessMetaData process = instance.getProcess(runProcessRequest.getProcessName()); + QInstance instance = runProcessInput.getInstance(); + QProcessMetaData process = instance.getProcess(runProcessInput.getProcessName()); QTableMetaData table = instance.getTable(process.getTableName()); if(table == null) @@ -358,8 +358,8 @@ public class QJavalinProcessHandler Optional processState = RunProcessAction.getState(processUUID); if(processState.isPresent()) { - RunProcessResult runProcessResult = new RunProcessResult(processState.get()); - serializeRunProcessResultForCaller(resultForCaller, runProcessResult); + RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get()); + serializeRunProcessResultForCaller(resultForCaller, runProcessOutput); } else { 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 91cc0479..3effbf77 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java @@ -26,9 +26,9 @@ 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.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; 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 133d1c07..ecede5f9 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -27,18 +27,18 @@ 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.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; -import com.kingsrook.qqq.backend.core.model.metadata.QCodeUsage; -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.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.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; @@ -311,11 +311,11 @@ public class TestUtils ** ******************************************************************************/ @Override - public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { try { - Thread.sleep(runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS)); + Thread.sleep(runBackendStepInput.getValueInteger(FIELD_SLEEP_MILLIS)); } catch(InterruptedException e) { @@ -357,12 +357,12 @@ public class TestUtils ** ******************************************************************************/ @Override - public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { int sleepMillis; try { - sleepMillis = runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS); + sleepMillis = runBackendStepInput.getValueInteger(FIELD_SLEEP_MILLIS); } catch(QValueException qve) { From caaf7e3d9311a30a4b7fd0ecea1c7dff5d31f9a4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Jul 2022 12:49:18 -0500 Subject: [PATCH 49/60] QQQ-26 add table exports --- .circleci/config.yml | 2 + .gitignore | 1 + pom.xml | 12 +- .../javalin/QJavalinImplementation.java | 229 +++++++++++++++--- .../javalin/QJavalinProcessHandler.java | 8 +- .../javalin/QJavalinImplementationTest.java | 102 +++++++- 6 files changed, 314 insertions(+), 40 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fe4c371c..658f75d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,8 @@ commands: name: Run Maven command: | mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >> + - store_artifacts: + path: target/site/jacoco - run: name: Save test results command: | diff --git a/.gitignore b/.gitignore index 39736a21..b65cb041 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +.DS_Store diff --git a/pom.xml b/pom.xml index 70e32c14..037a81e1 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220719.154219-3 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220719.155012-5 test @@ -66,7 +66,7 @@ io.javalin javalin - 3.13.13 + 4.6.1 com.konghq @@ -85,6 +85,12 @@ slf4j-simple 1.7.36 + + org.assertj + assertj-core + 3.23.1 + test + diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 392a58ff..0e5ce3c9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -24,14 +24,21 @@ package com.kingsrook.qqq.backend.javalin; import java.io.File; import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -49,6 +56,8 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInpu import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; @@ -182,36 +191,46 @@ public class QJavalinImplementation { return (() -> { + ///////////////////// + // metadata routes // + ///////////////////// path("/metaData", () -> { get("/", QJavalinImplementation::metaData); - path("/table/:table", () -> + path("/table/{table}", () -> { get("", QJavalinImplementation::tableMetaData); }); - path("/process/:processName", () -> + path("/process/{processName}", () -> { get("", QJavalinImplementation::processMetaData); }); }); - path("/data", () -> - { - path("/:table", () -> - { - get("/", QJavalinImplementation::dataQuery); - post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. - get("/count", QJavalinImplementation::dataCount); - // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records. - path("/:primaryKey", () -> - { - get("", QJavalinImplementation::dataGet); - patch("", QJavalinImplementation::dataUpdate); - put("", QJavalinImplementation::dataUpdate); // todo - want different semantics?? - delete("", QJavalinImplementation::dataDelete); - }); + ///////////////////////// + // table (data) routes // + ///////////////////////// + path("/data/{table}", () -> + { + get("/", QJavalinImplementation::dataQuery); + post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single. + get("/count", QJavalinImplementation::dataCount); + get("/export", QJavalinImplementation::dataExportWithoutFilename); + get("/export/{filename}", QJavalinImplementation::dataExportWithFilename); + + // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records. + path("/{primaryKey}", () -> + { + get("", QJavalinImplementation::dataGet); + patch("", QJavalinImplementation::dataUpdate); + put("", QJavalinImplementation::dataUpdate); // todo - want different semantics?? + delete("", QJavalinImplementation::dataDelete); }); }); + + //////////////////// + // process routes // + //////////////////// path("", QJavalinProcessHandler.getRoutes()); }); } @@ -221,16 +240,16 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - static void setupSession(Context context, AbstractActionInput request) throws QModuleDispatchException + static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); // todo - does this need some per-provider logic actually? mmm... Map authenticationContext = new HashMap<>(); authenticationContext.put("sessionId", context.cookie("sessionId")); QSession session = authenticationModule.createSession(authenticationContext); - request.setSession(session); + input.setSession(session); context.cookie("sessionId", session.getIdReference(), SESSION_COOKIE_AGE); } @@ -357,10 +376,10 @@ public class QJavalinImplementation { try { - String tableName = context.pathParam("table"); - QTableMetaData table = qInstance.getTable(tableName); - String primaryKey = context.pathParam("primaryKey"); - QueryInput queryInput = new QueryInput(qInstance); + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); + QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); queryInput.setTableName(tableName); @@ -549,31 +568,181 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataExportWithFilename(Context context) + { + String filename = context.pathParam("filename"); + dataExport(context, Optional.of(filename)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataExportWithoutFilename(Context context) + { + dataExport(context, Optional.empty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void dataExport(Context context, Optional optionalFilename) + { + try + { + ////////////////////////////////////////// + // read params from the request context // + ////////////////////////////////////////// + String tableName = context.pathParam("table"); + String format = context.queryParam("format"); + String filter = context.queryParam("filter"); + Integer limit = integerQueryParam(context, "limit"); + + ///////////////////////////////////////////////////////////////////////////////////////// + // if a format query param wasn't given, then try to get file extension from file name // + ///////////////////////////////////////////////////////////////////////////////////////// + if(!StringUtils.hasContent(format) && optionalFilename.isPresent() && StringUtils.hasContent(optionalFilename.get())) + { + String filename = optionalFilename.get(); + if(filename.contains(".")) + { + format = filename.substring(filename.lastIndexOf(".") + 1); + } + } + + ReportFormat reportFormat; + try + { + reportFormat = ReportFormat.fromString(format); + } + catch(QUserFacingException e) + { + handleException(HttpStatus.Code.BAD_REQUEST, context, e); + return; + } + + String filename = optionalFilename.orElse(tableName + "." + reportFormat.toString().toLowerCase(Locale.ROOT)); + + ///////////////////////////////////////////// + // set up the report action's input object // + ///////////////////////////////////////////// + ReportInput reportInput = new ReportInput(qInstance); + setupSession(context, reportInput); + reportInput.setTableName(tableName); + reportInput.setReportFormat(reportFormat); + reportInput.setFilename(filename); + reportInput.setLimit(limit); + + String fields = stringQueryParam(context, "fields"); + if(StringUtils.hasContent(fields)) + { + reportInput.setFieldNames(List.of(fields.split(","))); + } + + if(filter != null) + { + reportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // set up the I/O pipe streams. // + // Critically, we must NOT open the outputStream in a try-with-resources. The thread that writes to // + // the stream must close it when it's done writing. // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + PipedOutputStream pipedOutputStream = new PipedOutputStream(); + PipedInputStream pipedInputStream = new PipedInputStream(); + pipedOutputStream.connect(pipedInputStream); + reportInput.setReportOutputStream(pipedOutputStream); + + ReportAction reportAction = new ReportAction(); + reportAction.preExecute(reportInput); + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // start the async job. // + // Critically, this must happen before the pipedInputStream is passed to the javalin result method // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + new AsyncJobManager().startJob("Javalin>ReportAction", (o) -> + { + try + { + reportAction.execute(reportInput); + return (true); + } + catch(Exception e) + { + pipedOutputStream.write(("Error generating report: " + e.getMessage()).getBytes()); + pipedOutputStream.close(); + return (false); + } + }); + + //////////////////////////////////////////// + // set the response content type & stream // + //////////////////////////////////////////// + context.contentType(reportFormat.getMimeType()); + context.header("Content-Disposition", "filename=" + filename); + context.result(pipedInputStream); + + //////////////////////////////////////////////////////////////////////////////////////////// + // we'd like to check to see if the job failed, and if so, to give the user an error... // + // but if we "block" here, then piped streams seem to never flush, so we deadlock things. // + //////////////////////////////////////////////////////////////////////////////////////////// + // AsyncJobStatus asyncJobStatus = asyncJobManager.waitForJob(jobUUID); + // if(asyncJobStatus.getState().equals(AsyncJobState.ERROR)) + // { + // System.out.println("Well, here we are..."); + // throw (new QUserFacingException("Error running report: " + asyncJobStatus.getCaughtException().getMessage())); + // } + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ public static void handleException(Context context, Exception e) + { + handleException(null, context, e); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void handleException(HttpStatus.Code statusCode, Context context, Exception e) { QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); if(userFacingException != null) { if(userFacingException instanceof QNotFoundException) { - context.status(HttpStatus.NOT_FOUND_404) - .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND).getCode(); + context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); } else { LOG.info("User-facing exception", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode(); + context.status(code).result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); } } else { LOG.warn("Exception in javalin request", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); + int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode(); + context.status(code).result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } } diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 308634a9..1c0775e9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -87,15 +87,15 @@ public class QJavalinProcessHandler { path("/processes", () -> { - path("/:processName", () -> + path("/{processName}", () -> { get("/init", QJavalinProcessHandler::processInit); post("/init", QJavalinProcessHandler::processInit); - path("/:processUUID", () -> + path("/{processUUID}", () -> { - post("/step/:step", QJavalinProcessHandler::processStep); - get("/status/:jobUUID", QJavalinProcessHandler::processStatus); + post("/step/{step}", QJavalinProcessHandler::processStep); + get("/status/{jobUUID}", QJavalinProcessHandler::processStatus); get("/records", QJavalinProcessHandler::processRecords); }); }); diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 459ff2bb..26604c8c 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -27,6 +27,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; @@ -34,6 +35,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -52,8 +54,6 @@ class QJavalinImplementationTest extends QJavalinTestBase { - - /******************************************************************************* ** test the top-level meta-data endpoint ** @@ -269,7 +269,7 @@ class QJavalinImplementationTest extends QJavalinTestBase @Test public void test_dataQueryWithFilter() { - String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; + String filterJson = getFirstNameEqualsTimFilterJSON(); HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); assertEquals(200, response.getStatus()); @@ -284,6 +284,17 @@ class QJavalinImplementationTest extends QJavalinTestBase + /******************************************************************************* + ** + *******************************************************************************/ + private String getFirstNameEqualsTimFilterJSON() + { + return """ + {"criteria":[{"fieldName":"firstName","operator":"EQUALS","values":["Tim"]}]}"""; + } + + + /******************************************************************************* ** test an insert ** @@ -375,4 +386,89 @@ class QJavalinImplementationTest extends QJavalinTestBase })); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportCsvPerFileName() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/MyPersonExport.csv").asString(); + assertEquals(200, response.getStatus()); + assertEquals("text/csv", response.getHeaders().get("Content-Type").get(0)); + assertEquals("filename=MyPersonExport.csv", response.getHeaders().get("Content-Disposition").get(0)); + String[] csvLines = response.getBody().split("\n"); + assertEquals(6, csvLines.length); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportNoFormat() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/").asString(); + assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus()); + assertThat(response.getBody()).contains("Report format was not specified"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportExcelPerFormatQueryParam() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/?format=xlsx").asString(); + assertEquals(200, response.getStatus()); + assertEquals(ReportFormat.XLSX.getMimeType(), response.getHeaders().get("Content-Type").get(0)); + assertEquals("filename=person.xlsx", response.getHeaders().get("Content-Disposition").get(0)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportFilterQueryParam() + { + String filterJson = getFirstNameEqualsTimFilterJSON(); + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/Favorite People.csv?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); + assertEquals("filename=Favorite People.csv", response.getHeaders().get("Content-Disposition").get(0)); + String[] csvLines = response.getBody().split("\n"); + assertEquals(2, csvLines.length); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportFieldsQueryParam() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString(); + String[] csvLines = response.getBody().split("\n"); + assertEquals(""" + "Id","Birth Date\"""", csvLines[0]); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExportSupportedFormat() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/?format=docx").asString(); + assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus()); + assertThat(response.getBody()).contains("Unsupported report format"); + } + } From 8010036905d2510df7448b35d2776cc3b95b9993 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 19 Jul 2022 18:24:47 -0500 Subject: [PATCH 50/60] QQQ-27: updates to allow Auth0 to be an authentication model in javalin --- .../javalin/QJavalinImplementation.java | 63 +++++++++++++------ .../qqq/backend/javalin/TestUtils.java | 5 +- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 392a58ff..aaf30649 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; @@ -66,6 +67,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; @@ -97,6 +99,7 @@ public class QJavalinImplementation private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; + private static final String SESSION_ID_COOKIE_NAME = "sessionId"; static QInstance qInstance; @@ -224,15 +227,27 @@ public class QJavalinImplementation static void setupSession(Context context, AbstractActionInput request) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); - // todo - does this need some per-provider logic actually? mmm... - Map authenticationContext = new HashMap<>(); - authenticationContext.put("sessionId", context.cookie("sessionId")); - QSession session = authenticationModule.createSession(authenticationContext); - request.setSession(session); + try + { + Map authenticationContext = new HashMap<>(); + authenticationContext.put(SESSION_ID_COOKIE_NAME, context.cookie(SESSION_ID_COOKIE_NAME)); + QSession session = authenticationModule.createSession(qInstance, authenticationContext); + request.setSession(session); - context.cookie("sessionId", session.getIdReference(), SESSION_COOKIE_AGE); + context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE); + } + catch(QAuthenticationException qae) + { + //////////////////////////////////////////////////////////////////////////////// + // if exception caught, clear out the cookie so the frontend will reauthorize // + //////////////////////////////////////////////////////////////////////////////// + if(authenticationModule instanceof Auth0AuthenticationModule) + { + context.removeCookie(SESSION_ID_COOKIE_NAME); + } + } } @@ -244,7 +259,7 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); @@ -273,9 +288,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -317,9 +332,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -357,9 +372,9 @@ public class QJavalinImplementation { try { - String tableName = context.pathParam("table"); - QTableMetaData table = qInstance.getTable(tableName); - String primaryKey = context.pathParam("primaryKey"); + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); @@ -561,20 +576,32 @@ public class QJavalinImplementation { context.status(HttpStatus.NOT_FOUND_404) .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + return; } else { LOG.info("User-facing exception", e); context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + return; } } else { - LOG.warn("Exception in javalin request", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); + if(e instanceof QAuthenticationException) + { + context.status(HttpStatus.UNAUTHORIZED_401) + .result("{\"error\":\"" + e.getMessage() + "\"}"); + return; + } } + + //////////////////////////////// + // default exception handling // + //////////////////////////////// + LOG.warn("Exception in javalin request", e); + context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) + .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index ecede5f9..818c2b53 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -28,10 +28,11 @@ import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; @@ -134,7 +135,7 @@ public class TestUtils { return new QAuthenticationMetaData() .withName("mock") - .withType("mock"); + .withType(QAuthenticationType.MOCK); } From feb01a8b234abe278dfa089f57fdaeaace221fc0 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 21 Jul 2022 11:54:02 -0500 Subject: [PATCH 51/60] QQQ-27: upped qqq-backend-core / rdbms versions --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 70e32c14..96c67854 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 test From b83bac653126622ca4e8c5e86c82b77bb0a0de8c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 22 Jul 2022 18:40:35 -0500 Subject: [PATCH 52/60] QQQ-28 updates for bulk actions --- pom.xml | 4 +- .../javalin/QJavalinProcessHandler.java | 49 +++++++++++++++++-- .../javalin/QJavalinImplementationTest.java | 3 +- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 70e32c14..f8d3c898 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220722.233134-9 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220722.233524-9 test diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 308634a9..60de405e 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; +import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; @@ -53,12 +54,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.state.StateType; +import com.kingsrook.qqq.backend.core.state.TempFileStateProvider; +import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; +import io.javalin.http.UploadedFile; import org.apache.commons.lang.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -216,7 +221,7 @@ public class QJavalinProcessHandler { Throwable rootException = ExceptionUtils.getRootException(exception); LOG.warn("Uncaught Exception in process", exception); - resultForCaller.put("error", "Original error message: " + rootException.getMessage()); + resultForCaller.put("error", "Error message: " + rootException.getMessage()); } } @@ -224,11 +229,14 @@ public class QJavalinProcessHandler /******************************************************************************* ** take values from query-string params, and put them into the run process request - ** todo - better from POST body, or with a "field-" type of prefix?? + ** todo - make query params have a "field-" type of prefix?? ** *******************************************************************************/ private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessInput runProcessInput) throws IOException { + ////////////////////////// + // process query string // + ////////////////////////// for(Map.Entry> queryParam : context.queryParamMap().entrySet()) { String fieldName = queryParam.getKey(); @@ -239,6 +247,37 @@ public class QJavalinProcessHandler } } + //////////////////////////// + // process form/post body // + //////////////////////////// + for(Map.Entry> formParam : context.formParamMap().entrySet()) + { + String fieldName = formParam.getKey(); + List values = formParam.getValue(); + if(CollectionUtils.nullSafeHasContents(values)) + { + runProcessInput.addValue(fieldName, values.get(0)); + } + } + + //////////////////////////// + // process uploaded files // + //////////////////////////// + for(UploadedFile uploadedFile : context.uploadedFiles()) + { + QUploadedFile qUploadedFile = new QUploadedFile(); + qUploadedFile.setBytes(uploadedFile.getContent().readAllBytes()); + qUploadedFile.setFilename(uploadedFile.getFilename()); + + UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); + TempFileStateProvider.getInstance().put(key, qUploadedFile); + LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key); + runProcessInput.addValue("uploadedFileKey", key); + } + + ///////////////////////////////////////////////////////////// + // deal with params that specify an initial-records filter // + ///////////////////////////////////////////////////////////// QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessInput); if(initialRecordsFilter != null) { @@ -331,11 +370,12 @@ public class QJavalinProcessHandler *******************************************************************************/ public static void processStatus(Context context) { - Map resultForCaller = new HashMap<>(); - String processUUID = context.pathParam("processUUID"); String jobUUID = context.pathParam("jobUUID"); + Map resultForCaller = new HashMap<>(); + resultForCaller.put("processUUID", processUUID); + LOG.info("Request for status of process " + processUUID + ", job " + jobUUID); Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); if(optionalJobStatus.isEmpty()) @@ -412,6 +452,7 @@ public class QJavalinProcessHandler Map resultForCaller = new HashMap<>(); List recordPage = CollectionUtils.safelyGetPage(records, skip, limit); resultForCaller.put("records", recordPage); + resultForCaller.put("totalRecords", records.size()); context.result(JsonUtils.toJson(resultForCaller)); } catch(Exception e) diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 459ff2bb..d0f7b78d 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -362,8 +362,7 @@ class QJavalinImplementationTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); - assertEquals(1, jsonObject.getJSONArray("records").length()); - assertEquals(3, jsonObject.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); + assertEquals(1, jsonObject.getInt("deletedRecordCount")); TestUtils.runTestSql("SELECT id FROM person", (rs -> { int rowsFound = 0; while(rs.next()) From fb5dcb6d825b6a15f1888f09d1b09bb32468f30e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 25 Jul 2022 13:52:56 -0500 Subject: [PATCH 53/60] Update core version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 571204d2..a888a933 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220725.183211-13 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220725.183409-11 test From a24c8e6237874172c9fd96dc8931f2d2bbdf8821 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 25 Jul 2022 13:54:42 -0500 Subject: [PATCH 54/60] Fixed exception handling --- .../qqq/backend/javalin/QJavalinImplementation.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index e932fe4a..2dddf8d6 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -761,17 +761,13 @@ public class QJavalinImplementation return; } + //////////////////////////////// + // default exception handling // + //////////////////////////////// LOG.warn("Exception in javalin request", e); int code = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR).getCode(); context.status(code).result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } - - //////////////////////////////// - // default exception handling // - //////////////////////////////// - LOG.warn("Exception in javalin request", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } From fc0a227ce4fe5d9e79f0f873befc84f955e7b2f3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 26 Jul 2022 16:40:32 -0500 Subject: [PATCH 55/60] Feedback from code reviews --- .../kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 6eb021a0..08175cdb 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -272,7 +272,7 @@ public class QJavalinProcessHandler UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); TempFileStateProvider.getInstance().put(key, qUploadedFile); LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key); - runProcessInput.addValue("uploadedFileKey", key); + runProcessInput.addValue(QUploadedFile.DEFAULT_UPLOADED_FILE_FIELD_NAME, key); } ///////////////////////////////////////////////////////////// From be48b07f0b0b1d51d75142b05131d4efd224fd3b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 26 Jul 2022 16:50:02 -0500 Subject: [PATCH 56/60] Update deps --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a888a933..b72529db 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-20220725.183211-13 + 0.2.0-20220726.214150-15 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-20220725.183409-11 + 0.2.0-20220726.214633-12 test From e5ceedd3362a9ca0b732e5e17cf1a0a2c36b0d73 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 27 Jul 2022 23:44:39 -0500 Subject: [PATCH 57/60] QQQ-27: gitignore .env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b65cb041..2c7054c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ *.iml +.env ############################################# From b6b6c35fde07deb787b13a59b7e000e84fbb5c10 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 28 Jul 2022 09:25:23 -0500 Subject: [PATCH 58/60] Update qqq-backend-core to 0.2.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b72529db..d73f2ef8 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-20220726.214150-15 + 0.2.0 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-20220726.214633-12 + 0.2.0 test From cb3f2324df46e8064e812eb2b33d4991f3c40e4c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 28 Jul 2022 09:29:23 -0500 Subject: [PATCH 59/60] Update versions for release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d73f2ef8..6b94a87c 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-javalin - 0.2.0-SNAPSHOT + 0.2.0 scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git From 29fe4fab52697ad5082c62987bf886636f3bbd1f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 28 Jul 2022 09:29:24 -0500 Subject: [PATCH 60/60] Update for next development version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b94a87c..9c530b97 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-javalin - 0.2.0 + 0.3.0-SNAPSHOT scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git