From de3eabb1cf443cb0b2254197d0162bc25eea1c0c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 6 Jul 2022 13:55:50 -0500 Subject: [PATCH] QQQ-21 adding process metaData, single-record GET --- pom.xml | 2 +- .../javalin/QJavalinImplementation.java | 88 ++++++++++++- .../javalin/QJavalinImplementationTest.java | 117 ++++++++++++++++-- .../qqq/backend/javalin/TestUtils.java | 63 ++++++++-- 4 files changed, 244 insertions(+), 26 deletions(-) diff --git a/pom.xml b/pom.xml index e9e67df9..174fa7b9 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ com.kingsrook.qqq qqq-backend-core - 0.0.0 + 0.1.0-20220706.184937-2 com.kingsrook.qqq diff --git a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 6c68564d..de2d2dff 100644 --- a/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -36,6 +36,7 @@ import java.util.concurrent.TimeoutException; import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; +import com.kingsrook.qqq.backend.core.actions.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.QueryAction; import com.kingsrook.qqq.backend.core.actions.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; @@ -43,6 +44,7 @@ import com.kingsrook.qqq.backend.core.actions.UpdateAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest; @@ -51,10 +53,14 @@ import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest; import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataResult; +import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataRequest; +import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataResult; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest; import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; +import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest; import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult; @@ -162,10 +168,13 @@ public class QJavalinImplementation path("/metaData", () -> { get("/", QJavalinImplementation::metaData); - path("/:table", () -> + path("/table/:table", () -> { get("", QJavalinImplementation::tableMetaData); - // todo - process meta data - just under tables? or top-level too? maybe move tables to be under /tables/? + }); + path("/process/:process", () -> + { + get("", QJavalinImplementation::processMetaData); }); }); path("/data", () -> @@ -334,7 +343,43 @@ public class QJavalinImplementation ********************************************************************************/ private static void dataGet(Context context) { - context.result("{\"todo\":\"not-done\",\"getResult\":{}}"); + try + { + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); + QueryRequest queryRequest = new QueryRequest(qInstance); + + setupSession(context, queryRequest); + queryRequest.setTableName(tableName); + + /////////////////////////////////////////////////////// + // setup a filter for the primaryKey = the path-pram // + /////////////////////////////////////////////////////// + queryRequest.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName(table.getPrimaryKeyField()) + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(primaryKey)))); + + QueryAction queryAction = new QueryAction(); + QueryResult queryResult = queryAction.execute(queryRequest); + + /////////////////////////////////////////////////////// + // throw a not found error if the record isn't found // + /////////////////////////////////////////////////////// + if(queryResult.getRecords().isEmpty()) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + + context.result(JsonUtils.toJson(queryResult.getRecords().get(0))); + } + catch(Exception e) + { + handleException(context, e); + } } @@ -427,6 +472,29 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void processMetaData(Context context) + { + try + { + ProcessMetaDataRequest processMetaDataRequest = new ProcessMetaDataRequest(qInstance); + setupSession(context, processMetaDataRequest); + processMetaDataRequest.setProcessName(context.pathParam("process")); + ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction(); + ProcessMetaDataResult processMetaDataResult = processMetaDataAction.execute(processMetaDataRequest); + + context.result(JsonUtils.toJson(processMetaDataResult)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -435,9 +503,17 @@ public class QJavalinImplementation QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); if(userFacingException != null) { - LOG.info("User-facing exception", e); - context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) - .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + if(userFacingException instanceof QNotFoundException) + { + context.status(HttpStatus.NOT_FOUND_404) + .result("{\"error\":\"" + e.getMessage() + "\"}"); + } + else + { + LOG.info("User-facing exception", e); + context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) + .result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); + } } else { diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index f8376c5c..88011374 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -51,7 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; *******************************************************************************/ class QJavalinImplementationTest { - private static final int PORT = 6262; + private static final int PORT = 6262; private static final String BASE_URL = "http://localhost:" + PORT; @@ -61,7 +61,7 @@ class QJavalinImplementationTest ** *******************************************************************************/ @BeforeAll - public static void beforeAll() throws Exception + public static void beforeAll() { QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); qJavalinImplementation.startJavalinServer(PORT); @@ -111,7 +111,7 @@ class QJavalinImplementationTest @Test public void test_tableMetaData() { - HttpResponse response = Unirest.get(BASE_URL + "/metaData/person").asString(); + HttpResponse response = Unirest.get(BASE_URL + "/metaData/table/person").asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); @@ -136,9 +136,9 @@ class QJavalinImplementationTest @Test public void test_tableMetaData_notFound() { - HttpResponse response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").asString(); + HttpResponse response = Unirest.get(BASE_URL + "/metaData/table/notAnActualTable").asString(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); // todo 404? + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); String error = jsonObject.getString("error"); @@ -147,6 +147,104 @@ class QJavalinImplementationTest + /******************************************************************************* + ** test the process-level meta-data endpoint + ** + *******************************************************************************/ + @Test + public void test_processMetaData() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData/process/greetInteractive").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + JSONObject process = jsonObject.getJSONObject("process"); + assertEquals(4, process.keySet().size(), "Number of mid-level keys"); + assertEquals("greetInteractive", process.getString("name")); + assertEquals("Greet Interactive", process.getString("label")); + assertEquals("person", process.getString("tableName")); + + JSONArray frontendSteps = process.getJSONArray("frontendSteps"); + JSONObject setupStep = frontendSteps.getJSONObject(0); + assertEquals("Setup", setupStep.getString("label")); + JSONArray setupFields = setupStep.getJSONArray("formFields"); + assertEquals(2, setupFields.length()); + assertTrue(setupFields.toList().stream().anyMatch(field -> "greetingPrefix".equals(((Map) field).get("name")))); + } + + + + /******************************************************************************* + ** test the process-level meta-data endpoint for a non-real name + ** + *******************************************************************************/ + @Test + public void test_processMetaData_notFound() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData/process/notAnActualProcess").asString(); + + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + String error = jsonObject.getString("error"); + assertTrue(error.contains("not found")); + } + + + + /******************************************************************************* + ** test a table get (single record) + ** + *******************************************************************************/ + @Test + public void test_dataGet() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/1").asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("values")); + assertEquals("person", jsonObject.getString("tableName")); + JSONObject values = jsonObject.getJSONObject("values"); + assertTrue(values.has("firstName")); + assertTrue(values.has("id")); + } + + + + /******************************************************************************* + ** test a table get (single record) for an id that isn't found + ** + *******************************************************************************/ + @Test + public void test_dataGetNotFound() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/98765").asString(); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + String error = jsonObject.getString("error"); + assertEquals("Could not find Person with Id of 98765", error); + } + + + + /******************************************************************************* + ** test a table get (single record) for an id that isn't the expected type + ** + *******************************************************************************/ + @Test + public void test_dataGetWrongIdType() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/not-an-integer").asString(); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); + } + + + /******************************************************************************* ** test a table query ** @@ -178,8 +276,8 @@ class QJavalinImplementationTest @Test public void test_dataQueryWithFilter() { - String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; - HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); + String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; + HttpResponse response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); assertEquals(200, response.getStatus()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); @@ -226,6 +324,7 @@ class QJavalinImplementationTest } + /******************************************************************************* ** test an update ** @@ -293,7 +392,7 @@ class QJavalinImplementationTest ** *******************************************************************************/ @Test - public void test_processGreetInit() throws Exception + public void test_processGreetInit() { HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init") .header("Content-Type", "application/json") @@ -312,7 +411,7 @@ class QJavalinImplementationTest ** *******************************************************************************/ @Test - public void test_processGreetInitWithQueryValues() throws Exception + public void test_processGreetInitWithQueryValues() { HttpResponse response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude") .header("Content-Type", "application/json") diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index a25a5104..f6dcf37a 100644 --- a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; import java.util.List; -import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody; +import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; @@ -34,16 +34,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; -import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; import static junit.framework.Assert.assertNotNull; @@ -54,6 +53,9 @@ import static junit.framework.Assert.assertNotNull; *******************************************************************************/ public class TestUtils { + public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; + + /******************************************************************************* ** Prime a test database (e.g., h2, in-memory) @@ -101,6 +103,7 @@ public class TestUtils qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); qInstance.addProcess(defineProcessGreetPeople()); + qInstance.addProcess(defineProcessGreetPeopleInteractive()); return (qInstance); } @@ -167,10 +170,10 @@ public class TestUtils return new QProcessMetaData() .withName("greet") .withTableName("person") - .addFunction(new QFunctionMetaData() + .addStep(new QBackendStepMetaData() .withName("prepare") .withCode(new QCodeReference() - .withName(MockFunctionBody.class.getName()) + .withName(MockBackendStep.class.getName()) .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() @@ -185,9 +188,49 @@ public class TestUtils .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) - .withOutputView(new QOutputView() - .withMessageField("outputMessage") - .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) + ); + } + + + + /******************************************************************************* + ** Define an interactive version of the 'greet people' process + *******************************************************************************/ + private static QProcessMetaData defineProcessGreetPeopleInteractive() + { + return new QProcessMetaData() + .withName(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE) + .withTableName("person") + + .addStep(new QFrontendStepMetaData() + .withName("setup") + .withFormField(new QFieldMetaData("greetingPrefix", QFieldType.STRING)) + .withFormField(new QFieldMetaData("greetingSuffix", QFieldType.STRING)) + ) + + .addStep(new QBackendStepMetaData() + .withName("doWork") + .withCode(new QCodeReference() + .withName(MockBackendStep.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withFieldList(List.of( + new QFieldMetaData("greetingPrefix", QFieldType.STRING), + new QFieldMetaData("greetingSuffix", QFieldType.STRING) + ))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName("person") + .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) + ) + .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) + ) + + .addStep(new QFrontendStepMetaData() + .withName("results") + .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)) ); }