From 1baade0449c2f1c5ba1425c074f0d12121bda8a9 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 18 Jan 2024 11:50:40 -0600 Subject: [PATCH] Change insert & update actions to set default values for createDate & modifyDate based on FieldBehaviors instead of based on field names (though field names are used in Enricher to add those beavhiors); Some refactoring of FieldBehaviors. --- .../core/actions/tables/InsertAction.java | 37 +--- .../core/actions/tables/UpdateAction.java | 2 +- .../UpdateActionRecordSplitHelper.java | 29 ---- .../actions/values/ValueBehaviorApplier.java | 49 ++---- .../core/instances/QInstanceEnricher.java | 85 ++++++++- .../core/instances/QInstanceValidator.java | 2 +- .../fields/DynamicDefaultValueBehavior.java | 164 ++++++++++++++++++ .../model/metadata/fields/FieldBehavior.java | 33 +++- .../model/metadata/fields/QFieldMetaData.java | 69 ++++++-- .../metadata/fields/ValueTooLongBehavior.java | 68 +++++++- .../UpdateActionRecordSplitHelperTest.java | 9 +- .../values/ValueBehaviorApplierTest.java | 8 +- .../core/instances/QInstanceEnricherTest.java | 36 ++++ .../DynamicDefaultValueBehaviorTest.java | 139 +++++++++++++++ .../metadata/fields/QFieldMetaDataTest.java | 71 ++++++++ 15 files changed, 670 insertions(+), 131 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehavior.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehaviorTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaDataTest.java 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 794c9f57..bdc53b8d 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 @@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; -import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -207,17 +206,6 @@ public class InsertAction extends AbstractQActionFunction updatableFields = table.getFields().values().stream() .map(QFieldMetaData::getName) // todo - intent here is to avoid non-updateable fields - but this @@ -147,29 +141,6 @@ public class UpdateActionRecordSplitHelper - /******************************************************************************* - ** If the table has a field with the given name, then set the given value in the - ** given record. - *******************************************************************************/ - protected void setValueIfTableHasField(QRecord record, QTableMetaData table, String fieldName, Serializable value) - { - try - { - if(table.getFields().containsKey(fieldName)) - { - record.setValue(fieldName, value); - } - } - catch(Exception e) - { - ///////////////////////////////////////////////// - // this means field doesn't exist, so, ignore. // - ///////////////////////////////////////////////// - } - } - - - /******************************************************************************* ** Getter for haveAnyWithoutErrors ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java index 4e89af6e..8e92e786 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java @@ -25,12 +25,10 @@ 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.FieldBehavior; 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.model.statusmessages.BadInputStatusMessage; -import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -42,16 +40,10 @@ public class ValueBehaviorApplier /******************************************************************************* ** *******************************************************************************/ - public static void applyFieldBehaviors(QInstance instance, QTableMetaData table, List recordList) + public enum Action { - for(QFieldMetaData field : table.getFields().values()) - { - String fieldName = field.getName(); - if(field.getType().equals(QFieldType.STRING) && field.getMaxLength() != null) - { - applyValueTooLongBehavior(instance, recordList, field, fieldName); - } - } + INSERT, + UPDATE } @@ -59,31 +51,18 @@ public class ValueBehaviorApplier /******************************************************************************* ** *******************************************************************************/ - private static void applyValueTooLongBehavior(QInstance instance, List recordList, QFieldMetaData field, String fieldName) + public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List recordList) { - 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)) + if(CollectionUtils.nullSafeIsEmpty(recordList)) { - for(QRecord record : recordList) + return; + } + + for(QFieldMetaData field : table.getFields().values()) + { + for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors())) { - 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(new BadInputStatusMessage("The value for " + field.getLabel() + " is too long (max allowed length=" + field.getMaxLength() + ")")); - case PASS_THROUGH -> - { - } - default -> throw new IllegalStateException("Unexpected valueTooLongBehavior: " + valueTooLongBehavior); - } - } + fieldBehavior.apply(action, recordList, instance, table, field); } } } 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 f3afb6af..232ade6c 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 @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior; 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; @@ -94,10 +95,8 @@ public class QInstanceEnricher private JoinGraph joinGraph; - ////////////////////////////////////////////////////////// - // todo - come up w/ a way for app devs to set configs! // - ////////////////////////////////////////////////////////// - private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true; + private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true; + private boolean configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate = true; ////////////////////////////////////////////////////////////////////////////////////////////////// // let an instance define mappings to be applied during name-to-label enrichments, // @@ -464,6 +463,22 @@ public class QInstanceEnricher } } } + + ///////////////////////////////////////////////////////////////////////// + // add field behaviors for create date & modify date, if so configured // + ///////////////////////////////////////////////////////////////////////// + if(configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate) + { + if("createDate".equals(field.getName()) && field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class) == null) + { + field.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE); + } + + if("modifyDate".equals(field.getName()) && field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class) == null) + { + field.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE); + } + } } @@ -1220,4 +1235,66 @@ public class QInstanceEnricher labelMappings.clear(); } + + + /******************************************************************************* + ** Getter for configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels + *******************************************************************************/ + public boolean getConfigRemoveIdFromNameWhenCreatingPossibleValueFieldLabels() + { + return (this.configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels); + } + + + + /******************************************************************************* + ** Setter for configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels + *******************************************************************************/ + public void setConfigRemoveIdFromNameWhenCreatingPossibleValueFieldLabels(boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels) + { + this.configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels; + } + + + + /******************************************************************************* + ** Fluent setter for configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels + *******************************************************************************/ + public QInstanceEnricher withConfigRemoveIdFromNameWhenCreatingPossibleValueFieldLabels(boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels) + { + this.configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels; + return (this); + } + + + + /******************************************************************************* + ** Getter for configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate + *******************************************************************************/ + public boolean getConfigAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate() + { + return (this.configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate); + } + + + + /******************************************************************************* + ** Setter for configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate + *******************************************************************************/ + public void setConfigAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate(boolean configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate) + { + this.configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate = configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate + *******************************************************************************/ + public QInstanceEnricher withConfigAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate(boolean configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate) + { + this.configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate = configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate; + return (this); + } + } 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 725b9c4d..070d08e4 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 @@ -691,7 +691,7 @@ public class QInstanceValidator String prefix = "Field " + fieldName + " in table " + tableName + " "; - ValueTooLongBehavior behavior = field.getBehavior(qInstance, ValueTooLongBehavior.class); + ValueTooLongBehavior behavior = field.getBehaviorOrDefault(qInstance, ValueTooLongBehavior.class); if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH)) { assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength."); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehavior.java new file mode 100644 index 00000000..f1243776 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehavior.java @@ -0,0 +1,164 @@ +/* + * 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; + + +import java.io.Serializable; +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +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.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Field behavior that sets a default value for a field dynamically. + ** e.g., create-date fields get set to 'now' on insert. + ** e.g., modify-date fields get set to 'now' on insert and on update. + *******************************************************************************/ +public enum DynamicDefaultValueBehavior implements FieldBehavior +{ + CREATE_DATE, + MODIFY_DATE, + NONE; + + private static final QLogger LOG = QLogger.getLogger(ValueTooLongBehavior.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public DynamicDefaultValueBehavior getDefault() + { + return (NONE); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + if(this.equals(NONE)) + { + return; + } + + switch(this) + { + case CREATE_DATE -> applyCreateDate(action, recordList, table, field); + case MODIFY_DATE -> applyModifyDate(action, recordList, table, field); + default -> throw new IllegalStateException("Unexpected enum value: " + this); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void applyCreateDate(ValueBehaviorApplier.Action action, List recordList, QTableMetaData table, QFieldMetaData field) + { + if(!ValueBehaviorApplier.Action.INSERT.equals(action)) + { + return; + } + + setCreateDateOrModifyDateOnList(recordList, table, field); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void applyModifyDate(ValueBehaviorApplier.Action action, List recordList, QTableMetaData table, QFieldMetaData field) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // check both of these (even though they're the only 2 values at the time of this writing), just in case more enum values are added in the future // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(!ValueBehaviorApplier.Action.INSERT.equals(action) && !ValueBehaviorApplier.Action.UPDATE.equals(action)) + { + return; + } + + setCreateDateOrModifyDateOnList(recordList, table, field); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void setCreateDateOrModifyDateOnList(List recordList, QTableMetaData table, QFieldMetaData field) + { + String fieldName = field.getName(); + Serializable value = getNow(table, field); + + for(QRecord record : CollectionUtils.nonNullList(recordList)) + { + record.setValue(fieldName, value); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Serializable getNow(QTableMetaData table, QFieldMetaData field) + { + if(QFieldType.DATE_TIME.equals(field.getType())) + { + return (Instant.now()); + } + else if(QFieldType.DATE.equals(field.getType())) + { + return (LocalDate.now()); + } + else + { + LOG.debug("Request to apply a " + this.name() + " DynamicDefaultValueBehavior to a non-date or date-time field", logPair("table", table.getName()), logPair("field", field.getName())); + return (null); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void noop() + { + + } + +} 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 index db4a2b86..0169dd3a 100644 --- 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 @@ -22,10 +22,41 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +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.tables.QTableMetaData; + + /******************************************************************************* + ** Interface for (expected to be?) enums which define behaviors that get applied + ** to fields. + ** + ** At the present, these behaviors get applied before a field is stored (insert + ** or update), through the ValueBehaviorApplier class. ** *******************************************************************************/ -public interface FieldBehavior +public interface FieldBehavior> { + /******************************************************************************* + ** In case a behavior of this type wasn't set on the field, what should the + ** default of this type be? + *******************************************************************************/ + T getDefault(); + + /******************************************************************************* + ** Apply this behavior to a list of records + *******************************************************************************/ + void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field); + + /******************************************************************************* + ** control if multiple behaviors of this type should be allowed together on a field. + *******************************************************************************/ + default boolean allowMultipleBehaviorsOfThisType() + { + return (false); + } + } 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 6bbdb6cd..8e4a5c5e 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 @@ -35,6 +35,7 @@ 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.instances.QInstanceHelpContentManager; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; @@ -44,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -52,6 +54,8 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class QFieldMetaData implements Cloneable { + private static final QLogger LOG = QLogger.getLogger(QFieldMetaData.class); + private String name; private String label; private String backendName; @@ -73,8 +77,8 @@ public class QFieldMetaData implements Cloneable private String possibleValueSourceName; private QQueryFilter possibleValueSourceFilter; - private Integer maxLength; - private Set behaviors; + private Integer maxLength; + private Set> behaviors; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // w/ longer-term vision for FieldBehaviors // @@ -674,7 +678,7 @@ public class QFieldMetaData implements Cloneable ** Getter for behaviors ** *******************************************************************************/ - public Set getBehaviors() + public Set> getBehaviors() { return behaviors; } @@ -682,11 +686,12 @@ public class QFieldMetaData implements Cloneable /******************************************************************************* - ** + ** Get the FieldBehavior object of a given behaviorType (class) - but - if one + ** isn't set, then use the default from that type. *******************************************************************************/ - public T getBehavior(QInstance instance, Class behaviorType) + public > T getBehaviorOrDefault(QInstance instance, Class behaviorType) { - for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(behaviors)) + for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(behaviors)) { if(behaviorType.isInstance(fieldBehavior)) { @@ -701,9 +706,33 @@ public class QFieldMetaData implements Cloneable /////////////////////////////////////////// // return default behavior for this type // /////////////////////////////////////////// - if(behaviorType.equals(ValueTooLongBehavior.class)) + if(behaviorType.isEnum()) { - return behaviorType.cast(ValueTooLongBehavior.getDefault()); + return (behaviorType.getEnumConstants()[0].getDefault()); + } + + return (null); + } + + + + /******************************************************************************* + ** Get the FieldBehavior object of a given behaviorType (class) - and if one + ** isn't set, then return null. + *******************************************************************************/ + public > T getBehaviorOnlyIfSet(Class behaviorType) + { + if(behaviors == null) + { + return (null); + } + + for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(behaviors)) + { + if(behaviorType.isInstance(fieldBehavior)) + { + return (behaviorType.cast(fieldBehavior)); + } } return (null); @@ -715,7 +744,7 @@ public class QFieldMetaData implements Cloneable ** Setter for behaviors ** *******************************************************************************/ - public void setBehaviors(Set behaviors) + public void setBehaviors(Set> behaviors) { this.behaviors = behaviors; } @@ -726,7 +755,7 @@ public class QFieldMetaData implements Cloneable ** Fluent setter for behaviors ** *******************************************************************************/ - public QFieldMetaData withBehaviors(Set behaviors) + public QFieldMetaData withBehaviors(Set> behaviors) { this.behaviors = behaviors; return (this); @@ -738,12 +767,30 @@ public class QFieldMetaData implements Cloneable ** Fluent setter for behaviors ** *******************************************************************************/ - public QFieldMetaData withBehavior(FieldBehavior behavior) + public QFieldMetaData withBehavior(FieldBehavior behavior) { + if(behavior == null) + { + LOG.debug("Skipping request to add null behavior", logPair("fieldName", getName())); + return (this); + } + if(behaviors == null) { behaviors = new HashSet<>(); } + + if(!behavior.allowMultipleBehaviorsOfThisType()) + { + @SuppressWarnings("unchecked") + FieldBehavior existingBehaviorOfThisType = getBehaviorOnlyIfSet(behavior.getClass()); + if(existingBehaviorOfThisType != null) + { + LOG.debug("Replacing a field behavior", logPair("fieldName", getName()), logPair("oldBehavior", existingBehaviorOfThisType), logPair("newBehavior", behavior)); + this.behaviors.remove(existingBehaviorOfThisType); + } + } + 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 index 57b465be..5d091ba1 100644 --- 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 @@ -22,23 +22,85 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +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.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + /******************************************************************************* + ** Behaviors for string fields, if their value is too long. ** + ** Note: This was the first implementation of a FieldBehavior, so its test + ** coverage is provided in ValueBehaviorApplierTest. *******************************************************************************/ -public enum ValueTooLongBehavior implements FieldBehavior +public enum ValueTooLongBehavior implements FieldBehavior { TRUNCATE, TRUNCATE_ELLIPSIS, ERROR, PASS_THROUGH; + private static final QLogger LOG = QLogger.getLogger(ValueTooLongBehavior.class); + /******************************************************************************* ** *******************************************************************************/ - public static FieldBehavior getDefault() + @Override + public ValueTooLongBehavior getDefault() { - return PASS_THROUGH; + return (PASS_THROUGH); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + if(this.equals(PASS_THROUGH)) + { + return; + } + + String fieldName = field.getName(); + if(!QFieldType.STRING.equals(field.getType())) + { + LOG.debug("Request to apply a ValueTooLongBehavior to a non-string field", logPair("table", table.getName()), logPair("field", fieldName)); + return; + } + + if(field.getMaxLength() == null) + { + LOG.debug("Request to apply a ValueTooLongBehavior to string field without a maxLength", logPair("table", table.getName()), logPair("field", fieldName)); + return; + } + + for(QRecord record : recordList) + { + String value = record.getValueString(fieldName); + if(value != null && value.length() > field.getMaxLength()) + { + switch(this) + { + 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(new BadInputStatusMessage("The value for " + field.getLabel() + " is too long (max allowed length=" + field.getMaxLength() + ")")); + /////////////////////////////////// + // PASS_THROUGH is handled above // + /////////////////////////////////// + default -> throw new IllegalStateException("Unexpected enum value: " + this); + } + } + } } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java index 7d3a55d0..f7632dfa 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java @@ -38,7 +38,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.utils.ListingHash; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -64,6 +63,7 @@ class UpdateActionRecordSplitHelperTest extends BaseTest .withField(new QFieldMetaData("B", QFieldType.INTEGER)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))); + Instant now = Instant.now(); UpdateInput updateInput = new UpdateInput(tableName) .withRecord(new QRecord().withValue("id", 1).withValue("A", 1)) .withRecord(new QRecord().withValue("id", 2).withValue("A", 2)) @@ -71,6 +71,7 @@ class UpdateActionRecordSplitHelperTest extends BaseTest .withRecord(new QRecord().withValue("id", 4).withValue("B", 3)) .withRecord(new QRecord().withValue("id", 5).withValue("B", 3)) .withRecord(new QRecord().withValue("id", 6).withValue("A", 4).withValue("B", 5)); + updateInput.getRecords().forEach(r -> r.setValue("modifyDate", now)); UpdateActionRecordSplitHelper updateActionRecordSplitHelper = new UpdateActionRecordSplitHelper(); updateActionRecordSplitHelper.init(updateInput); ListingHash, QRecord> recordsByFieldBeingUpdated = updateActionRecordSplitHelper.getRecordsByFieldBeingUpdated(); @@ -78,12 +79,6 @@ class UpdateActionRecordSplitHelperTest extends BaseTest Function, Set> extractIds = (records) -> records.stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet()); - //////////////////////////////////////// - // validate that modify dates got set // - //////////////////////////////////////// - updateInput.getRecords().forEach(r -> - assertThat(r.getValue("modifyDate")).isInstanceOf(Instant.class)); - ////////////////////////////////////////////////////////////// // validate the grouping of records by fields-being-updated // ////////////////////////////////////////////////////////////// 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 index bb5f8b13..60589039 100644 --- 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 @@ -39,7 +39,9 @@ import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* - ** Unit test for ValueBehaviorApplier + ** Unit test for ValueBehaviorApplier - and also providing coverage for + ** ValueTooLongBehavior (the first implementation, which was previously in the + ** class under test). *******************************************************************************/ class ValueBehaviorApplierTest extends BaseTest { @@ -61,7 +63,7 @@ class ValueBehaviorApplierTest extends BaseTest 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); + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList); assertEquals("First name", getRecordById(recordList, 1).getValueString("firstName")); assertEquals("Last na...", getRecordById(recordList, 2).getValueString("lastName")); @@ -93,7 +95,7 @@ class ValueBehaviorApplierTest extends BaseTest 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); + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList); assertEquals("First name too long", getRecordById(recordList, 1).getValueString("firstName")); assertNull(getRecordById(recordList, 1).getValueString("lastName")); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index 22e0da84..53e9ec96 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -29,6 +29,7 @@ import java.util.Optional; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior; 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; @@ -493,4 +494,39 @@ class QInstanceEnricherTest extends BaseTest return (tableMetaData); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCreateDateAndModifyDateBehaviors() + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.addTable(newTable("A", "id", "createDate", "modifyDate")); + QTableMetaData table = qInstance.getTable("A"); + + //////////////////////////////////////////////// + // make sure behavior wasn't there by default // + //////////////////////////////////////////////// + assertNull(table.getField("createDate").getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + assertNull(table.getField("modifyDate").getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + + ////////////////////////////////////////////////////////////////// + // make sure if config'ing off the adding of the behavior works // + ////////////////////////////////////////////////////////////////// + new QInstanceEnricher(qInstance) + .withConfigAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate(false) + .enrich(); + assertNull(table.getField("createDate").getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + assertNull(table.getField("modifyDate").getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + + ///////////////////////////////////////////////////////////////////////////////////////////// + // make sure default value for the config (e.g., in a new enricher) is to add the behavior // + ///////////////////////////////////////////////////////////////////////////////////////////// + new QInstanceEnricher(qInstance).enrich(); + assertEquals(DynamicDefaultValueBehavior.CREATE_DATE, table.getField("createDate").getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + assertEquals(DynamicDefaultValueBehavior.MODIFY_DATE, table.getField("modifyDate").getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehaviorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehaviorTest.java new file mode 100644 index 00000000..d93057a6 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DynamicDefaultValueBehaviorTest.java @@ -0,0 +1,139 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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; + + +import java.time.LocalDate; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.context.QContext; +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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for DynamicDefaultValueBehavior + *******************************************************************************/ +class DynamicDefaultValueBehaviorTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCreateDateHappyPath() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + + QRecord record = new QRecord().withValue("id", 1); + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); + + assertNotNull(record.getValue("createDate")); + assertNotNull(record.getValue("modifyDate")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testModifyDateHappyPath() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + + QRecord record = new QRecord().withValue("id", 1); + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record)); + + assertNull(record.getValue("createDate")); + assertNotNull(record.getValue("modifyDate")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNone() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.getField("createDate").withBehavior(DynamicDefaultValueBehavior.NONE); + table.getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.NONE); + + QRecord record = new QRecord().withValue("id", 1); + + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); + assertNull(record.getValue("createDate")); + assertNull(record.getValue("modifyDate")); + + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record)); + assertNull(record.getValue("createDate")); + assertNull(record.getValue("modifyDate")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDateInsteadOfDateTimeField() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.getField("createDate").withType(QFieldType.DATE); + + QRecord record = new QRecord().withValue("id", 1); + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); + assertNotNull(record.getValue("createDate")); + assertThat(record.getValue("createDate")).isInstanceOf(LocalDate.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNonDateField() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.getField("firstName").withBehavior(DynamicDefaultValueBehavior.CREATE_DATE); + + QRecord record = new QRecord().withValue("id", 1); + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); + assertNull(record.getValue("firstName")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaDataTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaDataTest.java new file mode 100644 index 00000000..f8dacf15 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaDataTest.java @@ -0,0 +1,71 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for QFieldMetaData + *******************************************************************************/ +class QFieldMetaDataTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldBehaviors() + { + ///////////////////////////////////////// + // create field - assert default state // + ///////////////////////////////////////// + QFieldMetaData field = new QFieldMetaData("createDate", QFieldType.DATE_TIME); + assertTrue(CollectionUtils.nullSafeIsEmpty(field.getBehaviors())); + assertNull(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + assertEquals(DynamicDefaultValueBehavior.NONE, field.getBehaviorOrDefault(new QInstance(), DynamicDefaultValueBehavior.class)); + + ////////////////////////////////////// + // add NONE behavior - assert state // + ////////////////////////////////////// + field.withBehavior(DynamicDefaultValueBehavior.NONE); + assertEquals(1, field.getBehaviors().size()); + assertEquals(DynamicDefaultValueBehavior.NONE, field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + assertEquals(DynamicDefaultValueBehavior.NONE, field.getBehaviorOrDefault(new QInstance(), DynamicDefaultValueBehavior.class)); + + ///////////////////////////////////////////////////////// + // replace behavior - assert it got rid of the old one // + ///////////////////////////////////////////////////////// + field.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE); + assertEquals(1, field.getBehaviors().size()); + assertEquals(DynamicDefaultValueBehavior.CREATE_DATE, field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)); + assertEquals(DynamicDefaultValueBehavior.CREATE_DATE, field.getBehaviorOrDefault(new QInstance(), DynamicDefaultValueBehavior.class)); + } + +} \ No newline at end of file