diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index 1efa034e..3462bf89 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.actions.dashboard; +import java.io.Serializable; import java.net.URLEncoder; import java.nio.charset.Charset; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -37,6 +40,7 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils; public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer { + /******************************************************************************* ** *******************************************************************************/ @@ -104,7 +108,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer /******************************************************************************* ** *******************************************************************************/ - protected String linkTableBulkLoad(RenderWidgetInput input, String tableName) throws QException + public static String linkTableBulkLoad(RenderWidgetInput input, String tableName) throws QException { String tablePath = input.getInstance().getTablePath(input, tableName); return (tablePath + "/" + tableName + ".bulkInsert"); @@ -115,10 +119,34 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer /******************************************************************************* ** *******************************************************************************/ - protected String linkTableFilter(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException + public static String linkTableFilter(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException { String tablePath = input.getInstance().getTablePath(input, tableName); return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset())); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException + { + String tablePath = input.getInstance().getTablePath(input, tableName); + return (tablePath + "/" + recordId + "/edit"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException + { + QProcessMetaData process = input.getInstance().getProcess(processName); + String tableName = process.getTableName(); + String tablePath = input.getInstance().getTablePath(input, tableName); + return (tablePath + "/" + recordId + "/" + processName); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java index d28e951e..cbca6067 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java @@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; @@ -55,6 +56,9 @@ public class InsertAction extends AbstractQActionFunction. + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.util.List; +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.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** Utility class to apply value behaviors to records. + *******************************************************************************/ +public class ValueBehaviorApplier +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public static void applyFieldBehaviors(QInstance instance, QTableMetaData table, List recordList) + { + for(QFieldMetaData field : table.getFields().values()) + { + String fieldName = field.getName(); + if(field.getType().equals(QFieldType.STRING) && field.getMaxLength() != null) + { + applyValueTooLongBehavior(instance, recordList, field, fieldName); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void applyValueTooLongBehavior(QInstance instance, List recordList, QFieldMetaData field, String fieldName) + { + ValueTooLongBehavior valueTooLongBehavior = field.getBehavior(instance, ValueTooLongBehavior.class); + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // don't process PASS_THROUGH - so we don't have to iterate over the whole record list to do noop // + //////////////////////////////////////////////////////////////////////////////////////////////////// + if(valueTooLongBehavior != null && !valueTooLongBehavior.equals(ValueTooLongBehavior.PASS_THROUGH)) + { + for(QRecord record : recordList) + { + String value = record.getValueString(fieldName); + if(value != null && value.length() > field.getMaxLength()) + { + switch(valueTooLongBehavior) + { + case TRUNCATE -> record.setValue(fieldName, StringUtils.safeTruncate(value, field.getMaxLength())); + case TRUNCATE_ELLIPSIS -> record.setValue(fieldName, StringUtils.safeTruncate(value, field.getMaxLength(), "...")); + case ERROR -> record.addError("The value for " + field.getLabel() + " is too long (max allowed length=" + field.getMaxLength() + ")"); + case PASS_THROUGH -> + { + } + default -> throw new IllegalStateException("Unexpected valueTooLongBehavior: " + valueTooLongBehavior); + } + } + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java index 1988bd6a..3cdec2a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java @@ -26,6 +26,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; /******************************************************************************* @@ -67,6 +68,16 @@ public @interface QField *******************************************************************************/ String possibleValueSourceName() default ""; + /******************************************************************************* + ** + *******************************************************************************/ + int maxLength() default Integer.MAX_VALUE; + + /******************************************************************************* + ** + *******************************************************************************/ + ValueTooLongBehavior valueTooLongBehavior() default ValueTooLongBehavior.PASS_THROUGH; + ////////////////////////////////////////////////////////////////////////////////////////// // new attributes here likely need implementation in QFieldMetaData.constructFromGetter // ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehavior.java new file mode 100644 index 00000000..db4a2b86 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehavior.java @@ -0,0 +1,31 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface FieldBehavior +{ + +} 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 9000614f..ca11da98 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 @@ -25,12 +25,16 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import com.github.hervian.reflection.Fun; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -56,6 +60,19 @@ public class QFieldMetaData implements Cloneable private Serializable defaultValue; private String possibleValueSourceName; + private Integer maxLength; + private Set behaviors; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // w/ longer-term vision: // + // - more enums that implement ValueTooLongBehavior e.g., NumberOutsideRangeBehavior or DecimalPrecisionErrorBehavior // + // - QInstance.Set defaultFieldBehaviors // + // - QBackendMetaData.Set defaultFieldBehaviors // + // - QTableMetaData.Set defaultFieldBehaviors // + // - inherit behaviors all the way down (up?) // + // - instance validation to make sure you don’t specify more than 1 behavior of a given type at a given level. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private List adornments; @@ -176,6 +193,16 @@ public class QFieldMetaData implements Cloneable { setPossibleValueSourceName(fieldAnnotation.possibleValueSourceName()); } + + if(fieldAnnotation.maxLength() != Integer.MAX_VALUE) + { + setMaxLength(fieldAnnotation.maxLength()); + } + + if(fieldAnnotation.valueTooLongBehavior() != ValueTooLongBehavior.PASS_THROUGH) + { + withBehavior(fieldAnnotation.valueTooLongBehavior()); + } } } catch(QException qe) @@ -532,4 +559,118 @@ public class QFieldMetaData implements Cloneable return (this); } + + + /******************************************************************************* + ** Getter for maxLength + ** + *******************************************************************************/ + public Integer getMaxLength() + { + return maxLength; + } + + + + /******************************************************************************* + ** Setter for maxLength + ** + *******************************************************************************/ + public void setMaxLength(Integer maxLength) + { + this.maxLength = maxLength; + } + + + + /******************************************************************************* + ** Fluent setter for maxLength + ** + *******************************************************************************/ + public QFieldMetaData withMaxLength(Integer maxLength) + { + this.maxLength = maxLength; + return (this); + } + + + + /******************************************************************************* + ** Getter for behaviors + ** + *******************************************************************************/ + public Set getBehaviors() + { + return behaviors; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public T getBehavior(QInstance instance, Class behaviorType) + { + for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(behaviors)) + { + if(behaviorType.isInstance(fieldBehavior)) + { + return (behaviorType.cast(fieldBehavior)); + } + } + + ///////////////////////////////////////////////////////////////////////// + // todo - cascade/inherit behaviors down from table, backend, instance // + ///////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////// + // return default behavior for this type // + /////////////////////////////////////////// + if(behaviorType.equals(ValueTooLongBehavior.class)) + { + return behaviorType.cast(ValueTooLongBehavior.getDefault()); + } + + return (null); + } + + + + /******************************************************************************* + ** Setter for behaviors + ** + *******************************************************************************/ + public void setBehaviors(Set behaviors) + { + this.behaviors = behaviors; + } + + + + /******************************************************************************* + ** Fluent setter for behaviors + ** + *******************************************************************************/ + public QFieldMetaData withBehaviors(Set behaviors) + { + this.behaviors = behaviors; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for behaviors + ** + *******************************************************************************/ + public QFieldMetaData withBehavior(FieldBehavior behavior) + { + if(behaviors == null) + { + behaviors = new HashSet<>(); + } + this.behaviors.add(behavior); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/ValueTooLongBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/ValueTooLongBehavior.java new file mode 100644 index 00000000..57b465be --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/ValueTooLongBehavior.java @@ -0,0 +1,44 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum ValueTooLongBehavior implements FieldBehavior +{ + TRUNCATE, + TRUNCATE_ELLIPSIS, + ERROR, + PASS_THROUGH; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static FieldBehavior getDefault() + { + return PASS_THROUGH; + } +} 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 18fb4a73..1ec145b1 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,12 +28,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +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.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; @@ -45,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -304,6 +308,41 @@ public class GeneralProcessUtils + /******************************************************************************* + ** Load all rows from a table, into a map, keyed by the keyFieldName - typed as + ** the specified keyType. + ** + ** Note - null values from the key field are NOT put in the map. + ** + ** If multiple values are found for the key, they'll squash each other, and only + ** one random value will appear. + ** + ** Also, note, this is inherently unsafe, if you were to call it on a table with + ** too many rows... Caveat emptor. + *******************************************************************************/ + public static Map loadTableToMap(AbstractActionInput parentActionInput, String tableName, Class keyType, String keyFieldName) throws QException + { + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(tableName); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + List records = queryOutput.getRecords(); + + Map map = new HashMap<>(); + for(QRecord record : records) + { + Serializable value = record.getValue(keyFieldName); + if(value != null) + { + T valueAsT = ValueUtils.getValueAsType(keyType, value); + map.put(valueAsT, record); + } + } + return (map); + } + + + /******************************************************************************* ** Note - null values from the key field are NOT put in the map. *******************************************************************************/ @@ -406,4 +445,19 @@ public class GeneralProcessUtils return (rs); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer count(AbstractActionInput input, String tableName, QQueryFilter filter) throws QException + { + CountInput countInput = new CountInput(input.getInstance()); + countInput.setSession(input.getSession()); + countInput.setTableName(tableName); + countInput.setFilter(filter); + CountOutput countOutput = new CountAction().execute(countInput); + return (countOutput.getCount()); + + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 4bff494f..52fb127c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -585,4 +585,51 @@ public class ValueUtils throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to ByteArray.")); } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static T getValueAsType(Class type, Serializable value) + { + if(type.equals(Integer.class)) + { + return (T) getValueAsInteger(value); + } + else if(type.equals(String.class)) + { + return (T) getValueAsString(value); + } + else if(type.equals(Boolean.class)) + { + return (T) getValueAsBoolean(value); + } + else if(type.equals(BigDecimal.class)) + { + return (T) getValueAsBigDecimal(value); + } + else if(type.equals(LocalDateTime.class)) + { + return (T) getValueAsLocalDateTime(value); + } + else if(type.equals(LocalDate.class)) + { + return (T) getValueAsLocalDate(value); + } + else if(type.equals(Instant.class)) + { + return (T) getValueAsInstant(value); + } + else if(type.equals(byte[].class)) + { + return (T) getValueAsByteArray(value); + } + else + { + throw new QValueException("Unsupported type [" + type.getSimpleName() + "] in getValueAsType."); + } + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java new file mode 100644 index 00000000..83841c18 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java @@ -0,0 +1,117 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.util.List; +import java.util.Optional; +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.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** Unit test for ValueBehaviorApplier + *******************************************************************************/ +class ValueBehaviorApplierTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValueTooLongNormalCases() + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.getField("firstName").withMaxLength(10).withBehavior(ValueTooLongBehavior.TRUNCATE); + table.getField("lastName").withMaxLength(10).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS); + table.getField("email").withMaxLength(20).withBehavior(ValueTooLongBehavior.ERROR); + + List recordList = List.of( + new QRecord().withValue("id", 1).withValue("firstName", "First name too long").withValue("lastName", "Smith").withValue("email", "john@smith.com"), + new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Last name too long").withValue("email", "john@smith.com"), + new QRecord().withValue("id", 3).withValue("firstName", "First name too long").withValue("lastName", "Smith").withValue("email", "john.smith@emaildomainwayytolongtofit.com") + ); + ValueBehaviorApplier.applyFieldBehaviors(qInstance, table, recordList); + + assertEquals("First name", getRecordById(recordList, 1).getValueString("firstName")); + assertEquals("Last na...", getRecordById(recordList, 2).getValueString("lastName")); + assertEquals("john.smith@emaildomainwayytolongtofit.com", getRecordById(recordList, 3).getValueString("email")); + assertFalse(getRecordById(recordList, 3).getErrors().isEmpty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValueTooLongEdgeCases() + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure PASS THROUGH actually does nothing, and that a maxLength w/ no behavior specified also does nothing (e.g., does PASS_THROUGH) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + table.getField("firstName").withMaxLength(10).withBehavior(ValueTooLongBehavior.PASS_THROUGH); + table.getField("lastName").withMaxLength(10); + + List recordList = List.of( + //////////////////////////////////////////////////////////////// + // make sure nulls and empty are okay, and don't get changed. // + //////////////////////////////////////////////////////////////// + new QRecord().withValue("id", 1).withValue("firstName", "First name too long").withValue("lastName", null).withValue("email", "john@smith.com"), + new QRecord().withValue("id", 2).withValue("firstName", "").withValue("lastName", "Last name too long").withValue("email", "john@smith.com") + ); + ValueBehaviorApplier.applyFieldBehaviors(qInstance, table, recordList); + + assertEquals("First name too long", getRecordById(recordList, 1).getValueString("firstName")); + assertNull(getRecordById(recordList, 1).getValueString("lastName")); + assertEquals("Last name too long", getRecordById(recordList, 2).getValueString("lastName")); + assertEquals("", getRecordById(recordList, 2).getValueString("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QRecord getRecordById(List recordList, Integer id) + { + Optional recordOpt = recordList.stream().filter(r -> r.getValueInteger("id").equals(id)).findFirst(); + if(recordOpt.isEmpty()) + { + fail("Didn't find record with id=" + id); + } + return (recordOpt.get()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java index 998fae6d..ae00eccf 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java @@ -22,8 +22,10 @@ package com.kingsrook.qqq.backend.core.utils; +import java.io.Serializable; import java.math.BigDecimal; import java.math.MathContext; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -34,6 +36,7 @@ import java.util.GregorianCalendar; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -231,6 +234,7 @@ class ValueUtilsTest } + /******************************************************************************* ** *******************************************************************************/ @@ -251,4 +255,20 @@ class ValueUtilsTest assertThat(assertThrows(QValueException.class, () -> ValueUtils.getValueAsInstant(new Object())).getMessage()).contains("Unsupported class"); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetValueAsType() + { + assertEquals(1, ValueUtils.getValueAsType(Integer.class, "1")); + assertEquals("1", ValueUtils.getValueAsType(String.class, 1)); + assertEquals(BigDecimal.ONE, ValueUtils.getValueAsType(BigDecimal.class, 1)); + assertEquals(true, ValueUtils.getValueAsType(Boolean.class, "true")); + assertArrayEquals("a".getBytes(StandardCharsets.UTF_8), ValueUtils.getValueAsType(byte[].class, "a")); + assertThrows(QValueException.class, () -> ValueUtils.getValueAsType(Serializable.class, 1)); + } + } \ No newline at end of file diff --git a/qqq-middleware-javalin/pom.xml b/qqq-middleware-javalin/pom.xml index efef7d1e..d225ee0f 100644 --- a/qqq-middleware-javalin/pom.xml +++ b/qqq-middleware-javalin/pom.xml @@ -60,18 +60,18 @@ io.javalin javalin - 4.6.1 + 5.1.4 com.konghq unirest-java - 3.4.00 + 3.13.12 test com.h2database h2 - 2.1.210 + 2.1.214 test diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index cbdae90b..9f2c9a7f 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -26,6 +26,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.time.LocalDate; import java.util.ArrayList; @@ -287,16 +288,19 @@ public class QJavalinProcessHandler //////////////////////////// for(UploadedFile uploadedFile : context.uploadedFiles()) { - QUploadedFile qUploadedFile = new QUploadedFile(); - qUploadedFile.setBytes(uploadedFile.getContent().readAllBytes()); - qUploadedFile.setFilename(uploadedFile.getFilename()); + try(InputStream content = uploadedFile.content()) + { + QUploadedFile qUploadedFile = new QUploadedFile(); + qUploadedFile.setBytes(content.readAllBytes()); + qUploadedFile.setFilename(uploadedFile.filename()); - UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); - TempFileStateProvider.getInstance().put(key, qUploadedFile); - LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key); - runProcessInput.addValue(QUploadedFile.DEFAULT_UPLOADED_FILE_FIELD_NAME, key); + UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); + TempFileStateProvider.getInstance().put(key, qUploadedFile); + LOG.info("Stored uploaded file in TempFileStateProvider under key: " + key); + runProcessInput.addValue(QUploadedFile.DEFAULT_UPLOADED_FILE_FIELD_NAME, key); - archiveUploadedFile(runProcessInput, qUploadedFile); + archiveUploadedFile(runProcessInput, qUploadedFile); + } } ///////////////////////////////////////////////////////////// diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index e88ec6c5..93f987e7 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -25,6 +25,7 @@ package com.kingsrook.sampleapp; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.Javalin; +import io.javalin.plugin.bundled.CorsPluginConfig; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -66,8 +67,8 @@ public class SampleJavalinServer QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance); javalinService = Javalin.create(config -> { - // todo - not all!! - config.enableCorsForAllOrigins(); + // todo - not all? + config.plugins.enableCors(cors -> cors.add(CorsPluginConfig::anyHost)); }).start(PORT); javalinService.routes(qJavalinImplementation.getRoutes()); @@ -88,8 +89,7 @@ public class SampleJavalinServer }); javalinService.before(QJavalinImplementation::hotSwapQInstance); - javalinService.after(ctx -> - ctx.res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000")); + javalinService.after(ctx -> ctx.res().setHeader("Access-Control-Allow-Origin", "http://localhost:3000")); } catch(Exception e) {