QQQ-21 adding process metaData, single-record GET

This commit is contained in:
2022-07-06 13:55:50 -05:00
parent c8acda5919
commit de3eabb1cf
4 changed files with 244 additions and 26 deletions

View File

@ -51,7 +51,7 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.0.0</version> <version>0.1.0-20220706.184937-2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>

View File

@ -36,6 +36,7 @@ import java.util.concurrent.TimeoutException;
import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.InsertAction;
import com.kingsrook.qqq.backend.core.actions.MetaDataAction; 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.QueryAction;
import com.kingsrook.qqq.backend.core.actions.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; 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.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; 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.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest; 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.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.insert.InsertResult;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest; 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.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.TableMetaDataRequest;
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult; 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.RunProcessRequest;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; 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.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest; 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.query.QueryResult;
@ -162,10 +168,13 @@ public class QJavalinImplementation
path("/metaData", () -> path("/metaData", () ->
{ {
get("/", QJavalinImplementation::metaData); get("/", QJavalinImplementation::metaData);
path("/:table", () -> path("/table/:table", () ->
{ {
get("", QJavalinImplementation::tableMetaData); 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", () -> path("/data", () ->
@ -334,7 +343,43 @@ public class QJavalinImplementation
********************************************************************************/ ********************************************************************************/
private static void dataGet(Context context) 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); QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
if(userFacingException != null) if(userFacingException != null)
{ {
LOG.info("User-facing exception", e); if(userFacingException instanceof QNotFoundException)
context.status(HttpStatus.INTERNAL_SERVER_ERROR_500) {
.result("{\"error\":\"" + userFacingException.getMessage() + "\"}"); 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 else
{ {

View File

@ -51,7 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*******************************************************************************/ *******************************************************************************/
class QJavalinImplementationTest class QJavalinImplementationTest
{ {
private static final int PORT = 6262; private static final int PORT = 6262;
private static final String BASE_URL = "http://localhost:" + PORT; private static final String BASE_URL = "http://localhost:" + PORT;
@ -61,7 +61,7 @@ class QJavalinImplementationTest
** **
*******************************************************************************/ *******************************************************************************/
@BeforeAll @BeforeAll
public static void beforeAll() throws Exception public static void beforeAll()
{ {
QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance());
qJavalinImplementation.startJavalinServer(PORT); qJavalinImplementation.startJavalinServer(PORT);
@ -111,7 +111,7 @@ class QJavalinImplementationTest
@Test @Test
public void test_tableMetaData() public void test_tableMetaData()
{ {
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/person").asString(); HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/table/person").asString();
assertEquals(200, response.getStatus()); assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
@ -136,9 +136,9 @@ class QJavalinImplementationTest
@Test @Test
public void test_tableMetaData_notFound() public void test_tableMetaData_notFound()
{ {
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").asString(); HttpResponse<String> 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()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys"); assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
String error = jsonObject.getString("error"); String error = jsonObject.getString("error");
@ -147,6 +147,104 @@ class QJavalinImplementationTest
/*******************************************************************************
** test the process-level meta-data endpoint
**
*******************************************************************************/
@Test
public void test_processMetaData()
{
HttpResponse<String> 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<String> 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<String> 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<String> 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<String> 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 ** test a table query
** **
@ -178,8 +276,8 @@ class QJavalinImplementationTest
@Test @Test
public void test_dataQueryWithFilter() public void test_dataQueryWithFilter()
{ {
String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}"; String filterJson = "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"EQUALS\",\"values\":[\"Tim\"]}]}";
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString(); HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person?filter=" + URLEncoder.encode(filterJson, StandardCharsets.UTF_8)).asString();
assertEquals(200, response.getStatus()); assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
@ -226,6 +324,7 @@ class QJavalinImplementationTest
} }
/******************************************************************************* /*******************************************************************************
** test an update ** test an update
** **
@ -293,7 +392,7 @@ class QJavalinImplementationTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processGreetInit() throws Exception public void test_processGreetInit()
{ {
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init") HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
@ -312,7 +411,7 @@ class QJavalinImplementationTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processGreetInitWithQueryValues() throws Exception public void test_processGreetInitWithQueryValues()
{ {
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude") HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")

View File

@ -25,7 +25,7 @@ package com.kingsrook.qqq.backend.javalin;
import java.io.InputStream; import java.io.InputStream;
import java.sql.Connection; import java.sql.Connection;
import java.util.List; 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.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; 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.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.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; 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.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.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.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.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; 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.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; 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 org.apache.commons.io.IOUtils;
import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNotNull;
@ -54,6 +53,9 @@ import static junit.framework.Assert.assertNotNull;
*******************************************************************************/ *******************************************************************************/
public class TestUtils public class TestUtils
{ {
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
/******************************************************************************* /*******************************************************************************
** Prime a test database (e.g., h2, in-memory) ** Prime a test database (e.g., h2, in-memory)
@ -101,6 +103,7 @@ public class TestUtils
qInstance.addBackend(defineBackend()); qInstance.addBackend(defineBackend());
qInstance.addTable(defineTablePerson()); qInstance.addTable(defineTablePerson());
qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeople());
qInstance.addProcess(defineProcessGreetPeopleInteractive());
return (qInstance); return (qInstance);
} }
@ -167,10 +170,10 @@ public class TestUtils
return new QProcessMetaData() return new QProcessMetaData()
.withName("greet") .withName("greet")
.withTableName("person") .withTableName("person")
.addFunction(new QFunctionMetaData() .addStep(new QBackendStepMetaData()
.withName("prepare") .withName("prepare")
.withCode(new QCodeReference() .withCode(new QCodeReference()
.withName(MockFunctionBody.class.getName()) .withName(MockBackendStep.class.getName())
.withCodeType(QCodeType.JAVA) .withCodeType(QCodeType.JAVA)
.withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context?
.withInputData(new QFunctionInputMetaData() .withInputData(new QFunctionInputMetaData()
@ -185,9 +188,49 @@ public class TestUtils
.addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
) )
.withFieldList(List.of(new QFieldMetaData("outputMessage", 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))
); );
} }