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

@ -194,32 +194,57 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
continue;
}
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
if(field.getType().equals(QFieldType.BLOB))
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
}
else
{
if(!Objects.equals(oldValue, value))
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
if(field.getType().equals(QFieldType.BLOB))
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
}
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
}
}
}
}

View File

@ -33,6 +33,7 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
@ -160,10 +161,12 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
@ -187,8 +190,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
if(record == null)
{
QTableMetaData table = input.getInstance().getTable(join.getLeftTable());
throw (new QNotFoundException("Could not find " + (table == null ? "" : table.getLabel()) + " with primary key " + id));
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
}
////////////////////////////////////////////////////////////////////
@ -209,6 +211,8 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
int totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
@ -222,11 +226,10 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
totalRows = new CountAction().execute(countInput).getCount();
}
QTableMetaData table = input.getInstance().getTable(join.getRightTable());
String tablePath = input.getInstance().getTablePath(table.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
String tablePath = input.getInstance().getTablePath(rightTable.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink, totalRows);
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{

View File

@ -386,13 +386,22 @@ public class ExportAction
fieldList = new ArrayList<>(table.getFields().values());
}
//////////////////////////////////////////
// add fields for possible value labels //
//////////////////////////////////////////
List<QFieldMetaData> returnList = new ArrayList<>();
for(QFieldMetaData field : fieldList)
{
/////////////////////////////////////////////////////////////////////////////////////////
// skip heavy fields. they aren't fetched, and we generally think we don't want them. //
/////////////////////////////////////////////////////////////////////////////////////////
if(field.getIsHeavy())
{
continue;
}
returnList.add(field);
//////////////////////////////////////////
// add fields for possible value labels //
//////////////////////////////////////////
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
returnList.add(new QFieldMetaData(field.getName() + ":possibleValueLabel", QFieldType.STRING).withLabel(field.getLabel() + " Name"));

View File

@ -81,6 +81,16 @@ public class QueryAction
{
ActionHelper.validateSession(queryInput);
if(queryInput.getTableName() == null)
{
throw (new QException("Table name was not specified in query input"));
}
if(queryInput.getTable() == null)
{
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
}
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;

View File

@ -26,20 +26,30 @@ import java.io.StringWriter;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.app.event.MethodExceptionEventHandler;
import org.apache.velocity.context.Context;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Basic action to render a template!
**
** hard-coded built to only assume Velocity right now. could expand (and refactor) in future.
*******************************************************************************/
public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplateInput, RenderTemplateOutput>
{
private static final QLogger LOG = QLogger.getLogger(RenderTemplateAction.class);
/*******************************************************************************
**
@ -52,9 +62,12 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
if(TemplateType.VELOCITY.equals(input.getTemplateType()))
{
Velocity.init();
Context context = new VelocityContext(input.getContext());
Context context = new VelocityContext(input.getContext());
setupEventHandlers(context);
StringWriter stringWriter = new StringWriter();
Velocity.evaluate(context, stringWriter, "logTag", input.getCode());
Velocity.evaluate(context, stringWriter, StringUtils.hasContent(input.getTemplateIdentifier()) ? input.getTemplateIdentifier() : "anonymous", input.getCode());
output.setResult(stringWriter.getBuffer().toString());
}
else
@ -67,12 +80,28 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
/*******************************************************************************
**
*******************************************************************************/
private static void setupEventHandlers(Context context)
{
EventCartridge eventCartridge = new EventCartridge();
eventCartridge.addEventHandler((MethodExceptionEventHandler) (ctx, aClass, method, exception, info) ->
{
LOG.info("Exception in velocity template", exception, logPair("at", info.toString()));
return (null);
});
eventCartridge.attachToContext(context);
}
/*******************************************************************************
** Most convenient static wrapper to render a Velocity template.
*******************************************************************************/
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
{
return (render(parentActionInput, TemplateType.VELOCITY, context, code));
return (render(TemplateType.VELOCITY, context, code));
}
@ -80,7 +109,7 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
/*******************************************************************************
** Convenient static wrapper to render a template of an arbitrary type (language).
*******************************************************************************/
public static String render(AbstractActionInput parentActionInput, TemplateType templateType, Map<String, Object> context, String code) throws QException
public static String render(TemplateType templateType, Map<String, Object> context, String code) throws QException
{
RenderTemplateInput renderTemplateInput = new RenderTemplateInput();
renderTemplateInput.setCode(code);

View File

@ -29,13 +29,18 @@ import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -434,4 +439,127 @@ public class QValueFormatter
}
}
/*******************************************************************************
** For any BLOB type fields in the list of records, change their value to
** the URL where they can be downloaded, and set their display value to a file name.
*******************************************************************************/
public static void setBlobValuesToDownloadUrls(QTableMetaData table, List<QRecord> records)
{
for(QFieldMetaData field : table.getFields().values())
{
if(field.getType().equals(QFieldType.BLOB))
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// file name comes from: //
// if there's a FILE_DOWNLOAD adornment, with a FILE_NAME_FIELD value, then the full filename comes from that field //
// - unless it was empty - then we do the "default thing": //
// else - the "default thing" is: //
// - tableLabel primaryKey fieldLabel //
// - and - if the FILE_DOWNLOAD adornment had a DEFAULT_EXTENSION, then it gets added (preceded by a dot) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Optional<FieldAdornment> fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
Map<String, Serializable> adornmentValues = Collections.emptyMap();
if(fileDownloadAdornment.isPresent())
{
adornmentValues = fileDownloadAdornment.get().getValues();
}
String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD));
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));
String defaultExtension = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.DEFAULT_EXTENSION));
for(QRecord record : records)
{
if(!doesFieldHaveValue(field, record))
{
continue;
}
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
String fileName = null;
//////////////////////////////////////////////////
// try to make file name from the fileNameField //
//////////////////////////////////////////////////
if(StringUtils.hasContent(fileNameField))
{
fileName = record.getValueString(fileNameField);
}
if(!StringUtils.hasContent(fileName))
{
if(StringUtils.hasContent(fileNameFormat))
{
@SuppressWarnings("unchecked") // instance validation should make this safe!
List<String> fileNameFormatFields = (List<String>) adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS);
List<String> values = fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList();
fileName = QValueFormatter.formatStringWithValues(fileNameFormat, values);
}
}
if(!StringUtils.hasContent(fileName))
{
//////////////////////////////////
// make default name if missing //
//////////////////////////////////
fileName = table.getLabel() + " " + primaryKey + " " + field.getLabel();
if(StringUtils.hasContent(defaultExtension))
{
//////////////////////////////////////////
// add default extension if we have one //
//////////////////////////////////////////
fileName += "." + defaultExtension;
}
}
record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName);
record.setDisplayValue(field.getName(), fileName);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean doesFieldHaveValue(QFieldMetaData field, QRecord record)
{
boolean fieldHasValue = false;
try
{
if(record.getValue(field.getName()) != null)
{
fieldHasValue = true;
}
else if(field.getIsHeavy())
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// heavy fields that weren't fetched - they should have a backend-detail specifying their length (or null if null) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> heavyFieldLengths = (Map<String, Serializable>) record.getBackendDetail(QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS);
if(heavyFieldLengths != null)
{
Integer fieldLength = ValueUtils.getValueAsInteger(heavyFieldLengths.get(field.getName()));
if(fieldLength != null && fieldLength > 0)
{
fieldHasValue = true;
}
}
}
}
catch(Exception e)
{
LOG.info("Error checking if field has value", e, logPair("fieldName", field.getName()), logPair("record", record));
}
return fieldHasValue;
}
}

View File

@ -715,7 +715,7 @@ public class QInstanceEnricher
try
{
QFieldMetaData field = table.getField(fieldName);
if(field.getIsEditable())
if(field.getIsEditable() && !field.getType().equals(QFieldType.BLOB))
{
editableFields.add(field);
}
@ -734,7 +734,7 @@ public class QInstanceEnricher
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
.withName("upload")
.withLabel("Upload File")
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withIsRequired(true))
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withLabel(table.getLabel() + " File").withIsRequired(true))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("previewText", "file upload instructions")
@ -773,6 +773,7 @@ public class QInstanceEnricher
List<QFieldMetaData> editableFields = table.getFields().values().stream()
.filter(QFieldMetaData::getIsEditable)
.filter(f -> !f.getType().equals(QFieldType.BLOB))
.toList();
QFrontendStepMetaData editScreen = new QFrontendStepMetaData()

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.instances;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@ -50,6 +51,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
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.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.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -80,6 +83,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -416,7 +420,7 @@ public class QInstanceValidator
{
table.getFields().forEach((fieldName, field) ->
{
validateTableField(qInstance, tableName, fieldName, field);
validateTableField(qInstance, tableName, fieldName, table, field);
});
}
@ -657,7 +661,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QFieldMetaData field)
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
{
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
@ -699,6 +703,55 @@ public class QInstanceValidator
assertCondition(fieldSecurityLock.getDefaultBehavior() != null, prefix + "has a fieldSecurityLock that is missing a defaultBehavior");
assertCondition(CollectionUtils.nullSafeHasContents(fieldSecurityLock.getOverrideValues()), prefix + "has a fieldSecurityLock that is missing overrideValues");
}
for(FieldAdornment adornment : CollectionUtils.nonNullList(field.getAdornments()))
{
Map<String, Serializable> adornmentValues = CollectionUtils.nonNullMap(adornment.getValues());
if(assertCondition(adornment.getType() != null, prefix + "has an adornment that is missing a type"))
{
String adornmentPrefix = prefix.trim() + ", " + adornment.getType() + " adornment ";
switch(adornment.getType())
{
case SIZE ->
{
String width = ValueUtils.getValueAsString(adornmentValues.get("width"));
if(assertCondition(StringUtils.hasContent(width), adornmentPrefix + "is missing a width value"))
{
assertNoException(() -> AdornmentType.Size.valueOf(width.toUpperCase()), adornmentPrefix + "has an unrecognized width value [" + width + "]");
}
}
case FILE_DOWNLOAD ->
{
String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD));
if(StringUtils.hasContent(fileNameField)) // file name isn't required - but if given, must be a field on the table.
{
assertNoException(() -> table.getField(fileNameField), adornmentPrefix + "specifies an unrecognized fileNameField [" + fileNameField + "]");
}
if(adornmentValues.containsKey(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS))
{
try
{
@SuppressWarnings("unchecked")
List<String> formatFieldNames = (List<String>) adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS);
for(String formatFieldName : CollectionUtils.nonNullList(formatFieldNames))
{
assertNoException(() -> table.getField(formatFieldName), adornmentPrefix + "specifies an unrecognized field name in fileNameFormatFields [" + formatFieldName + "]");
}
}
catch(Exception e)
{
errors.add(adornmentPrefix + "fileNameFormatFields could not be accessed (is it a List<String>?)");
}
}
}
default ->
{
// no validations by default
}
}
}
}
}

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -290,4 +291,25 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addAuditSingleInput(AuditSingleInput auditSingleInput)
{
if(getAuditInputList() == null)
{
setAuditInputList(new ArrayList<>());
}
if(getAuditInputList().isEmpty())
{
getAuditInputList().add(new AuditInput());
}
AuditInput auditInput = getAuditInputList().get(0);
auditInput.addAuditSingleInput(auditSingleInput);
}
}

View File

@ -73,7 +73,11 @@ public class QRecord implements Serializable
private Map<String, List<QRecord>> associatedRecords = new HashMap<>();
public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject";
////////////////////////////////////////////////
// well-known keys for the backendDetails map //
////////////////////////////////////////////////
public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject"; // String of JSON
public static final String BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS = "heavyFieldLengths"; // Map<fieldName, length>

View File

@ -295,7 +295,8 @@ public abstract class QRecordEntity
|| returnType.equals(BigDecimal.class)
|| returnType.equals(Instant.class)
|| returnType.equals(LocalDate.class)
|| returnType.equals(LocalTime.class));
|| returnType.equals(LocalTime.class)
|| returnType.equals(byte[].class));
/////////////////////////////////////////////
// note - this list has implications upon: //
// - QFieldType.fromClass //

View File

@ -165,6 +165,11 @@ public class QRecordEntityField
{
return (ValueUtils.getValueAsLocalTime(value));
}
if(type.equals(byte[].class))
{
return (ValueUtils.getValueAsByteArray(value));
}
}
catch(Exception e)
{

View File

@ -167,7 +167,8 @@ public interface QRecordEnum
|| returnType.equals(BigDecimal.class)
|| returnType.equals(Instant.class)
|| returnType.equals(LocalDate.class)
|| returnType.equals(LocalTime.class));
|| returnType.equals(LocalTime.class)
|| returnType.equals(byte[].class));
/////////////////////////////////////////////
// note - this list has implications upon: //
// - QFieldType.fromClass //

View File

@ -40,6 +40,7 @@ public enum AdornmentType
CODE_EDITOR,
RENDER_HTML,
REVEAL,
FILE_DOWNLOAD,
ERROR;
//////////////////////////////////////////////////////////////////////////
// keep these values in sync with AdornmentType.ts in qqq-frontend-core //
@ -58,6 +59,26 @@ public enum AdornmentType
/*******************************************************************************
**
*******************************************************************************/
public interface FileDownloadValues
{
String FILE_NAME_FIELD = "fileNameField";
String DEFAULT_EXTENSION = "defaultExtension";
String DEFAULT_MIME_TYPE = "defaultMimeType";
////////////////////////////////////////////////////
// use these two together, as in: //
// FILE_NAME_FORMAT = "Order %s Packing Slip.pdf" //
// FILE_NAME_FORMAT_FIELDS = "orderId" //
////////////////////////////////////////////////////
String FILE_NAME_FORMAT = "fileNameFormat";
String FILE_NAME_FORMAT_FIELDS = "fileNameFormatFields";
}
/*******************************************************************************
**
*******************************************************************************/
@ -112,6 +133,7 @@ public enum AdornmentType
XSMALL,
SMALL,
MEDIUM,
MEDLARGE,
LARGE,
XLARGE;

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.utils.Pair;
@ -116,6 +118,22 @@ public class FieldAdornment
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public Optional<Serializable> getValue(String key)
{
if(key != null && values != null)
{
return (Optional.ofNullable(values.get(key)));
}
return (Optional.empty());
}
/*******************************************************************************
** Setter for values
**

View File

@ -31,6 +31,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.github.hervian.reflection.Fun;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -534,6 +535,29 @@ public class QFieldMetaData implements Cloneable
/*******************************************************************************
** Getter for adornments
**
*******************************************************************************/
@JsonIgnore
public Optional<FieldAdornment> getAdornment(AdornmentType adornmentType)
{
if(adornmentType != null && adornments != null)
{
for(FieldAdornment adornment : adornments)
{
if(adornmentType.equals(adornment.getType()))
{
return Optional.of((adornment));
}
}
}
return (Optional.empty());
}
/*******************************************************************************
** Setter for adornments
**

View File

@ -85,6 +85,10 @@ public enum QFieldType
{
return (BOOLEAN);
}
if(c.equals(byte[].class))
{
return (BLOB);
}
throw (new QException("Unrecognized class [" + c + "]"));
}

View File

@ -43,6 +43,7 @@ public class QFrontendFieldMetaData
private QFieldType type;
private boolean isRequired;
private boolean isEditable;
private boolean isHeavy;
private String possibleValueSourceName;
private String displayFormat;
@ -64,6 +65,7 @@ public class QFrontendFieldMetaData
this.type = fieldMetaData.getType();
this.isRequired = fieldMetaData.getIsRequired();
this.isEditable = fieldMetaData.getIsEditable();
this.isHeavy = fieldMetaData.getIsHeavy();
this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName();
this.displayFormat = fieldMetaData.getDisplayFormat();
this.adornments = fieldMetaData.getAdornments();
@ -126,6 +128,17 @@ public class QFrontendFieldMetaData
/*******************************************************************************
** Getter for isHeavy
**
*******************************************************************************/
public boolean getIsHeavy()
{
return isHeavy;
}
/*******************************************************************************
** Getter for displayFormat
**

View File

@ -70,6 +70,17 @@ public class AbstractProcessMetaDataBuilder
/*******************************************************************************
**
*******************************************************************************/
public AbstractProcessMetaDataBuilder withInputFieldDefaultValue(String fieldName, Serializable value)
{
setInputFieldDefaultValue(fieldName, value);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
*******************************************************************************/
public class RenderTemplateInput extends AbstractActionInput
{
private String templateIdentifier;
private String code; // todo - TemplateReference, like CodeReference??
private TemplateType templateType;
@ -147,4 +148,35 @@ public class RenderTemplateInput extends AbstractActionInput
return (this);
}
/*******************************************************************************
** Getter for templateIdentifier
*******************************************************************************/
public String getTemplateIdentifier()
{
return (this.templateIdentifier);
}
/*******************************************************************************
** Setter for templateIdentifier
*******************************************************************************/
public void setTemplateIdentifier(String templateIdentifier)
{
this.templateIdentifier = templateIdentifier;
}
/*******************************************************************************
** Fluent setter for templateIdentifier
*******************************************************************************/
public RenderTemplateInput withTemplateIdentifier(String templateIdentifier)
{
this.templateIdentifier = templateIdentifier;
return (this);
}
}

View File

@ -140,6 +140,11 @@ public class ColumnStatsStep implements BackendStep
throw (new QException("Could not find field by name: " + fieldName));
}
if(field.getType().equals(QFieldType.BLOB))
{
throw (new QException("Column stats are not supported for this field's data type."));
}
////////////////////////////////////////////
// do a count query grouped by this field //
////////////////////////////////////////////

View File

@ -47,6 +47,8 @@ public abstract class AbstractLoadStep implements BackendStep
private Optional<QBackendTransaction> transaction = Optional.empty();
protected QSession session;
private AbstractTransformStep transformStep;
/*******************************************************************************
@ -122,4 +124,35 @@ public abstract class AbstractLoadStep implements BackendStep
return (null);
}
/*******************************************************************************
** Getter for transformStep
*******************************************************************************/
public AbstractTransformStep getTransformStep()
{
return (this.transformStep);
}
/*******************************************************************************
** Setter for transformStep
*******************************************************************************/
public void setTransformStep(AbstractTransformStep transformStep)
{
this.transformStep = transformStep;
}
/*******************************************************************************
** Fluent setter for transformStep
*******************************************************************************/
public AbstractLoadStep withTransformStep(AbstractTransformStep transformStep)
{
this.transformStep = transformStep;
return (this);
}
}

View File

@ -109,6 +109,11 @@ public class ExtractViaQueryStep extends AbstractExtractStep
queryInput.setRecordPipe(getRecordPipe());
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
if(runBackendStepInput.getValuePrimitiveBoolean(StreamedETLWithFrontendProcess.FIELD_FETCH_HEAVY_FIELDS))
{
queryInput.setShouldFetchHeavyFields(true);
}
customizeInputPreQuery(queryInput);
new QueryAction().execute(queryInput);

View File

@ -34,6 +34,8 @@ import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -73,6 +75,8 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
AbstractLoadStep loadStep = getLoadStep(runBackendStepInput);
loadStep.setTransformStep(transformStep);
/////////////////////////////////////////////////////////////////////////////
// let the load step override the capacity for the record pipe. //
// this is useful for slower load steps - so that the extract step doesn't //
@ -140,13 +144,17 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// get the process summary from the load step, if it's a summary-provider -- else, use the transform step (which is always a provider) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<ProcessSummaryLineInterface> processSummaryLines = null;
if(loadStep instanceof ProcessSummaryProviderInterface provider)
{
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, provider.doGetProcessSummary(runBackendStepOutput, true));
processSummaryLines = provider.doGetProcessSummary(runBackendStepOutput, true);
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, processSummaryLines);
}
else
if(CollectionUtils.nullSafeIsEmpty(processSummaryLines))
{
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true));
processSummaryLines = transformStep.doGetProcessSummary(runBackendStepOutput, true);
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, processSummaryLines);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -81,6 +81,7 @@ public class StreamedETLWithFrontendProcess
public static final String FIELD_DESTINATION_TABLE = "destinationTable"; // String
public static final String FIELD_RECORD_COUNT = "recordCount"; // Integer
public static final String FIELD_DEFAULT_QUERY_FILTER = "defaultQueryFilter"; // QQueryFilter or String (json, of q QQueryFilter)
public static final String FIELD_FETCH_HEAVY_FIELDS = "fetchHeavyFields"; // Boolean
public static final String FIELD_SUPPORTS_FULL_VALIDATION = "supportsFullValidation"; // Boolean
public static final String FIELD_DO_FULL_VALIDATION = "doFullValidation"; // Boolean
@ -142,6 +143,7 @@ public class StreamedETLWithFrontendProcess
.withCode(new QCodeReference(StreamedETLPreviewStep.class))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData(FIELD_SOURCE_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_SOURCE_TABLE)))
.withField(new QFieldMetaData(FIELD_FETCH_HEAVY_FIELDS, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_FETCH_HEAVY_FIELDS, false)))
.withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE)))
.withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true)))
.withField(new QFieldMetaData(FIELD_DO_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.get(FIELD_DO_FULL_VALIDATION)))

View File

@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -394,9 +395,25 @@ public class GeneralProcessUtils
** Note - null values from the key field are NOT put in the map.
*******************************************************************************/
public static <T extends QRecordEntity> Map<Serializable, T> loadTableToMap(AbstractActionInput parentActionInput, String tableName, String keyFieldName, Class<T> entityClass) throws QException
{
return (loadTableToMap(tableName, keyFieldName, entityClass, null));
}
/*******************************************************************************
** Note - null values from the key field are NOT put in the map.
*******************************************************************************/
public static <T extends QRecordEntity> Map<Serializable, T> loadTableToMap(String tableName, String keyFieldName, Class<T> entityClass, Consumer<QueryInput> queryInputCustomizer) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
if(queryInputCustomizer != null)
{
queryInputCustomizer.accept(queryInput);
}
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<QRecord> records = queryOutput.getRecords();