diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java index f76d261e..9ded0791 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java @@ -194,32 +194,57 @@ public class DMLAuditAction extends AbstractQActionFunction(table.getFields().values()); } - ////////////////////////////////////////// - // add fields for possible value labels // - ////////////////////////////////////////// List 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")); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 0bf8e33c..5ba0002b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -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; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java index 5449c942..1bb6e3ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java @@ -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 { + private static final QLogger LOG = QLogger.getLogger(RenderTemplateAction.class); + + /******************************************************************************* ** @@ -52,9 +62,12 @@ public class RenderTemplateAction extends AbstractQActionFunction + { + 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 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 context, String code) throws QException + public static String render(TemplateType templateType, Map context, String code) throws QException { RenderTemplateInput renderTemplateInput = new RenderTemplateInput(); renderTemplateInput.setCode(code); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 3356fbae..c58d78cc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -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 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 fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD); + Map 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 fileNameFormatFields = (List) adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS); + List 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 heavyFieldLengths = (Map) 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; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index c55dd441..587addbb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -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 editableFields = table.getFields().values().stream() .filter(QFieldMetaData::getIsEditable) + .filter(f -> !f.getType().equals(QFieldType.BLOB)) .toList(); QFrontendStepMetaData editScreen = new QFrontendStepMetaData() diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 138093cf..6a55e80a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -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 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 formatFieldNames = (List) 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?)"); + } + } + } + default -> + { + // no validations by default + } + } + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java index fac4b6c4..9a915141 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java @@ -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); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 71f0e669..6cf505c0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -73,7 +73,11 @@ public class QRecord implements Serializable private Map> 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 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index 0806955a..8bc11e70 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -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 // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java index 510308c0..a9999b25 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java @@ -165,6 +165,11 @@ public class QRecordEntityField { return (ValueUtils.getValueAsLocalTime(value)); } + + if(type.equals(byte[].class)) + { + return (ValueUtils.getValueAsByteArray(value)); + } } catch(Exception e) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java index 83eea005..f5530b3b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java @@ -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 // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java index e46f4c2c..ba78ceef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java @@ -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; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldAdornment.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldAdornment.java index 6bf68f48..2ca70af1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldAdornment.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldAdornment.java @@ -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 getValue(String key) + { + if(key != null && values != null) + { + return (Optional.ofNullable(values.get(key))); + } + + return (Optional.empty()); + } + + + /******************************************************************************* ** Setter for values ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index 298ffaf4..23f5b992 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -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 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 ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 367a4960..8971a846 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -85,6 +85,10 @@ public enum QFieldType { return (BOOLEAN); } + if(c.equals(byte[].class)) + { + return (BLOB); + } throw (new QException("Unrecognized class [" + c + "]")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index d4414208..abb79f9c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -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 ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java index 64d2580e..c1fe43f9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java @@ -70,6 +70,17 @@ public class AbstractProcessMetaDataBuilder + /******************************************************************************* + ** + *******************************************************************************/ + public AbstractProcessMetaDataBuilder withInputFieldDefaultValue(String fieldName, Serializable value) + { + setInputFieldDefaultValue(fieldName, value); + return (this); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java index eeaf05aa..87302d40 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java @@ -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); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java index 2bb90e28..ae5a12ed 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/columnstats/ColumnStatsStep.java @@ -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 // //////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractLoadStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractLoadStep.java index fa7b086a..835e79d7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractLoadStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractLoadStep.java @@ -47,6 +47,8 @@ public abstract class AbstractLoadStep implements BackendStep private Optional 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); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java index 8f38b8fd..c4d7ff89 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java @@ -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); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java index 2b61ead3..96933097 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java @@ -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 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); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java index 7b31440d..126735ef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java @@ -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))) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java index 98a41a4f..7c7afc3a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java @@ -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 Map loadTableToMap(AbstractActionInput parentActionInput, String tableName, String keyFieldName, Class entityClass) throws QException + { + return (loadTableToMap(tableName, keyFieldName, entityClass, null)); + } + + + + /******************************************************************************* + ** Note - null values from the key field are NOT put in the map. + *******************************************************************************/ + public static Map loadTableToMap(String tableName, String keyFieldName, Class entityClass, Consumer queryInputCustomizer) throws QException { QueryInput queryInput = new QueryInput(); queryInput.setTableName(tableName); + + if(queryInputCustomizer != null) + { + queryInputCustomizer.accept(queryInput); + } + QueryOutput queryOutput = new QueryAction().execute(queryInput); List records = queryOutput.getRecords(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java index a145a196..62393cf8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java @@ -26,11 +26,15 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; +import java.util.Map; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfInput; +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 org.junit.jupiter.api.Test; @@ -46,10 +50,10 @@ class ConvertHtmlToPdfActionTest extends BaseTest @Test void test() throws QException, IOException { - QInstance instance = QContext.getQInstance(); - ConvertHtmlToPdfInput input = new ConvertHtmlToPdfInput(); + QInstance instance = QContext.getQInstance(); - input.setHtml(""" + RenderTemplateInput renderTemplateInput = new RenderTemplateInput(); + renderTemplateInput.setCode("""