Compare commits

..

1 Commits

30 changed files with 229 additions and 948 deletions

View File

@ -91,6 +91,20 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
long start = System.currentTimeMillis();
DMLType dmlType = getDMLType(tableActionInput);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// currently, the table's primary key must be integer... so, log (once) and return early if not that //
///////////////////////////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(table.getPrimaryKeyField());
if(!QFieldType.INTEGER.equals(field.getType()))
{
if(!loggedUnauditableTableNames.contains(table.getName()))
{
LOG.info("Cannot audit table without integer as its primary key", logPair("tableName", table.getName()));
loggedUnauditableTableNames.add(table.getName());
}
return (output);
}
try
{
List<QRecord> recordList = CollectionUtils.nonNullList(input.getRecordList()).stream()
@ -105,21 +119,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
return (output);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// currently, the table's primary key must be integer... so, log (once) and return early if not that //
// (or, if no primary key!) //
///////////////////////////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getFields().get(table.getPrimaryKeyField());
if(field == null || !QFieldType.INTEGER.equals(field.getType()))
{
if(!loggedUnauditableTableNames.contains(table.getName()))
{
LOG.info("Cannot audit table without integer as its primary key", logPair("tableName", table.getName()));
loggedUnauditableTableNames.add(table.getName());
}
return (output);
}
String contextSuffix = getContentSuffix(input);
AuditInput auditInput = new AuditInput();

View File

@ -23,6 +23,7 @@ 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;
@ -206,6 +207,17 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
return (rs);
}
/////////////////////////////////////////////
// set values in create date & modify date //
// todo .. better (not hard-coded names) //
/////////////////////////////////////////////
Instant now = Instant.now();
for(QRecord record : insertInput.getRecords())
{
setValueIfTableHasField(record, insertInput.getTable(), "createDate", now);
setValueIfTableHasField(record, insertInput.getTable(), "modifyDate", now);
}
//////////////////////////////////////////////////////
// load the backend module and its insert interface //
//////////////////////////////////////////////////////
@ -221,6 +233,29 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
** If the table has a field with the given name, then set the given value in the
** given record.
*******************************************************************************/
private static 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. //
/////////////////////////////////////////////////
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -242,7 +277,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
setDefaultValuesInRecords(table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
setErrorsIfUniqueKeyErrors(insertInput, table);

View File

@ -235,7 +235,7 @@ public class UpdateAction
/////////////////////////////
// run standard validators //
/////////////////////////////
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords());
validatePrimaryKeysAreGiven(updateInput);
if(oldRecordList.isPresent())

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@ -60,6 +61,11 @@ public class UpdateActionRecordSplitHelper
for(QRecord record : updateInput.getRecords())
{
////////////////////////////////////////////
// todo .. better (not a hard-coded name) //
////////////////////////////////////////////
setValueIfTableHasField(record, table, "modifyDate", now);
List<String> updatableFields = table.getFields().values().stream()
.map(QFieldMetaData::getName)
// todo - intent here is to avoid non-updateable fields - but this
@ -141,6 +147,29 @@ 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
**

View File

@ -25,10 +25,12 @@ 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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@ -40,10 +42,16 @@ public class ValueBehaviorApplier
/*******************************************************************************
**
*******************************************************************************/
public enum Action
public static void applyFieldBehaviors(QInstance instance, QTableMetaData table, List<QRecord> recordList)
{
INSERT,
UPDATE
for(QFieldMetaData field : table.getFields().values())
{
String fieldName = field.getName();
if(field.getType().equals(QFieldType.STRING) && field.getMaxLength() != null)
{
applyValueTooLongBehavior(instance, recordList, field, fieldName);
}
}
}
@ -51,18 +59,31 @@ public class ValueBehaviorApplier
/*******************************************************************************
**
*******************************************************************************/
public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList)
private static void applyValueTooLongBehavior(QInstance instance, List<QRecord> recordList, QFieldMetaData field, String fieldName)
{
if(CollectionUtils.nullSafeIsEmpty(recordList))
{
return;
}
ValueTooLongBehavior valueTooLongBehavior = field.getBehavior(instance, ValueTooLongBehavior.class);
for(QFieldMetaData field : table.getFields().values())
////////////////////////////////////////////////////////////////////////////////////////////////////
// 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(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
for(QRecord record : recordList)
{
fieldBehavior.apply(action, recordList, instance, table, field);
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);
}
}
}
}
}

View File

@ -42,7 +42,6 @@ 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;
@ -95,8 +94,10 @@ public class QInstanceEnricher
private JoinGraph joinGraph;
private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true;
private boolean configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate = true;
//////////////////////////////////////////////////////////
// todo - come up w/ a way for app devs to set configs! //
//////////////////////////////////////////////////////////
private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true;
//////////////////////////////////////////////////////////////////////////////////////////////////
// let an instance define mappings to be applied during name-to-label enrichments, //
@ -463,22 +464,6 @@ 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);
}
}
}
@ -1235,66 +1220,4 @@ 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);
}
}

View File

@ -31,7 +31,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
@ -76,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
@ -87,6 +87,7 @@ 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;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
/*******************************************************************************
@ -493,6 +494,11 @@ public class QInstanceValidator
validateTableRecordSecurityLocks(qInstance, table);
validateTableAssociations(qInstance, table);
validateExposedJoins(qInstance, joinGraph, table);
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
{
supplementalTableMetaData.validate(qInstance, table, this);
}
});
}
}
@ -692,7 +698,7 @@ public class QInstanceValidator
String prefix = "Field " + fieldName + " in table " + tableName + " ";
ValueTooLongBehavior behavior = field.getBehaviorOrDefault(qInstance, ValueTooLongBehavior.class);
ValueTooLongBehavior behavior = field.getBehavior(qInstance, ValueTooLongBehavior.class);
if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH))
{
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
@ -1245,25 +1251,7 @@ public class QInstanceValidator
{
if(fieldMetaData.getDefaultValue() != null && fieldMetaData.getDefaultValue() instanceof QCodeReference codeReference)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// by default, assume that any process field which is a QCodeReference should be a reference to a BackendStep... //
// but... allow a secondary field name to be set, to tell us what class to *actually* expect here... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Class<?> expectedClass = BackendStep.class;
try
{
Optional<QFieldMetaData> expectedTypeField = backendStepMetaData.getInputMetaData().getField(fieldMetaData.getName() + "_expectedType");
if(expectedTypeField.isPresent() && expectedTypeField.get().getDefaultValue() != null)
{
expectedClass = Class.forName(ValueUtils.getValueAsString(expectedTypeField.get().getDefaultValue()));
}
}
catch(Exception e)
{
warn("Error loading expectedType for field [" + fieldMetaData.getName() + "] in process [" + processName + "]: " + e.getMessage());
}
validateSimpleCodeReference("Process " + processName + " code reference: ", codeReference, expectedClass);
validateSimpleCodeReference("Process " + processName + " backend step code reference: ", codeReference, BackendStep.class);
}
}
}
@ -1784,20 +1772,6 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
@FunctionalInterface
interface UnsafeLambda
{
/*******************************************************************************
**
*******************************************************************************/
void run() throws Exception;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -186,7 +186,7 @@ public class QRecord implements Serializable
//////////////////////////////////////////////////////////////////////////////
// we know entry is serializable at this point, based on type param's bound //
//////////////////////////////////////////////////////////////////////////////
LOG.debug("Non-primitive serializable value in QRecord - calling SerializationUtils.clone...", logPair("key", entry.getKey()), logPair("type", value.getClass()));
LOG.info("Non-primitive serializable value in QRecord - calling SerializationUtils.clone...", logPair("key", entry.getKey()), logPair("type", value.getClass()));
clone.put(entry.getKey(), (V) SerializationUtils.clone(entry.getValue()));
}
}

View File

@ -746,22 +746,12 @@ public class QInstance
/*******************************************************************************
** If pass a QInstanceValidationKey (which can only be instantiated by the validator),
** then the hasBeenValidated field will be set to true.
** Setter for hasBeenValidated
**
** Else, if passed a null, hasBeenValidated will be reset to false - e.g., to
** re-trigger validation (can be useful in tests).
*******************************************************************************/
public void setHasBeenValidated(QInstanceValidationKey key)
{
if(key == null)
{
this.hasBeenValidated = false;
}
else
{
this.hasBeenValidated = true;
}
this.hasBeenValidated = true;
}

View File

@ -1,164 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<DynamicDefaultValueBehavior>
{
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<QRecord> 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<QRecord> recordList, QTableMetaData table, QFieldMetaData field)
{
if(!ValueBehaviorApplier.Action.INSERT.equals(action))
{
return;
}
setCreateDateOrModifyDateOnList(recordList, table, field);
}
/*******************************************************************************
**
*******************************************************************************/
private void applyModifyDate(ValueBehaviorApplier.Action action, List<QRecord> 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<QRecord> 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()
{
}
}

View File

@ -22,41 +22,10 @@
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<T extends FieldBehavior<T>>
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<QRecord> 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);
}
}

View File

@ -35,7 +35,6 @@ 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;
@ -45,7 +44,6 @@ 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;
/*******************************************************************************
@ -54,8 +52,6 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
*******************************************************************************/
public class QFieldMetaData implements Cloneable
{
private static final QLogger LOG = QLogger.getLogger(QFieldMetaData.class);
private String name;
private String label;
private String backendName;
@ -77,8 +73,8 @@ public class QFieldMetaData implements Cloneable
private String possibleValueSourceName;
private QQueryFilter possibleValueSourceFilter;
private Integer maxLength;
private Set<FieldBehavior<?>> behaviors;
private Integer maxLength;
private Set<FieldBehavior> behaviors;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// w/ longer-term vision for FieldBehaviors //
@ -678,7 +674,7 @@ public class QFieldMetaData implements Cloneable
** Getter for behaviors
**
*******************************************************************************/
public Set<FieldBehavior<?>> getBehaviors()
public Set<FieldBehavior> getBehaviors()
{
return behaviors;
}
@ -686,12 +682,11 @@ 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 extends FieldBehavior<T>> T getBehaviorOrDefault(QInstance instance, Class<T> behaviorType)
public <T extends FieldBehavior> T getBehavior(QInstance instance, Class<T> behaviorType)
{
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(behaviors))
for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(behaviors))
{
if(behaviorType.isInstance(fieldBehavior))
{
@ -706,33 +701,9 @@ public class QFieldMetaData implements Cloneable
///////////////////////////////////////////
// return default behavior for this type //
///////////////////////////////////////////
if(behaviorType.isEnum())
if(behaviorType.equals(ValueTooLongBehavior.class))
{
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 extends FieldBehavior<T>> T getBehaviorOnlyIfSet(Class<T> behaviorType)
{
if(behaviors == null)
{
return (null);
}
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(behaviors))
{
if(behaviorType.isInstance(fieldBehavior))
{
return (behaviorType.cast(fieldBehavior));
}
return behaviorType.cast(ValueTooLongBehavior.getDefault());
}
return (null);
@ -744,7 +715,7 @@ public class QFieldMetaData implements Cloneable
** Setter for behaviors
**
*******************************************************************************/
public void setBehaviors(Set<FieldBehavior<?>> behaviors)
public void setBehaviors(Set<FieldBehavior> behaviors)
{
this.behaviors = behaviors;
}
@ -755,7 +726,7 @@ public class QFieldMetaData implements Cloneable
** Fluent setter for behaviors
**
*******************************************************************************/
public QFieldMetaData withBehaviors(Set<FieldBehavior<?>> behaviors)
public QFieldMetaData withBehaviors(Set<FieldBehavior> behaviors)
{
this.behaviors = behaviors;
return (this);
@ -767,30 +738,12 @@ 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);
}

View File

@ -22,85 +22,23 @@
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<ValueTooLongBehavior>
public enum ValueTooLongBehavior implements FieldBehavior
{
TRUNCATE,
TRUNCATE_ELLIPSIS,
ERROR,
PASS_THROUGH;
private static final QLogger LOG = QLogger.getLogger(ValueTooLongBehavior.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public ValueTooLongBehavior getDefault()
public static FieldBehavior getDefault()
{
return (PASS_THROUGH);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> 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);
}
}
}
return PASS_THROUGH;
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -69,4 +70,16 @@ public abstract class QSupplementalTableMetaData
// noop in base class //
////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
{
////////////////////////
// noop in base class //
////////////////////////
}
}

View File

@ -176,7 +176,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// process a sessionUUID - looks up userSession record - cannot create token this way. //
/////////////////////////////////////////////////////////////////////////////////////////
String sessionUUID = context.get(SESSION_UUID_KEY);
LOG.debug("Creating session from sessionUUID (userSession)", logPair("sessionUUID", maskForLog(sessionUUID)));
LOG.info("Creating session from sessionUUID (userSession)", logPair("sessionUUID", maskForLog(sessionUUID)));
if(sessionUUID != null)
{
accessToken = getAccessTokenFromSessionUUID(metaData, sessionUUID);

View File

@ -276,7 +276,6 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
if(sourceKeyValue == null || "".equals(sourceKeyValue))
{
LOG.debug("Skipping record without a value in the sourceKeyField", logPair("keyField", sourceTableKeyField));
errorMissingKeyField.incrementCountAndAddPrimaryKey(sourcePrimaryKey);
try

View File

@ -176,19 +176,6 @@ public class StringUtils
/*******************************************************************************
** safely appends a string to another, changing empty string if either value is null
**
*******************************************************************************/
public static String safeAppend(String input, String contentToAppend)
{
input = input != null ? input : "";
contentToAppend = contentToAppend != null ? contentToAppend : "";
return input + contentToAppend;
}
/*******************************************************************************
** returns input if not null, or nullOutput if input == null (as in SQL NVL)
**

View File

@ -0,0 +1,37 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils.lambdas;
/*******************************************************************************
**
*******************************************************************************/
@FunctionalInterface
public interface UnsafeLambda
{
/*******************************************************************************
**
*******************************************************************************/
void run() throws Exception;
}

View File

@ -400,49 +400,4 @@ class DMLAuditActionTest extends BaseTest
QContext.popAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableWithoutIntegerPrimaryKey() throws QException
{
QInstance qInstance = QContext.getQInstance();
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
////////////////////////////////////////////////////////////////////////////////////////////////////
// we used to throw if table had no primary key. first, assert that we do not throw in that case //
////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.getQInstance().addTable(
new QTableMetaData()
.withName("nullPkey")
.withField(new QFieldMetaData("foo", QFieldType.STRING))
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD)));
new DMLAuditAction().execute(new DMLAuditInput()
.withTableActionInput(new InsertInput("nullPkey"))
.withRecordList(List.of(new QRecord())));
//////////////////////////////////////////////////////////////////////////////////////////////
// next, make sure we don't throw (and don't record anything) if table's pkey isn't integer //
//////////////////////////////////////////////////////////////////////////////////////////////
QContext.getQInstance().addTable(
new QTableMetaData()
.withName("stringPkey")
.withField(new QFieldMetaData("idString", QFieldType.STRING))
.withPrimaryKeyField("idString")
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD)));
new DMLAuditAction().execute(new DMLAuditInput()
.withTableActionInput(new InsertInput("stringPkey"))
.withRecordList(List.of(new QRecord())));
//////////////////////////////////
// make sure no audits happened //
//////////////////////////////////
List<QRecord> auditList = TestUtils.queryTable("audit");
assertTrue(auditList.isEmpty());
}
}

View File

@ -38,6 +38,7 @@ 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;
@ -63,7 +64,6 @@ 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,7 +71,6 @@ 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<List<String>, QRecord> recordsByFieldBeingUpdated = updateActionRecordSplitHelper.getRecordsByFieldBeingUpdated();
@ -79,6 +78,12 @@ class UpdateActionRecordSplitHelperTest extends BaseTest
Function<Collection<QRecord>, Set<Integer>> 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 //
//////////////////////////////////////////////////////////////

View File

@ -39,9 +39,7 @@ import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
** Unit test for ValueBehaviorApplier - and also providing coverage for
** ValueTooLongBehavior (the first implementation, which was previously in the
** class under test).
** Unit test for ValueBehaviorApplier
*******************************************************************************/
class ValueBehaviorApplierTest extends BaseTest
{
@ -63,7 +61,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(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList);
ValueBehaviorApplier.applyFieldBehaviors(qInstance, table, recordList);
assertEquals("First name", getRecordById(recordList, 1).getValueString("firstName"));
assertEquals("Last na...", getRecordById(recordList, 2).getValueString("lastName"));
@ -95,7 +93,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(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList);
ValueBehaviorApplier.applyFieldBehaviors(qInstance, table, recordList);
assertEquals("First name too long", getRecordById(recordList, 1).getValueString("firstName"));
assertNull(getRecordById(recordList, 1).getValueString("lastName"));

View File

@ -29,7 +29,6 @@ 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;
@ -494,39 +493,4 @@ 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));
}
}

View File

@ -1,139 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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"));
}
}

View File

@ -1,71 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@ -78,20 +78,6 @@ class StringUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void test_safeAppend()
{
assertEquals("Foo", StringUtils.safeAppend("Foo", null));
assertEquals("Foo", StringUtils.safeAppend(null, "Foo"));
assertEquals("FooBar", StringUtils.safeAppend("Foo", "Bar"));
assertEquals("", StringUtils.safeAppend(null, null));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -709,11 +709,11 @@ public class BaseAPIActionUtil
if(backendMetaData.getAuthorizationType().equals(AuthorizationType.BASIC_AUTH_USERNAME_PASSWORD))
{
request.setHeader("Authorization", getBasicAuthenticationHeader(record.getValueString(backendMetaData.getVariantOptionsTableUsernameField()), record.getValueString(backendMetaData.getVariantOptionsTablePasswordField())));
request.addHeader("Authorization", getBasicAuthenticationHeader(record.getValueString(backendMetaData.getVariantOptionsTableUsernameField()), record.getValueString(backendMetaData.getVariantOptionsTablePasswordField())));
}
else if(backendMetaData.getAuthorizationType().equals(AuthorizationType.API_KEY_HEADER))
{
request.setHeader("API-Key", record.getValueString(backendMetaData.getVariantOptionsTableApiKeyField()));
request.addHeader("API-Key", record.getValueString(backendMetaData.getVariantOptionsTableApiKeyField()));
}
else
{
@ -727,10 +727,10 @@ public class BaseAPIActionUtil
///////////////////////////////////////////////////////////////////////////////////////////
switch(backendMetaData.getAuthorizationType())
{
case BASIC_AUTH_API_KEY -> request.setHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getApiKey()));
case BASIC_AUTH_USERNAME_PASSWORD -> request.setHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getUsername(), backendMetaData.getPassword()));
case API_KEY_HEADER -> request.setHeader("API-Key", backendMetaData.getApiKey());
case API_TOKEN -> request.setHeader("Authorization", "Token " + backendMetaData.getApiKey());
case BASIC_AUTH_API_KEY -> request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getApiKey()));
case BASIC_AUTH_USERNAME_PASSWORD -> request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getUsername(), backendMetaData.getPassword()));
case API_KEY_HEADER -> request.addHeader("API-Key", backendMetaData.getApiKey());
case API_TOKEN -> request.addHeader("Authorization", "Token " + backendMetaData.getApiKey());
case OAUTH2 -> request.setHeader("Authorization", "Bearer " + getOAuth2Token());
case API_KEY_QUERY_PARAM ->
{
@ -786,9 +786,9 @@ public class BaseAPIActionUtil
if(setCredentialsInHeader)
{
request.setHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getClientId(), backendMetaData.getClientSecret()));
request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getClientId(), backendMetaData.getClientSecret()));
}
request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
HttpResponse response = executeOAuthTokenRequest(client, request);
int statusCode = response.getStatusLine().getStatusCode();
@ -850,7 +850,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected void setupContentTypeInRequest(HttpRequestBase request)
{
request.setHeader("Content-Type", backendMetaData.getContentType());
request.addHeader("Content-Type", backendMetaData.getContentType());
}
@ -872,7 +872,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
public void setupAdditionalHeaders(HttpRequestBase request)
{
request.setHeader("Accept", "application/json");
request.addHeader("Accept", "application/json");
}
@ -1081,7 +1081,7 @@ public class BaseAPIActionUtil
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// trim response body (just to keep logs smaller, or, in case someone consuming logs doesn't want such long lines) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.debug("Received successful response with code [" + qResponse.getStatusCode() + "] and content [" + StringUtils.safeTruncate(qResponse.getContent(), getMaxResponseMessageLengthForLog(), "...") + "].");
LOG.info("Received successful response with code [" + qResponse.getStatusCode() + "] and content [" + StringUtils.safeTruncate(qResponse.getContent(), getMaxResponseMessageLengthForLog(), "...") + "].");
return (qResponse);
}
}

View File

@ -141,38 +141,28 @@ public class FilesystemImporterMetaDataTemplate
/*******************************************************************************
** Set up importRecord table being built by this template to hve an automation-
** status field on it, and an automation details object attached to it.
**
*******************************************************************************/
public void addImportRecordAutomations(QFieldMetaData automationStatusField, QTableAutomationDetails automationDetails)
public void addAutomationStatusField(QTableMetaData table, QFieldMetaData automationStatusField)
{
getImportRecordTable().addField(automationStatusField);
getImportRecordTable().getSections().get(1).getFieldNames().add(0, automationStatusField.getName());
getImportRecordTable().withAutomationDetails(automationDetails);
table.addField(automationStatusField);
table.getSections().get(1).getFieldNames().add(0, automationStatusField.getName());
}
/*******************************************************************************
** Add 1 process as a post-insert automation-action on this template's importRecord
** table.
**
** The automation action is returned - which you may want for changing things, e.g.,
** its priority (e.g., addImportRecordPostInsertAutomationAction(...).withPriority(1);
*******************************************************************************/
public TableAutomationAction addImportRecordPostInsertAutomationAction(String processName)
public TableAutomationAction addStandardPostInsertAutomation(QTableMetaData table, QTableAutomationDetails automationDetails, String processName)
{
if(getImportRecordTable().getAutomationDetails() == null)
{
throw (new IllegalStateException(getImportRecordTable().getName() + " does not have automationDetails - do you need to call addAutomations first?"));
}
TableAutomationAction action = new TableAutomationAction()
.withName(processName)
.withName(table.getName() + "PostInsert")
.withTriggerEvent(TriggerEvent.POST_INSERT)
.withProcessName(processName);
getImportRecordTable().getAutomationDetails().withAction(action);
table.withAutomationDetails(automationDetails
.withAction(action));
return (action);
}

View File

@ -23,8 +23,6 @@ package com.kingsrook.qqq.backend.module.filesystem.processes.implementations.fi
import java.io.Serializable;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@ -64,15 +62,6 @@ public class FilesystemImporterProcessMetaDataBuilder extends AbstractProcessMet
.withField(new QFieldMetaData(FilesystemImporterStep.FIELD_ARCHIVE_PATH, QFieldType.STRING))
.withField(new QFieldMetaData(FilesystemImporterStep.FIELD_IMPORT_SECURITY_FIELD_NAME, QFieldType.STRING))
.withField(new QFieldMetaData(FilesystemImporterStep.FIELD_IMPORT_SECURITY_FIELD_VALUE, QFieldType.STRING))
//////////////////////////////////////////////////////////////////////////////////////
// define a QCodeReference - expected to be of type Function<QRecord, Serializable> //
// make sure the QInstanceValidator knows that the QCodeReference should be a //
// Function (not a BackendStep, which is the default for process fields) //
//////////////////////////////////////////////////////////////////////////////////////
.withField(new QFieldMetaData(FilesystemImporterStep.FIELD_IMPORT_SECURITY_VALUE_SUPPLIER, QFieldType.STRING))
.withField(new QFieldMetaData(FilesystemImporterStep.FIELD_IMPORT_SECURITY_VALUE_SUPPLIER + "_expectedType", QFieldType.STRING)
.withDefaultValue(Function.class.getName()))
)));
}
@ -197,15 +186,4 @@ public class FilesystemImporterProcessMetaDataBuilder extends AbstractProcessMet
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public FilesystemImporterProcessMetaDataBuilder withImportSecurityValueSupplierFunction(Class<? extends Function<QRecord, Serializable>> supplierFunction)
{
setInputFieldDefaultValue(FilesystemImporterStep.FIELD_IMPORT_SECURITY_VALUE_SUPPLIER, new QCodeReference(supplierFunction));
return (this);
}
}

View File

@ -33,9 +33,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.UUID;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -43,7 +41,6 @@ import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter;
import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@ -56,7 +53,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -87,9 +83,8 @@ public class FilesystemImporterStep implements BackendStep
public static final String FIELD_IMPORT_FILE_TABLE = "importFileTable";
public static final String FIELD_IMPORT_RECORD_TABLE = "importRecordTable";
public static final String FIELD_IMPORT_SECURITY_FIELD_NAME = "importSecurityFieldName";
public static final String FIELD_IMPORT_SECURITY_FIELD_VALUE = "importSecurityFieldValue";
public static final String FIELD_IMPORT_SECURITY_VALUE_SUPPLIER = "importSecurityFieldSupplier";
public static final String FIELD_IMPORT_SECURITY_FIELD_NAME = "importSecurityFieldName";
public static final String FIELD_IMPORT_SECURITY_FIELD_VALUE = "importSecurityFieldValue";
public static final String FIELD_ARCHIVE_FILE_ENABLED = "archiveFileEnabled";
public static final String FIELD_ARCHIVE_TABLE_NAME = "archiveTableName";
@ -98,7 +93,6 @@ public class FilesystemImporterStep implements BackendStep
public static final String FIELD_UPDATE_FILE_IF_NAME_EXISTS = "updateFileIfNameExists";
private Function<QRecord, Serializable> securitySupplier = null;
/*******************************************************************************
@ -273,34 +267,9 @@ public class FilesystemImporterStep implements BackendStep
*******************************************************************************/
private void addSecurityValue(RunBackendStepInput runBackendStepInput, QRecord record)
{
String securityField = runBackendStepInput.getValueString(FIELD_IMPORT_SECURITY_FIELD_NAME);
String securityField = runBackendStepInput.getValueString(FIELD_IMPORT_SECURITY_FIELD_NAME);
Serializable securityValue = runBackendStepInput.getValue(FIELD_IMPORT_SECURITY_FIELD_VALUE);
/////////////////////////////////////////////////////////////
// if we're using a security supplier function, load it up //
/////////////////////////////////////////////////////////////
QCodeReference securitySupplierReference = (QCodeReference) runBackendStepInput.getValue(FIELD_IMPORT_SECURITY_VALUE_SUPPLIER);
try
{
if(securitySupplierReference != null && securitySupplier == null)
{
securitySupplier = QCodeLoader.getAdHoc(Function.class, securitySupplierReference);
}
}
catch(Exception e)
{
throw (new QRuntimeException("Error loading Security Supplier Function from QCodeReference [" + securitySupplierReference + "]", e));
}
///////////////////////////////////////////////////////////////////////////////////////
// either get the security value from the supplier, or the field value field's value //
///////////////////////////////////////////////////////////////////////////////////////
Serializable securityValue = securitySupplier != null
? securitySupplier.apply(record)
: runBackendStepInput.getValue(FIELD_IMPORT_SECURITY_FIELD_VALUE);
////////////////////////////////////////////////////////////////////
// if we have a field name and a value, then add it to the record //
////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(securityField) && securityValue != null)
{
record.setValue(securityField, securityValue);

View File

@ -23,20 +23,16 @@ package com.kingsrook.qqq.backend.module.filesystem.processes.implementations.fi
import java.io.File;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
@ -265,57 +261,4 @@ class FilesystemImporterStepTest extends FilesystemActionTest
assertEquals(47, recordRecord.getValue("customerId"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSecuritySupplier() throws QException
{
//////////////////////////////////////////////
// Add a security name/value to our process //
//////////////////////////////////////////////
QProcessMetaData process = QContext.getQInstance().getProcess(TestUtils.LOCAL_PERSON_CSV_FILE_IMPORTER_PROCESS_NAME);
process.getInputFields().stream().filter(f -> f.getName().equals(FilesystemImporterStep.FIELD_IMPORT_SECURITY_FIELD_NAME)).findFirst().get().setDefaultValue("customerId");
process.getInputFields().stream().filter(f -> f.getName().equals(FilesystemImporterStep.FIELD_IMPORT_SECURITY_VALUE_SUPPLIER)).findFirst().get().setDefaultValue(new QCodeReference(SecuritySupplier.class));
//////////////////////////////////////////////////////////////////////////////////////////////////////
// re-validate our instance now that we have that code-reference in place for the security supplier //
//////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.getQInstance().setHasBeenValidated(null);
new QInstanceValidator().validate(QContext.getQInstance());
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(TestUtils.LOCAL_PERSON_CSV_FILE_IMPORTER_PROCESS_NAME);
new RunProcessAction().execute(runProcessInput);
////////////////////////////////////////////////////////////////////////////////////////////
// assert the security field gets its value on both the importFile & importRecord records //
////////////////////////////////////////////////////////////////////////////////////////////
String importBaseName = "personImporter";
QRecord fileRecord = new GetAction().executeForRecord(new GetInput(importBaseName + FilesystemImporterMetaDataTemplate.IMPORT_FILE_TABLE_SUFFIX).withPrimaryKey(1));
assertEquals(1701, fileRecord.getValue("customerId"));
QRecord recordRecord = new GetAction().executeForRecord(new GetInput(importBaseName + FilesystemImporterMetaDataTemplate.IMPORT_RECORD_TABLE_SUFFIX).withPrimaryKey(1));
assertEquals(1701, recordRecord.getValue("customerId"));
}
/*******************************************************************************
**
*******************************************************************************/
public static class SecuritySupplier implements Function<QRecord, Serializable>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable apply(QRecord qRecord)
{
return (1701);
}
}
}