Merge branch 'feature/CTLE-153-default-ct-live-packing-slips-to-deposco' into integration/sprint-26

# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
#	qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
#	qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
#	qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
#	qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
This commit is contained in:
2023-06-06 09:58:12 -05:00
47 changed files with 1145 additions and 114 deletions

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.javalin;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Serializable;
@ -53,6 +54,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
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.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -96,7 +98,10 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
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.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
@ -113,6 +118,7 @@ import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import io.javalin.Javalin;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import io.javalin.http.UploadedFile;
import org.apache.commons.io.FileUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
@ -356,6 +362,9 @@ public class QJavalinImplementation
put("", QJavalinImplementation::dataUpdate); // todo - want different semantics??
delete("", QJavalinImplementation::dataDelete);
get("/{fieldName}/{filename}", QJavalinImplementation::dataDownloadRecordField);
post("/{fieldName}/{filename}", QJavalinImplementation::dataDownloadRecordField);
QJavalinScriptsHandler.defineRecordRoutes();
});
});
@ -723,6 +732,53 @@ public class QJavalinImplementation
record.setValue(fieldName, null);
}
}
////////////////////////////
// process uploaded files //
////////////////////////////
for(Map.Entry<String, List<UploadedFile>> entry : CollectionUtils.nonNullMap(context.uploadedFileMap()).entrySet())
{
String fieldName = entry.getKey();
List<UploadedFile> uploadedFiles = entry.getValue();
if(uploadedFiles.size() > 0)
{
UploadedFile uploadedFile = uploadedFiles.get(0);
try(InputStream content = uploadedFile.content())
{
record.setValue(fieldName, content.readAllBytes());
}
QFieldMetaData blobField = tableMetaData.getField(fieldName);
blobField.getAdornment(AdornmentType.FILE_DOWNLOAD).ifPresent(adornment ->
{
adornment.getValue(AdornmentType.FileDownloadValues.FILE_NAME_FIELD).ifPresent(fileNameFieldName ->
{
record.setValue(ValueUtils.getValueAsString(fileNameFieldName), uploadedFile.filename());
});
});
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the record has any blob fields, and we're clearing them out (present in the values list, and set to null), //
// and they have a file-name field associated with them, then also clear out that file-name field //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QFieldMetaData field : tableMetaData.getFields().values())
{
if(field.getType().equals(QFieldType.BLOB))
{
field.getAdornment(AdornmentType.FILE_DOWNLOAD).ifPresent(adornment ->
{
adornment.getValue(AdornmentType.FileDownloadValues.FILE_NAME_FIELD).ifPresent(fileNameFieldName ->
{
if(record.getValues().containsKey(field.getName()) && record.getValue(field.getName()) == null)
{
record.setValue(ValueUtils.getValueAsString(fileNameFieldName), null);
}
});
});
}
}
}
@ -796,6 +852,7 @@ public class QJavalinImplementation
getInput.setTableName(tableName);
getInput.setShouldGenerateDisplayValues(true);
getInput.setShouldTranslatePossibleValues(true);
getInput.setShouldFetchHeavyFields(true);
PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ);
@ -807,6 +864,71 @@ public class QJavalinImplementation
GetAction getAction = new GetAction();
GetOutput getOutput = getAction.execute(getInput);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
QRecord record = getOutput.getRecord();
if(record == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
QJavalinAccessLogger.logEndSuccess();
context.result(JsonUtils.toJson(record));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void dataDownloadRecordField(Context context)
{
String tableName = context.pathParam("table");
String primaryKey = context.pathParam("primaryKey");
String fieldName = context.pathParam("fieldName");
String filename = context.pathParam("filename");
try
{
QTableMetaData table = qInstance.getTable(tableName);
GetInput getInput = new GetInput();
setupSession(context, getInput);
QJavalinAccessLogger.logStart("downloadRecordField", logPair("table", tableName), logPair("primaryKey", primaryKey), logPair("fieldName", fieldName));
////////////////////////////////////////////
// validate field name - 404 if not found //
////////////////////////////////////////////
QFieldMetaData fieldMetaData;
try
{
fieldMetaData = table.getField(fieldName);
}
catch(Exception e)
{
throw (new QNotFoundException("Could not find field named " + fieldName + " on table " + tableName));
}
getInput.setTableName(tableName);
getInput.setShouldFetchHeavyFields(true);
PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ);
getInput.setPrimaryKey(primaryKey);
GetAction getAction = new GetAction();
GetOutput getOutput = getAction.execute(getInput);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
@ -816,8 +938,27 @@ public class QJavalinImplementation
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
String mimeType = null;
Optional<FieldAdornment> fileDownloadAdornment = fieldMetaData.getAdornments().stream().filter(a -> a.getType().equals(AdornmentType.FILE_DOWNLOAD)).findFirst();
if(fileDownloadAdornment.isPresent())
{
Map<String, Serializable> values = fileDownloadAdornment.get().getValues();
mimeType = ValueUtils.getValueAsString(values.get(AdornmentType.FileDownloadValues.DEFAULT_MIME_TYPE));
}
if(mimeType != null)
{
context.contentType(mimeType);
}
if(context.queryParamMap().containsKey("download") || context.formParamMap().containsKey("download"))
{
context.header("Content-Disposition", "attachment; filename=" + filename);
}
context.result(getOutput.getRecord().getValueByteArray(fieldName));
QJavalinAccessLogger.logEndSuccess();
context.result(JsonUtils.toJson(getOutput.getRecord()));
}
catch(Exception e)
{
@ -951,6 +1092,8 @@ public class QJavalinImplementation
QueryAction queryAction = new QueryAction();
QueryOutput queryOutput = queryAction.execute(queryInput);
QValueFormatter.setBlobValuesToDownloadUrls(QContext.getQInstance().getTable(table), queryOutput.getRecords());
QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS), logPairIfSlow("joins", queryJoins, SLOW_LOG_THRESHOLD_MS));
context.result(JsonUtils.toJson(queryOutput));
}

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.javalin;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@ -185,6 +187,63 @@ class QJavalinImplementationTest extends QJavalinTestBase
JSONObject values = jsonObject.getJSONObject("values");
assertTrue(values.has("firstName"));
assertTrue(values.has("id"));
assertTrue(values.has("photo"));
JSONObject displayValues = jsonObject.getJSONObject("displayValues");
assertEquals("darin-photo.png", displayValues.getString("photo"));
////////////////////////////////////////////////////
// make sure person 2 doesn't have the blob value //
////////////////////////////////////////////////////
response = Unirest.get(BASE_URL + "/data/person/2").asString();
assertEquals(200, response.getStatus());
jsonObject = JsonUtils.toJSONObject(response.getBody());
values = jsonObject.getJSONObject("values");
assertFalse(values.has("photo"));
}
/*******************************************************************************
** test downloading a blob file
**
*******************************************************************************/
@Test
public void test_dataDownloadRecordField()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/1/photo/darin-photo.png").asString();
assertEquals(200, response.getStatus());
assertThat(response.getHeaders().get("content-type").get(0)).contains("image");
response = Unirest.get(BASE_URL + "/data/person/1/photo/darin-photo.png?download=1").asString();
assertEquals(200, response.getStatus());
assertThat(response.getHeaders().get("content-disposition").get(0))
.contains("attachment")
.contains("darin-photo.png");
/////////////////////////
// bad record id = 404 //
/////////////////////////
response = Unirest.get(BASE_URL + "/data/person/-1/photo/darin-photo.png").asString();
assertEquals(404, response.getStatus());
//////////////////////////
// bad field name = 404 //
//////////////////////////
response = Unirest.get(BASE_URL + "/data/person/1/notPhoto/darin-photo.png").asString();
assertEquals(404, response.getStatus());
/////////////////////////////
// missing file name = 404 //
/////////////////////////////
response = Unirest.get(BASE_URL + "/data/person/1/photo").asString();
assertEquals(404, response.getStatus());
//////////////////////////
// bad table name = 404 //
//////////////////////////
response = Unirest.get(BASE_URL + "/data/notPerson/1/photo/darin-photo.png").asString();
assertEquals(404, response.getStatus());
}
@ -431,29 +490,34 @@ class QJavalinImplementationTest extends QJavalinTestBase
**
*******************************************************************************/
@Test
public void test_dataInsertMultipartForm()
public void test_dataInsertMultipartForm() throws IOException
{
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/person")
.header("Content-Type", "application/json")
.multiPartContent()
.field("firstName", "Bobby")
.field("lastName", "Hull")
.field("email", "bobby@hull.com")
.asString();
try(InputStream photoInputStream = getClass().getResourceAsStream("/photo.png"))
{
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/person")
.header("Content-Type", "application/json")
.multiPartContent()
.field("firstName", "Bobby")
.field("lastName", "Hull")
.field("email", "bobby@hull.com")
.field("photo", photoInputStream.readAllBytes(), "image")
.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");
assertTrue(values0.has("firstName"));
assertEquals("Bobby", values0.getString("firstName"));
assertTrue(values0.has("id"));
assertEquals(7, values0.getInt("id"));
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");
assertTrue(values0.has("firstName"));
assertEquals("Bobby", values0.getString("firstName"));
assertTrue(values0.has("id"));
assertEquals(7, values0.getInt("id"));
assertTrue(values0.has("photo"));
}
}
@ -539,6 +603,44 @@ class QJavalinImplementationTest extends QJavalinTestBase
/*******************************************************************************
** test an update - posting the data as a multipart form
**
*******************************************************************************/
@Test
public void test_dataUpdateMultipartForm()
{
HttpResponse<String> response = Unirest.patch(BASE_URL + "/data/person/4")
.multiPartContent()
.field("firstName", "Free")
.field("birthDate", "")
.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"));
///////////////////////////////////////////////////////////////////
// re-GET the record, and validate that birthDate was nulled out //
///////////////////////////////////////////////////////////////////
response = Unirest.get(BASE_URL + "/data/person/4").asString();
assertEquals(200, response.getStatus());
jsonObject = JsonUtils.toJSONObject(response.getBody());
assertTrue(jsonObject.has("values"));
JSONObject values = jsonObject.getJSONObject("values");
assertFalse(values.has("birthDate"));
}
/*******************************************************************************
** test a delete
**
@ -718,4 +820,35 @@ class QJavalinImplementationTest extends QJavalinTestBase
assertEquals(5, jsonObject.getJSONArray("options").getJSONObject(1).getInt("id"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testServerInfo()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/serverInfo").asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertTrue(jsonObject.has("startTimeMillis"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthenticationMetaData()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/authentication").asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertTrue(jsonObject.has("name"));
assertTrue(jsonObject.has("type"));
}
}

View File

@ -43,6 +43,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticat
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.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.joins.JoinOn;
@ -228,7 +230,7 @@ public class TestUtils
*******************************************************************************/
public static QTableMetaData defineTablePerson()
{
return new QTableMetaData()
QTableMetaData qTableMetaData = new QTableMetaData()
.withName(TABLE_NAME_PERSON)
.withLabel("Person")
.withRecordLabelFormat("%s %s")
@ -244,10 +246,20 @@ public class TestUtils
.withField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withBackendName("partner_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
.withField(new QFieldMetaData("email", QFieldType.STRING))
.withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER).withBackendName("test_script_id"))
.withField(new QFieldMetaData("photo", QFieldType.BLOB).withBackendName("photo"))
.withField(new QFieldMetaData("photoFileName", QFieldType.STRING).withBackendName("photo_file_name"))
.withAssociatedScript(new AssociatedScript()
.withFieldName("testScriptId")
.withScriptTypeId(1)
.withScriptTester(new QCodeReference(TestScriptAction.class)));
qTableMetaData.getField("photo")
.withIsHeavy(true)
.withFieldAdornment(new FieldAdornment(AdornmentType.FILE_DOWNLOAD)
.withValue(AdornmentType.FileDownloadValues.DEFAULT_MIME_TYPE, "image")
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FIELD, "photoFileName"));
return (qTableMetaData);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

View File

@ -31,10 +31,12 @@ CREATE TABLE person
birth_date DATE,
email VARCHAR(250) NOT NULL,
partner_person_id INT,
test_script_id INT
test_script_id INT,
photo BLOB,
photo_file_name VARCHAR(50)
);
INSERT INTO person (id, first_name, last_name, birth_date, email, partner_person_id) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 6);
INSERT INTO person (id, first_name, last_name, birth_date, email, partner_person_id, photo, photo_file_name) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 6, '12345', 'darin-photo.png');
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');