From f6e28d5e31da72456ce269e34cc5a779c9280d62 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 28 Jun 2022 12:20:53 -0500 Subject: [PATCH] QQQ-14 Initial version of running processes in javalin --- .../javalin/QJavalinImplementation.java | 93 +++++++++++++++++-- .../javalin/QJavalinProcessCallback.java | 66 +++++++++++++ .../javalin/QJavalinImplementationTest.java | 39 ++++++++ .../qqq/backend/javalin/TestUtils.java | 77 +++++++++++---- 4 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java 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 ca33f553..bfd94268 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -33,9 +33,11 @@ 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.RunProcessAction; 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.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.AbstractQRequest; @@ -47,17 +49,21 @@ 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.processes.RunProcessRequest; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; 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.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.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; @@ -84,6 +90,7 @@ import static io.javalin.apibuilder.ApiBuilder.put; 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; @@ -154,6 +161,7 @@ public class QJavalinImplementation path("/:table", () -> { get("", QJavalinImplementation::tableMetaData); + // todo - process meta data - just under tables? or top-level too? maybe move tables to be under /tables/? }); }); path("/data", () -> @@ -172,6 +180,14 @@ public class QJavalinImplementation }); }); }); + path("/processes", () -> + { + path("/:process", () -> + { + get("/init", QJavalinImplementation::processInit); + get("/step", QJavalinImplementation::processStep); + }); + }); }); } @@ -183,7 +199,7 @@ public class QJavalinImplementation private static void setupSession(Context context, AbstractQRequest 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<>(); @@ -203,7 +219,7 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); @@ -232,9 +248,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); @@ -276,9 +292,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); @@ -422,7 +438,6 @@ public class QJavalinImplementation else { LOG.warn("Exception in javalin request", e); - e.printStackTrace(); context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) .result("{\"error\":\"" + e.getClass().getSimpleName() + " (" + e.getMessage() + ")\"}"); } @@ -462,4 +477,68 @@ public class QJavalinImplementation return (null); } + + + + /******************************************************************************* + ** Init a process (named in path param :process) + ** + *******************************************************************************/ + private static void processInit(Context context) throws QException + { + RunProcessRequest runProcessRequest = new RunProcessRequest(qInstance); + setupSession(context, runProcessRequest); + runProcessRequest.setProcessName(context.pathParam("process")); + runProcessRequest.setCallback(new QJavalinProcessCallback()); + + ///////////////////////////////////////////////////////////////////////////////////// + // 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?? // + ///////////////////////////////////////////////////////////////////////////////////// + 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)); + } + } + + try + { + /////////////////////////////////////////////////////// + // run the process // + // todo - async? some "job id" to return to caller? // + /////////////////////////////////////////////////////// + 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())); + } + + Map resultForCaller = new HashMap<>(); + resultForCaller.put("error", runProcessResult.getError()); + resultForCaller.put("values", runProcessResult.getValues()); + context.result(JsonUtils.toJson(resultForCaller)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** Run a step in a process (named in path param :process) + ** + *******************************************************************************/ + private static void processStep(Context context) + { + + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java new file mode 100644 index 00000000..328a66b2 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessCallback.java @@ -0,0 +1,66 @@ +/* + * 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/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 06189301..f8376c5c 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -285,4 +285,43 @@ class QJavalinImplementationTest assertEquals(4, rowsFound); })); } + + + + /******************************************************************************* + ** test running a process + ** + *******************************************************************************/ + @Test + public void test_processGreetInit() throws Exception + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init") + .header("Content-Type", "application/json") + .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() throws Exception + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude") + .header("Content-Type", "application/json") + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage")); + } + } 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 7b2e36ae..a25a5104 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,13 +25,23 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; import java.util.List; +import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody; import com.kingsrook.qqq.backend.core.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.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.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; -import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import org.apache.commons.io.IOUtils; @@ -52,9 +62,9 @@ public class TestUtils @SuppressWarnings("unchecked") public static void primeTestDatabase() throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(TestUtils.defineBackend())); - InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); + ConnectionManager connectionManager = new ConnectionManager(); + Connection connection = connectionManager.getConnection(TestUtils.defineBackend()); + 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(); @@ -74,7 +84,7 @@ public class TestUtils public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(defineBackend())); + Connection connection = connectionManager.getConnection(defineBackend()); QueryManager.executeStatement(connection, sql, resultSetProcessor); } @@ -90,6 +100,7 @@ public class TestUtils qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addProcess(defineProcessGreetPeople()); return (qInstance); } @@ -112,16 +123,16 @@ public class TestUtils ** Define the h2 rdbms backend ** *******************************************************************************/ - public static QBackendMetaData defineBackend() + public static RDBMSBackendMetaData defineBackend() { - return new QBackendMetaData() - .withName("default") - .withType("rdbms") - .withValue("vendor", "h2") - .withValue("hostName", "mem") - .withValue("databaseName", "test_database") - .withValue("username", "sa") - .withValue("password", ""); + RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData() + .withVendor("h2") + .withHostName("mem") + .withDatabaseName("test_database") + .withUsername("sa") + .withPassword(""); + rdbmsBackendMetaData.setName("default"); + return (rdbmsBackendMetaData); } @@ -146,4 +157,38 @@ public class TestUtils .withField(new QFieldMetaData("email", QFieldType.STRING)); } + + + /******************************************************************************* + ** Define the 'greet people' process + *******************************************************************************/ + private static QProcessMetaData defineProcessGreetPeople() + { + return new QProcessMetaData() + .withName("greet") + .withTableName("person") + .addFunction(new QFunctionMetaData() + .withName("prepare") + .withCode(new QCodeReference() + .withName(MockFunctionBody.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withFieldList(List.of( + new QFieldMetaData("greetingPrefix", QFieldType.STRING), + new QFieldMetaData("greetingSuffix", QFieldType.STRING) + ))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName("person") + .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) + ) + .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) + .withOutputView(new QOutputView() + .withMessageField("outputMessage") + .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) + ); + } + }