Compare commits

..

11 Commits

Author SHA1 Message Date
3d9a6fa249 updates to allow update input to omit updating the modify date behavior 2024-03-05 12:14:45 -06:00
e47b2c9497 Merge pull request #71 from Kingsrook/feature/CE-889-bug-orders-sent-to-wms-with
CE-889 - improvements for streaming etl pipes:
2024-03-04 11:55:39 -06:00
fdc948f96c Merge pull request #70 from Kingsrook/feature/CE-940-rollo-optimization-to-use-tnt
CE-940 Add AuditDetailAccumulator, and a means to share it (generical…
2024-03-04 11:46:50 -06:00
59ca1e3a1c Merge pull request #68 from Kingsrook/feature/heal-automations-updates
CE-847 - Add review screen to HealBadRecordAutomationStatusesProcess;…
2024-03-04 11:46:38 -06:00
52d7d0e8ae Merge pull request #73 from Kingsrook/hotfix/file-importer
Increasing logging for s3 importer archive errors
2024-03-04 11:00:11 -06:00
d1792dde11 Merge pull request #72 from Kingsrook/feature/CE-878-make-the-operations-dashboard
Feature/ce 878 make the operations dashboard
2024-03-04 10:58:36 -06:00
b0aaf61e99 CE-940 Add AuditDetailAccumulator, and a means to share it (generically) via QContext 2024-03-04 07:52:47 -06:00
dd103d323d CE-878: added flag to not do deletes upon replace action 2024-03-01 07:19:57 -06:00
238521aa57 CE-878: added sublabel to widget data 2024-02-29 15:33:26 -06:00
cdf59e8f2b CE-847 - Add review screen to HealBadRecordAutomationStatusesProcess; update to query by createDate for pending-inserts 2024-02-27 10:09:00 -06:00
01d96cc8ae Increasing logging for s3 importer archive errors 2024-02-20 18:24:48 -06:00
22 changed files with 754 additions and 87 deletions

View File

@ -242,7 +242,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
setDefaultValuesInRecords(table, insertInput.getRecords()); setDefaultValuesInRecords(table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords()); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords(), null);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS); runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
setErrorsIfUniqueKeyErrors(insertInput, table); setErrorsIfUniqueKeyErrors(insertInput, table);

View File

@ -136,6 +136,8 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
UpdateOutput updateOutput = new UpdateAction().execute(updateInput); UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
output.setUpdateOutput(updateOutput); output.setUpdateOutput(updateOutput);
if(input.getPerformDeletes())
{
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep)); QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
if(input.getFilter() != null) if(input.getFilter() != null)
{ {
@ -149,6 +151,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit()); deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
output.setDeleteOutput(deleteOutput); output.setDeleteOutput(deleteOutput);
}
if(weOwnTheTransaction) if(weOwnTheTransaction)
{ {

View File

@ -57,6 +57,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
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.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -72,6 +74,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -244,7 +247,13 @@ public class UpdateAction
///////////////////////////// /////////////////////////////
// run standard validators // // run standard validators //
///////////////////////////// /////////////////////////////
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords()); Set<FieldBehavior<?>> behaviorsToOmit = null;
if(BooleanUtils.isTrue(updateInput.getOmitModifyDateUpdate()))
{
behaviorsToOmit = Set.of(DynamicDefaultValueBehavior.MODIFY_DATE);
}
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords(), behaviorsToOmit);
validatePrimaryKeysAreGiven(updateInput); validatePrimaryKeysAreGiven(updateInput);
if(oldRecordList.isPresent()) if(oldRecordList.isPresent())

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
@ -51,7 +52,7 @@ public class ValueBehaviorApplier
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList) public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList, Set<FieldBehavior<?>> behaviorsToOmit)
{ {
if(CollectionUtils.nullSafeIsEmpty(recordList)) if(CollectionUtils.nullSafeIsEmpty(recordList))
{ {
@ -62,7 +63,7 @@ public class ValueBehaviorApplier
{ {
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors())) for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
{ {
fieldBehavior.apply(action, recordList, instance, table, field); fieldBehavior.apply(action, recordList, instance, table, field, behaviorsToOmit);
} }
} }
} }

View File

@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.context; package com.kingsrook.qqq.backend.core.context;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Stack; import java.util.Stack;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -31,6 +34,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -47,6 +51,7 @@ public class QContext
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>(); private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>(); private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Map<String, Serializable>> objectsThreadLocal = new ThreadLocal<>();
/******************************************************************************* /*******************************************************************************
@ -132,6 +137,7 @@ public class QContext
qSessionThreadLocal.remove(); qSessionThreadLocal.remove();
qBackendTransactionThreadLocal.remove(); qBackendTransactionThreadLocal.remove();
actionStackThreadLocal.remove(); actionStackThreadLocal.remove();
objectsThreadLocal.remove();
} }
@ -259,4 +265,92 @@ public class QContext
return (Optional.of(actionStackThreadLocal.get().get(0))); return (Optional.of(actionStackThreadLocal.get().get(0)));
} }
/*******************************************************************************
** get one named object from the Context for the current thread. may return null.
*******************************************************************************/
public static Serializable getObject(String key)
{
if(objectsThreadLocal.get() == null)
{
return null;
}
return objectsThreadLocal.get().get(key);
}
/*******************************************************************************
** get one named object from the Context for the current thread, cast to the
** specified type if possible. if not found, or wrong type, empty is returned.
*******************************************************************************/
public static <T extends Serializable> Optional<T> getObject(String key, Class<T> type)
{
Serializable object = getObject(key);
if(type.isInstance(object))
{
return Optional.of(type.cast(object));
}
else if(object == null)
{
return Optional.empty();
}
else
{
LOG.warn("Unexpected type of object found in session under key [" + key + "]",
logPair("expectedType", type.getName()),
logPair("actualType", object.getClass().getName())
);
return Optional.empty();
}
}
/*******************************************************************************
** put a named object into the Context for the current thread.
*******************************************************************************/
public static void setObject(String key, Serializable object)
{
if(objectsThreadLocal.get() == null)
{
objectsThreadLocal.set(new HashMap<>());
}
objectsThreadLocal.get().put(key, object);
}
/*******************************************************************************
** remove a named object from the Context of the current thread.
*******************************************************************************/
public static void removeObject(String key)
{
if(objectsThreadLocal.get() != null)
{
objectsThreadLocal.get().remove(key);
}
}
/*******************************************************************************
** get the full map of named objects for the current thread (possibly null).
*******************************************************************************/
public static Map<String, Serializable> getObjects()
{
return objectsThreadLocal.get();
}
/*******************************************************************************
** fully replace the map of named objets for the current thread.
*******************************************************************************/
public static void setObjects(Map<String, Serializable> objects)
{
objectsThreadLocal.set(objects);
}
} }

View File

@ -0,0 +1,156 @@
/*
* 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.actions.audits;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Object to accumulate multiple audit-details to be recorded under a single
** audit per-record, within a process step. Especially useful if/when the
** process step spreads its work out through multiple classes.
**
** Pattern of usage looks like:
**
** <pre>
** // declare as a field (or local) w/ message for the audit headers
** private AuditDetailAccumulator auditDetailAccumulator = new AuditDetailAccumulator("Audit header message");
**
** // put into thread context
** AuditDetailAccumulator.setInContext(auditDetailAccumulator);
**
** // add a detail message for a record
** auditDetailAccumulator.addAuditDetail(tableName, record, "Detail message");
**
** // in another class, get the accumulator from context and safely add a detail message
** AuditDetailAccumulator.getFromContext().ifPresent(ada -> ada.addAuditDetail(tableName, record, "More Details"));
**
** // at the end of a step run/runOnePage method, add the accumulated audit details to step output
** auditDetailAccumulator.getAccumulatedAuditSingleInputs().forEach(runBackendStepOutput::addAuditSingleInput);
** auditDetailAccumulator.clear();
** </pre>
*******************************************************************************/
public class AuditDetailAccumulator implements Serializable
{
private static final QLogger LOG = QLogger.getLogger(AuditDetailAccumulator.class);
private static final String objectKey = AuditDetailAccumulator.class.getSimpleName();
private String header;
private Map<TableNameAndPrimaryKey, AuditSingleInput> recordAuditInputMap = new HashMap<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AuditDetailAccumulator(String header)
{
this.header = header;
}
/*******************************************************************************
**
*******************************************************************************/
public void setInContext()
{
QContext.setObject(objectKey, this);
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<AuditDetailAccumulator> getFromContext()
{
return QContext.getObject(objectKey, AuditDetailAccumulator.class);
}
/*******************************************************************************
**
*******************************************************************************/
public void addAuditDetail(String tableName, QRecordEntity entity, String message)
{
if(entity != null)
{
addAuditDetail(tableName, entity.toQRecord(), message);
}
}
/*******************************************************************************
**
*******************************************************************************/
public void addAuditDetail(String tableName, QRecord record, String message)
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(primaryKey == null)
{
LOG.info("Missing primary key in input record - audit detail message will not be recorded.", logPair("message", message));
return;
}
AuditSingleInput auditSingleInput = recordAuditInputMap.computeIfAbsent(new TableNameAndPrimaryKey(tableName, primaryKey), (key) -> new AuditSingleInput(table, record, header));
auditSingleInput.addDetail(message);
}
/*******************************************************************************
**
*******************************************************************************/
public Collection<AuditSingleInput> getAccumulatedAuditSingleInputs()
{
return (recordAuditInputMap.values());
}
/*******************************************************************************
**
*******************************************************************************/
public void clear()
{
recordAuditInputMap.clear();
}
private record TableNameAndPrimaryKey(String tableName, Serializable primaryKey) {}
}

View File

@ -41,7 +41,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
** Input data to insert a single audit record (with optional child record).. ** Input data to insert a single audit record (with optional child record)..
*******************************************************************************/ *******************************************************************************/
public class AuditSingleInput public class AuditSingleInput implements Serializable
{ {
private String auditTableName; private String auditTableName;
private String auditUserName; private String auditUserName;

View File

@ -39,6 +39,7 @@ public class ReplaceInput extends AbstractTableActionInput
private UniqueKey key; private UniqueKey key;
private List<QRecord> records; private List<QRecord> records;
private QQueryFilter filter; private QQueryFilter filter;
private boolean performDeletes = true;
private boolean omitDmlAudit = false; private boolean omitDmlAudit = false;
@ -207,4 +208,35 @@ public class ReplaceInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for performDeletes
*******************************************************************************/
public boolean getPerformDeletes()
{
return (this.performDeletes);
}
/*******************************************************************************
** Setter for performDeletes
*******************************************************************************/
public void setPerformDeletes(boolean performDeletes)
{
this.performDeletes = performDeletes;
}
/*******************************************************************************
** Fluent setter for performDeletes
*******************************************************************************/
public ReplaceInput withPerformDeletes(boolean performDeletes)
{
this.performDeletes = performDeletes;
return (this);
}
} }

View File

@ -53,6 +53,7 @@ public class UpdateInput extends AbstractTableActionInput
private boolean omitTriggeringAutomations = false; private boolean omitTriggeringAutomations = false;
private boolean omitDmlAudit = false; private boolean omitDmlAudit = false;
private boolean omitModifyDateUpdate = false;
private String auditContext = null; private String auditContext = null;
@ -353,4 +354,35 @@ public class UpdateInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for omitModifyDateUpdate
*******************************************************************************/
public boolean getOmitModifyDateUpdate()
{
return (this.omitModifyDateUpdate);
}
/*******************************************************************************
** Setter for omitModifyDateUpdate
*******************************************************************************/
public void setOmitModifyDateUpdate(boolean omitModifyDateUpdate)
{
this.omitModifyDateUpdate = omitModifyDateUpdate;
}
/*******************************************************************************
** Fluent setter for omitModifyDateUpdate
*******************************************************************************/
public UpdateInput withOmitModifyDateUpdate(boolean omitModifyDateUpdate)
{
this.omitModifyDateUpdate = omitModifyDateUpdate;
return (this);
}
} }

View File

@ -34,6 +34,7 @@ import java.util.Map;
public abstract class QWidgetData public abstract class QWidgetData
{ {
private String label; private String label;
private String sublabel;
private String footerHTML; private String footerHTML;
private List<String> dropdownNameList; private List<String> dropdownNameList;
private List<String> dropdownLabelList; private List<String> dropdownLabelList;
@ -51,6 +52,7 @@ public abstract class QWidgetData
private List<List<Serializable>> csvData; private List<List<Serializable>> csvData;
/******************************************************************************* /*******************************************************************************
** Getter for type ** Getter for type
*******************************************************************************/ *******************************************************************************/
@ -356,4 +358,35 @@ public abstract class QWidgetData
return (this); return (this);
} }
/*******************************************************************************
** Getter for sublabel
*******************************************************************************/
public String getSublabel()
{
return (this.sublabel);
}
/*******************************************************************************
** Setter for sublabel
*******************************************************************************/
public void setSublabel(String sublabel)
{
this.sublabel = sublabel;
}
/*******************************************************************************
** Fluent setter for sublabel
*******************************************************************************/
public QWidgetData withSublabel(String sublabel)
{
this.sublabel = sublabel;
return (this);
}
} }

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -65,12 +66,16 @@ public enum DynamicDefaultValueBehavior implements FieldBehavior<DynamicDefaultV
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field, Set<FieldBehavior<?>> behaviorsToOmit)
{ {
if(this.equals(NONE)) if(this.equals(NONE))
{ {
return; return;
} }
if(behaviorsToOmit != null && behaviorsToOmit.contains(this))
{
return;
}
switch(this) switch(this)
{ {

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -49,7 +50,7 @@ public interface FieldBehavior<T extends FieldBehavior<T>>
/******************************************************************************* /*******************************************************************************
** Apply this behavior to a list of records ** Apply this behavior to a list of records
*******************************************************************************/ *******************************************************************************/
void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field); void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field, Set<FieldBehavior<?>> behaviorsToOmit);
/******************************************************************************* /*******************************************************************************
** control if multiple behaviors of this type should be allowed together on a field. ** control if multiple behaviors of this type should be allowed together on a field.

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -65,12 +66,16 @@ public enum ValueTooLongBehavior implements FieldBehavior<ValueTooLongBehavior>
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field, Set<FieldBehavior<?>> behaviorsToOmit)
{ {
if(this.equals(PASS_THROUGH)) if(this.equals(PASS_THROUGH))
{ {
return; return;
} }
if(behaviorsToOmit != null && behaviorsToOmit.contains(this))
{
return;
}
String fieldName = field.getName(); String fieldName = field.getName();
if(!QFieldType.STRING.equals(field.getType())) if(!QFieldType.STRING.equals(field.getType()))

View File

@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -101,20 +102,8 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
@Override @Override
public QProcessMetaData produce(QInstance qInstance) throws QException public QProcessMetaData produce(QInstance qInstance) throws QException
{ {
QProcessMetaData processMetaData = new QProcessMetaData() Function<String, QFrontendStepMetaData> makeReviewOrResultStep = (String name) -> new QFrontendStepMetaData()
.withName(NAME) .withName(name)
.withStepList(List.of(
new QFrontendStepMetaData()
.withName("input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
.withFormField(new QFieldMetaData("minutesOldLimit", QFieldType.INTEGER).withDefaultValue(60)),
new QBackendStepMetaData()
.withName("run")
.withCode(new QCodeReference(getClass())),
new QFrontendStepMetaData()
.withName("output")
.withComponent(new NoCodeWidgetFrontendComponentMetaData() .withComponent(new NoCodeWidgetFrontendComponentMetaData()
.withOutput(new WidgetHtmlLine() .withOutput(new WidgetHtmlLine()
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0)) .withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
@ -131,15 +120,29 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
#end #end
</ul> </ul>
"""))) """)))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM)) .withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
.withViewField(new QFieldMetaData("totalRecordsUpdated", QFieldType.INTEGER) /* todo - didn't display commas... .withDisplayFormat(DisplayFormat.COMMAS) */) .withViewField(new QFieldMetaData("review".equals(name) ? "totalRecordsToUpdate" : "totalRecordsUpdated", QFieldType.INTEGER) /* todo - didn't display commas... .withDisplayFormat(DisplayFormat.COMMAS) */)
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.RECORD_LIST)) .withComponent(new QFrontendComponentMetaData().withType(QComponentType.RECORD_LIST))
.withRecordListField(new QFieldMetaData("tableName", QFieldType.STRING)) .withRecordListField(new QFieldMetaData("tableName", QFieldType.STRING))
.withRecordListField(new QFieldMetaData("badStatus", QFieldType.STRING)) .withRecordListField(new QFieldMetaData("badStatus", QFieldType.STRING))
.withRecordListField(new QFieldMetaData("count", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) /* todo - didn't display commas... */) .withRecordListField(new QFieldMetaData("count", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) /* todo - didn't display commas... */);
QProcessMetaData processMetaData = new QProcessMetaData()
.withName(NAME)
.withStepList(List.of(
new QFrontendStepMetaData()
.withName("input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
.withFormField(new QFieldMetaData("minutesOldLimit", QFieldType.INTEGER).withDefaultValue(60)),
new QBackendStepMetaData()
.withName("preview")
.withCode(new QCodeReference(getClass())),
makeReviewOrResultStep.apply("review"),
new QBackendStepMetaData()
.withName("run")
.withCode(new QCodeReference(getClass())),
makeReviewOrResultStep.apply("result")
)); ));
return (processMetaData); return (processMetaData);
@ -154,6 +157,7 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{ {
int recordsUpdated = 0; int recordsUpdated = 0;
boolean isReview = "preview".equals(runBackendStepInput.getStepName());
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// if a table name is given, validate it, and run for just that table // // if a table name is given, validate it, and run for just that table //
@ -167,7 +171,7 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
throw (new QException("Unrecognized table name: " + tableName)); throw (new QException("Unrecognized table name: " + tableName));
} }
recordsUpdated += processTable(tableName, runBackendStepInput, runBackendStepOutput, warnings); recordsUpdated += processTable(isReview, tableName, runBackendStepInput, runBackendStepOutput, warnings);
} }
else else
{ {
@ -176,11 +180,12 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
for(QTableMetaData table : QContext.getQInstance().getTables().values()) for(QTableMetaData table : QContext.getQInstance().getTables().values())
{ {
recordsUpdated += processTable(table.getName(), runBackendStepInput, runBackendStepOutput, warnings); recordsUpdated += processTable(isReview, table.getName(), runBackendStepInput, runBackendStepOutput, warnings);
} }
} }
runBackendStepOutput.addValue("totalRecordsUpdated", recordsUpdated); runBackendStepOutput.addValue("totalRecordsUpdated", recordsUpdated);
runBackendStepOutput.addValue("totalRecordsToUpdate", recordsUpdated);
runBackendStepOutput.addValue("warnings", warnings); runBackendStepOutput.addValue("warnings", warnings);
runBackendStepOutput.addValue("warningCount", warnings.size()); runBackendStepOutput.addValue("warningCount", warnings.size());
@ -198,7 +203,7 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private int processTable(String tableName, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<String> warnings) private int processTable(boolean isReview, String tableName, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<String> warnings)
{ {
try try
{ {
@ -216,34 +221,42 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
// find the modify-date field on the table // // find the modify-date field on the table //
///////////////////////////////////////////// /////////////////////////////////////////////
String modifyDateFieldName = null; String modifyDateFieldName = null;
String createDateFieldName = null;
for(QFieldMetaData field : table.getFields().values()) for(QFieldMetaData field : table.getFields().values())
{ {
if(DynamicDefaultValueBehavior.MODIFY_DATE.equals(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class))) if(DynamicDefaultValueBehavior.MODIFY_DATE.equals(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)))
{ {
modifyDateFieldName = field.getName(); modifyDateFieldName = field.getName();
break;
} }
} if(DynamicDefaultValueBehavior.CREATE_DATE.equals(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)))
if(modifyDateFieldName == null)
{ {
warnings.add("Could not find a Modify Date field on table: " + tableName); createDateFieldName = field.getName();
LOG.info("Couldn't find a MODIFY_DATE field on table", logPair("tableName", tableName)); }
return 0; }
//////////////////////////////////////////////////////////////////////////////////////////////////
// set up a filter to query for records either FAILED, or RUNNING w/ create/modify date too old //
//////////////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
filter.addSubFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId())));
if(modifyDateFieldName != null)
{
filter.addSubFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.EQUALS, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId()))
.withCriteria(new QFilterCriteria(modifyDateFieldName, QCriteriaOperator.LESS_THAN, NowWithOffset.minus(minutesOldLimit, ChronoUnit.MINUTES))));
}
if(createDateFieldName != null)
{
filter.addSubFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.EQUALS, AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId()))
.withCriteria(new QFilterCriteria(createDateFieldName, QCriteriaOperator.LESS_THAN, NowWithOffset.minus(minutesOldLimit, ChronoUnit.MINUTES))));
} }
////////////////////////////////////////////////////////////////////////
// query for records either FAILED, or RUNNING w/ modify date too old //
////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName); queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR) queryInput.setFilter(filter);
.withSubFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId())))
.withSubFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId()))
.withCriteria(new QFilterCriteria(modifyDateFieldName, QCriteriaOperator.LESS_THAN, NowWithOffset.minus(minutesOldLimit, ChronoUnit.MINUTES))))
);
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -269,7 +282,10 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
} }
} }
if(!recordsToUpdate.isEmpty()) //////////////////////////////////////////////////////////////////////////////////////
// if there are record to update (and this isn't the review step), then update them //
//////////////////////////////////////////////////////////////////////////////////////
if(!recordsToUpdate.isEmpty() && !isReview)
{ {
LOG.info("Healing bad record automation statuses", logPair("tableName", tableName), logPair("count", recordsToUpdate.size())); LOG.info("Healing bad record automation statuses", logPair("tableName", tableName), logPair("count", recordsToUpdate.size()));
new UpdateAction().execute(new UpdateInput(tableName).withRecords(recordsToUpdate).withOmitTriggeringAutomations(true)); new UpdateAction().execute(new UpdateInput(tableName).withRecords(recordsToUpdate).withOmitTriggeringAutomations(true));
@ -278,7 +294,7 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
for(Map.Entry<String, Integer> entry : countByStatus.entrySet()) for(Map.Entry<String, Integer> entry : countByStatus.entrySet())
{ {
runBackendStepOutput.addRecord(new QRecord() runBackendStepOutput.addRecord(new QRecord()
.withValue("tableName", tableName) .withValue("tableName", QContext.getQInstance().getTable(tableName).getLabel())
.withValue("badStatus", entry.getKey()) .withValue("badStatus", entry.getKey())
.withValue("count", entry.getValue())); .withValue("count", entry.getValue()));
} }

View File

@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import org.apache.commons.lang.BooleanUtils;
/******************************************************************************* /*******************************************************************************
@ -41,6 +42,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
public class LoadViaUpdateStep extends AbstractLoadStep public class LoadViaUpdateStep extends AbstractLoadStep
{ {
public static final String FIELD_DESTINATION_TABLE = "destinationTable"; public static final String FIELD_DESTINATION_TABLE = "destinationTable";
public static final String DO_NOT_UPDATE_MODIFY_DATE_FIELD_NAME = "doNotUpdateModifyDateFieldName";
public static final String DO_NOT_TRIGGER_AUTOMATIONS_FIELD_NAME = "doNotTriggerAutomationsFieldName";
@ -67,6 +70,15 @@ public class LoadViaUpdateStep extends AbstractLoadStep
updateInput.setRecords(runBackendStepInput.getRecords()); updateInput.setRecords(runBackendStepInput.getRecords());
getTransaction().ifPresent(updateInput::setTransaction); getTransaction().ifPresent(updateInput::setTransaction);
updateInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback()); updateInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
//////////////////////////////////////////////////////////////////////////////////////////
// look for flags in the input to either not update modify dates or not run automations //
//////////////////////////////////////////////////////////////////////////////////////////
boolean doNotUpdateModifyDate = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean(LoadViaUpdateStep.DO_NOT_UPDATE_MODIFY_DATE_FIELD_NAME));
boolean doNotTriggerAutomations = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean(LoadViaUpdateStep.DO_NOT_TRIGGER_AUTOMATIONS_FIELD_NAME));
updateInput.setOmitModifyDateUpdate(doNotUpdateModifyDate);
updateInput.setOmitTriggeringAutomations(doNotTriggerAutomations);
UpdateOutput updateOutput = new UpdateAction().execute(updateInput); UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
runBackendStepOutput.getRecords().addAll(updateOutput.getRecords()); runBackendStepOutput.getRecords().addAll(updateOutput.getRecords());
} }

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -787,4 +788,59 @@ class UpdateActionTest extends BaseTest
} }
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUpdateDoNotUpdateModifyDate() throws QException
{
QContext.getQSession().withSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, ListBuilder.of(true)));
////////////////////////////////////////////////////
// create a test order and capture its modifyDate //
////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord()));
QRecord qRecord = new InsertAction().execute(insertInput).getRecords().get(0);
Instant initialModifyDate = qRecord.getValueInstant("modifyDate");
assertNotNull(initialModifyDate);
///////////////////////////////////////////////////////////////////
// update the order, the modify date should be in the future now //
///////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
updateInput.setRecords(List.of(qRecord));
QRecord updatedRecord = new UpdateAction().execute(updateInput).getRecords().get(0);
Instant newModifyDate = updatedRecord.getValueInstant("modifyDate");
assertNotNull(initialModifyDate);
assertThat(initialModifyDate).isBefore(newModifyDate);
/////////////////////////////////////////////////////////////////////////////
// set the initial modify date to this modify date for the next test below //
/////////////////////////////////////////////////////////////////////////////
initialModifyDate = newModifyDate;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// now do an update setting flag to not update the modify date, then compare, should be equal //
////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
updateInput.setRecords(List.of(qRecord));
updateInput.setOmitModifyDateUpdate(true);
QRecord updatedRecord = new UpdateAction().execute(updateInput).getRecords().get(0);
Instant newModifyDate = updatedRecord.getValueInstant("modifyDate");
assertNotNull(initialModifyDate);
assertThat(initialModifyDate).isEqualTo(newModifyDate);
}
}
} }

View File

@ -24,10 +24,12 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; 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.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
@ -35,6 +37,7 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -63,7 +66,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", 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") 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(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList, null);
assertEquals("First name", getRecordById(recordList, 1).getValueString("firstName")); assertEquals("First name", getRecordById(recordList, 1).getValueString("firstName"));
assertEquals("Last na...", getRecordById(recordList, 2).getValueString("lastName")); assertEquals("Last na...", getRecordById(recordList, 2).getValueString("lastName"));
@ -73,6 +76,38 @@ class ValueBehaviorApplierTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOmitBehaviors()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
table.getField("firstName").withMaxLength(10).withBehavior(ValueTooLongBehavior.TRUNCATE);
table.getField("lastName").withMaxLength(10).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS);
table.getField("email").withMaxLength(20).withBehavior(ValueTooLongBehavior.ERROR);
List<QRecord> recordList = List.of(
new QRecord().withValue("id", 1).withValue("firstName", "First name too long").withValue("lastName", "Smith").withValue("email", "john@smith.com"),
new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Last name too long").withValue("email", "john@smith.com"),
new QRecord().withValue("id", 3).withValue("firstName", "First name too long").withValue("lastName", "Smith").withValue("email", "john.smith@emaildomainwayytolongtofit.com")
);
Set<FieldBehavior<?>> behaviorsToOmit = Set.of(ValueTooLongBehavior.ERROR);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList, behaviorsToOmit);
///////////////////////////////////////////////////////////////////////////////////////////
// the third error behavior was set to be omitted, so no errors should be on that record //
///////////////////////////////////////////////////////////////////////////////////////////
assertEquals("First name", getRecordById(recordList, 1).getValueString("firstName"));
assertEquals("Last na...", getRecordById(recordList, 2).getValueString("lastName"));
assertEquals("john.smith@emaildomainwayytolongtofit.com", getRecordById(recordList, 3).getValueString("email"));
assertTrue(getRecordById(recordList, 3).getErrors().isEmpty());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -95,7 +130,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", 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") 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(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList, null);
assertEquals("First name too long", getRecordById(recordList, 1).getValueString("firstName")); assertEquals("First name too long", getRecordById(recordList, 1).getValueString("firstName"));
assertNull(getRecordById(recordList, 1).getValueString("lastName")); assertNull(getRecordById(recordList, 1).getValueString("lastName"));

View File

@ -0,0 +1,81 @@
/*
* 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.actions.audits;
import java.util.Collection;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.assertEquals;
/*******************************************************************************
** Unit test for AuditDetailAccumulator
*******************************************************************************/
class AuditDetailAccumulatorTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
AuditDetailAccumulator auditDetailAccumulator = new AuditDetailAccumulator("During test");
auditDetailAccumulator.addAuditDetail(TestUtils.TABLE_NAME_PERSON, new QRecord().withValue("id", 1701), "Something happened");
auditDetailAccumulator.addAuditDetail(TestUtils.TABLE_NAME_PERSON, new QRecord().withValue("id", 1701), "Something else happened");
auditDetailAccumulator.addAuditDetail(TestUtils.TABLE_NAME_PERSON, new QRecord().withValue("id", 74256), "Something happened here too");
auditDetailAccumulator.addAuditDetail(TestUtils.TABLE_NAME_ORDER, new QRecord().withValue("id", 74256), "Something happened to an order");
Collection<AuditSingleInput> auditSingleInputs = auditDetailAccumulator.getAccumulatedAuditSingleInputs();
assertEquals(3, auditSingleInputs.size());
assertThat(auditSingleInputs).anyMatch(asi -> asi.getAuditTableName().equals(TestUtils.TABLE_NAME_PERSON) && asi.getRecordId().equals(1701) && asi.getDetails().size() == 2);
assertThat(auditSingleInputs).anyMatch(asi -> asi.getAuditTableName().equals(TestUtils.TABLE_NAME_PERSON) && asi.getRecordId().equals(74256) && asi.getDetails().size() == 1);
assertThat(auditSingleInputs).anyMatch(asi -> asi.getAuditTableName().equals(TestUtils.TABLE_NAME_ORDER) && asi.getRecordId().equals(74256) && asi.getDetails().size() == 1);
auditDetailAccumulator.clear();;
auditSingleInputs = auditDetailAccumulator.getAccumulatedAuditSingleInputs();
assertEquals(0, auditSingleInputs.size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testContext()
{
AuditDetailAccumulator auditDetailAccumulator = new AuditDetailAccumulator("During test");
auditDetailAccumulator.setInContext();
AuditDetailAccumulator.getFromContext().ifPresent(ada -> ada.addAuditDetail(TestUtils.TABLE_NAME_PERSON, new QRecord().withValue("id", 1701), "Something happened"));
Collection<AuditSingleInput> auditSingleInputs = auditDetailAccumulator.getAccumulatedAuditSingleInputs();
assertEquals(1, auditSingleInputs.size());
assertThat(auditSingleInputs).anyMatch(asi -> asi.getAuditTableName().equals(TestUtils.TABLE_NAME_PERSON) && asi.getRecordId().equals(1701) && asi.getDetails().size() == 1);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -53,7 +54,7 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
QRecord record = new QRecord().withValue("id", 1); QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
assertNotNull(record.getValue("createDate")); assertNotNull(record.getValue("createDate"));
assertNotNull(record.getValue("modifyDate")); assertNotNull(record.getValue("modifyDate"));
@ -71,7 +72,7 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
QRecord record = new QRecord().withValue("id", 1); QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record)); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record), null);
assertNull(record.getValue("createDate")); assertNull(record.getValue("createDate"));
assertNotNull(record.getValue("modifyDate")); assertNotNull(record.getValue("modifyDate"));
@ -79,6 +80,25 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOmitModifyDateUpdate()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
Set<FieldBehavior<?>> behaviorsToOmit = Set.of(DynamicDefaultValueBehavior.MODIFY_DATE);
QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record), behaviorsToOmit);
assertNull(record.getValue("createDate"));
assertNull(record.getValue("modifyDate"));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -92,11 +112,11 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
QRecord record = new QRecord().withValue("id", 1); QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
assertNull(record.getValue("createDate")); assertNull(record.getValue("createDate"));
assertNull(record.getValue("modifyDate")); assertNull(record.getValue("modifyDate"));
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record)); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record), null);
assertNull(record.getValue("createDate")); assertNull(record.getValue("createDate"));
assertNull(record.getValue("modifyDate")); assertNull(record.getValue("modifyDate"));
} }
@ -114,7 +134,7 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
table.getField("createDate").withType(QFieldType.DATE); table.getField("createDate").withType(QFieldType.DATE);
QRecord record = new QRecord().withValue("id", 1); QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
assertNotNull(record.getValue("createDate")); assertNotNull(record.getValue("createDate"));
assertThat(record.getValue("createDate")).isInstanceOf(LocalDate.class); assertThat(record.getValue("createDate")).isInstanceOf(LocalDate.class);
} }
@ -132,7 +152,7 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
table.getField("firstName").withBehavior(DynamicDefaultValueBehavior.CREATE_DATE); table.getField("firstName").withBehavior(DynamicDefaultValueBehavior.CREATE_DATE);
QRecord record = new QRecord().withValue("id", 1); QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record)); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
assertNull(record.getValue("firstName")); assertNull(record.getValue("firstName"));
} }

View File

@ -103,7 +103,7 @@ class HealBadRecordAutomationStatusesProcessStepTest extends BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
void testOldRunning() throws QException void testOldRunningUpdates() throws QException
{ {
///////////////////////////////////////////////// /////////////////////////////////////////////////
// temporarily remove the modify-date behavior // // temporarily remove the modify-date behavior //
@ -160,6 +160,72 @@ class HealBadRecordAutomationStatusesProcessStepTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOldRunningInserts() throws QException
{
///////////////////////////////////////////////////////////////
// temporarily remove the create-date & modify-date behavior //
///////////////////////////////////////////////////////////////
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.NONE);
QContext.getQInstance().getTable(tableName).getField("createDate").withBehavior(DynamicDefaultValueBehavior.NONE);
//////////////////////////////////////////////////////////////////////////
// insert 2 records, one with an old createDate, one with 6 minutes ago //
// but set both with modifyDate very recent //
//////////////////////////////////////////////////////////////////////////
Instant old = Instant.parse("2023-01-01T12:00:00Z");
Instant recent = Instant.now().minus(6, ChronoUnit.MINUTES);
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
new QRecord().withValue("firstName", "Darin").withValue("createDate", old).withValue("modifyDate", recent),
new QRecord().withValue("firstName", "Tim").withValue("createDate", recent).withValue("modifyDate", recent)
)));
List<QRecord> records = queryAllRecords();
///////////////////////////////////////////////////////
// put those records both in status: running-inserts //
///////////////////////////////////////////////////////
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.RUNNING_INSERT_AUTOMATIONS, null);
assertThat(queryAllRecords())
.allMatch(r -> AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
//////////////////////////////////////////////////
// restore the createDate & modifyDate behavior //
//////////////////////////////////////////////////
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
QContext.getQInstance().getTable(tableName).getField("createDate").withBehavior(DynamicDefaultValueBehavior.CREATE_DATE);
/////////////////////////
// run code under test //
/////////////////////////
RunBackendStepOutput output = runProcessStep();
/////////////////////////////////////////////////////////////////////////////////////////////
// assert we updated 1 (the old one) to pending-inserts, the other left as running-inserts //
/////////////////////////////////////////////////////////////////////////////////////////////
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
assertThat(queryAllRecords())
.anyMatch(r -> AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
.anyMatch(r -> AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
/////////////////////////////////
// re-run, with 3-minute limit //
/////////////////////////////////
output = runProcessStep(new RunBackendStepInput().withValues(Map.of("minutesOldLimit", 3)));
/////////////////////////////////////////////////////////////////
// assert that one updated too, and all are now pending-insert //
/////////////////////////////////////////////////////////////////
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
assertThat(queryAllRecords())
.allMatch(r -> AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -362,7 +362,7 @@ public class FilesystemImporterStep implements BackendStep
+ "-" + sourceFileName.replaceAll(".*" + File.separator, ""); + "-" + sourceFileName.replaceAll(".*" + File.separator, "");
path = AbstractBaseFilesystemAction.stripDuplicatedSlashes(path); path = AbstractBaseFilesystemAction.stripDuplicatedSlashes(path);
LOG.info("Archiving file", logPair("path", path)); LOG.info("Archiving file", logPair("path", path), logPair("archiveBackendName", archiveBackend.getName()), logPair("archiveTableName", archiveTable.getName()));
archiveActionBase.writeFile(archiveBackend, path, bytes); archiveActionBase.writeFile(archiveBackend, path, bytes);
return (path); return (path);

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractF
import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException; import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMetaData; import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMetaData;
import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3Utils; import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3Utils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -162,10 +163,19 @@ public class AbstractS3Action extends AbstractBaseFilesystemAction<S3ObjectSumma
@Override @Override
public void writeFile(QBackendMetaData backendMetaData, String path, byte[] contents) throws IOException public void writeFile(QBackendMetaData backendMetaData, String path, byte[] contents) throws IOException
{ {
path = stripLeadingSlash(stripDuplicatedSlashes(path));
String bucketName = ((S3BackendMetaData) backendMetaData).getBucketName(); String bucketName = ((S3BackendMetaData) backendMetaData).getBucketName();
try
{
path = stripLeadingSlash(stripDuplicatedSlashes(path));
getS3Utils().writeFile(bucketName, path, contents); getS3Utils().writeFile(bucketName, path, contents);
} }
catch(Exception e)
{
LOG.warn("Error writing file", e, logPair("path", path), logPair("bucketName", bucketName));
throw (new IOException("Error writing file", e));
}
}