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))
);
}