mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
automatic audits
This commit is contained in:
@ -77,9 +77,9 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute to insert 1 audit, with a list of details (child records)
|
||||
** Execute to insert 1 audit, with a list of detail child records
|
||||
*******************************************************************************/
|
||||
public static void execute(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<String> details)
|
||||
public static void execute(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<QRecord> details)
|
||||
{
|
||||
new AuditAction().execute(new AuditInput().withAuditSingleInput(new AuditSingleInput()
|
||||
.withAuditTableName(tableName)
|
||||
@ -105,7 +105,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
/*******************************************************************************
|
||||
** Add 1 auditSingleInput to an AuditInput object - with a list of details (child records).
|
||||
*******************************************************************************/
|
||||
public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<String> details)
|
||||
public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<QRecord> details)
|
||||
{
|
||||
if(auditInput == null)
|
||||
{
|
||||
@ -198,7 +198,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
//////////////////////////////////////////
|
||||
// now look for children (auditDetails) //
|
||||
//////////////////////////////////////////
|
||||
int i = 0;
|
||||
int i = 0;
|
||||
List<QRecord> auditDetailRecords = new ArrayList<>();
|
||||
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
|
||||
{
|
||||
@ -209,12 +209,9 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
continue;
|
||||
}
|
||||
|
||||
for(String detail : CollectionUtils.nonNullList(auditSingleInput.getDetails()))
|
||||
for(QRecord detail : CollectionUtils.nonNullList(auditSingleInput.getDetails()))
|
||||
{
|
||||
auditDetailRecords.add(new QRecord()
|
||||
.withValue("auditId", auditId)
|
||||
.withValue("message", detail)
|
||||
);
|
||||
auditDetailRecords.add(detail.withValue("auditId", auditId));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,381 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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.actions.audits;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||
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.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Audit for a standard DML (Data Manipulation Language) activity - e.g.,
|
||||
** insert, edit, or delete.
|
||||
*******************************************************************************/
|
||||
public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAuditOutput>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(DMLAuditAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public DMLAuditOutput execute(DMLAuditInput input) throws QException
|
||||
{
|
||||
DMLAuditOutput output = new DMLAuditOutput();
|
||||
AbstractTableActionInput tableActionInput = input.getTableActionInput();
|
||||
List<QRecord> recordList = input.getRecordList();
|
||||
List<QRecord> oldRecordList = input.getOldRecordList();
|
||||
QTableMetaData table = tableActionInput.getTable();
|
||||
long start = System.currentTimeMillis();
|
||||
DMLType dmlType = getDMLType(tableActionInput);
|
||||
|
||||
try
|
||||
{
|
||||
AuditLevel auditLevel = getAuditLevel(tableActionInput);
|
||||
if(auditLevel == null || auditLevel.equals(AuditLevel.NONE))
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// return with noop for null or level NONE //
|
||||
/////////////////////////////////////////////
|
||||
return (output);
|
||||
}
|
||||
|
||||
String contextSuffix = "";
|
||||
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
|
||||
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
|
||||
{
|
||||
String processName = runProcessInput.getProcessName();
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process != null)
|
||||
{
|
||||
contextSuffix = " during process: " + process.getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
AuditInput auditInput = new AuditInput();
|
||||
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make many simple audits (no details) for RECORD level //
|
||||
// or for FIELD level, but on a DML type that doesn't support field-level details (e.g., DELETE or OTHER) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : recordList)
|
||||
{
|
||||
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix);
|
||||
}
|
||||
}
|
||||
else if(auditLevel.equals(AuditLevel.FIELD))
|
||||
{
|
||||
Map<Serializable, QRecord> oldRecordMap = buildOldRecordMap(table, oldRecordList);
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// do many audits, all with field level details, for FIELD level //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
|
||||
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
|
||||
|
||||
//////////////////////////////////////////
|
||||
// sort the field names by their labels //
|
||||
//////////////////////////////////////////
|
||||
List<String> sortedFieldNames = table.getFields().keySet().stream()
|
||||
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
|
||||
.toList();
|
||||
|
||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// build single audit input for each record //
|
||||
//////////////////////////////////////////////
|
||||
for(QRecord record : recordList)
|
||||
{
|
||||
QRecord oldRecord = oldRecordMap.get(ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(primaryKeyField.getName())));
|
||||
|
||||
List<QRecord> details = new ArrayList<>();
|
||||
for(String fieldName : sortedFieldNames)
|
||||
{
|
||||
if(!record.getValues().containsKey(fieldName))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the stored record doesn't have this field name, then don't audit anything about it //
|
||||
// this is to deal with our Patch style updates not looking like every field was cleared out. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QFieldMetaData field = table.getField(fieldName);
|
||||
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
|
||||
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
|
||||
QRecord detailRecord = null;
|
||||
|
||||
if(oldRecord == null)
|
||||
{
|
||||
if(DMLType.INSERT.equals(dmlType) && value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!Objects.equals(oldValue, value))
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
|
||||
|
||||
if(oldValue == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
else if(value == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
|
||||
detailRecord.withValue("oldValue", formattedOldValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
|
||||
detailRecord.withValue("oldValue", formattedOldValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(detailRecord != null)
|
||||
{
|
||||
detailRecord.withValue("fieldName", fieldName);
|
||||
details.add(detailRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if(details.isEmpty())
|
||||
{
|
||||
details.add(new QRecord().withValue("message", "No fields values were changed."));
|
||||
}
|
||||
|
||||
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
|
||||
}
|
||||
}
|
||||
|
||||
// new AuditAction().executeAsync(auditInput); // todo async??? maybe get that from rules???
|
||||
new AuditAction().execute(auditInput);
|
||||
long end = System.currentTimeMillis();
|
||||
LOG.debug("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error performing DML audit", e, logPair("type", String.valueOf(dmlType)), logPair("table", table.getName()));
|
||||
}
|
||||
|
||||
return (output);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value)
|
||||
{
|
||||
String formattedValue = null;
|
||||
if(value != null)
|
||||
{
|
||||
if(field.getType().equals(QFieldType.DATE_TIME) && value instanceof Instant instant)
|
||||
{
|
||||
formattedValue = QValueFormatter.formatDateTimeWithZone(instant.atZone(ZoneId.of(Objects.requireNonNullElse(QContext.getQInstance().getDefaultTimeZoneId(), "UTC"))));
|
||||
}
|
||||
else if(record.getDisplayValue(fieldName) != null)
|
||||
{
|
||||
formattedValue = record.getDisplayValue(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedValue = QValueFormatter.formatValue(field, value);
|
||||
}
|
||||
}
|
||||
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String formatFormattedValueForDetailMessage(QFieldMetaData field, String formattedValue)
|
||||
{
|
||||
if(formattedValue == null || "null".equals(formattedValue))
|
||||
{
|
||||
formattedValue = "--";
|
||||
}
|
||||
else
|
||||
{
|
||||
if(QFieldType.STRING.equals(field.getType()) || field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
formattedValue = '"' + formattedValue + '"';
|
||||
}
|
||||
}
|
||||
|
||||
return (formattedValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Map<Serializable, QRecord> buildOldRecordMap(QTableMetaData table, List<QRecord> oldRecordList)
|
||||
{
|
||||
Map<Serializable, QRecord> rs = new HashMap<>();
|
||||
for(QRecord record : CollectionUtils.nonNullList(oldRecordList))
|
||||
{
|
||||
rs.put(record.getValue(table.getPrimaryKeyField()), record);
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private DMLType getDMLType(AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
if(tableActionInput instanceof InsertInput)
|
||||
{
|
||||
return DMLType.INSERT;
|
||||
}
|
||||
else if(tableActionInput instanceof UpdateInput)
|
||||
{
|
||||
return DMLType.UPDATE;
|
||||
}
|
||||
else if(tableActionInput instanceof DeleteInput)
|
||||
{
|
||||
return DMLType.DELETE;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DMLType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
|
||||
{
|
||||
Map<String, Serializable> securityKeyValues = new HashMap<>();
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
|
||||
}
|
||||
return securityKeyValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static AuditLevel getAuditLevel(AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
QTableMetaData table = tableActionInput.getTable();
|
||||
if(table.getAuditRules() == null)
|
||||
{
|
||||
return (AuditLevel.NONE);
|
||||
}
|
||||
|
||||
return (table.getAuditRules().getAuditLevel());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private enum DMLType
|
||||
{
|
||||
INSERT("Inserted", true),
|
||||
UPDATE("Edited", true),
|
||||
DELETE("Deleted", false),
|
||||
OTHER("Processed", false);
|
||||
|
||||
private final String pastTenseVerb;
|
||||
private final boolean supportsFields;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
DMLType(String pastTenseVerb, boolean supportsFields)
|
||||
{
|
||||
this.pastTenseVerb = pastTenseVerb;
|
||||
this.supportsFields = supportsFields;
|
||||
}
|
||||
}
|
||||
}
|
@ -63,6 +63,18 @@ public class NoCodeWidgetVelocityUtils
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String helpIcon()
|
||||
{
|
||||
return ("""
|
||||
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: blue; position: relative; top: 6px;" aria-hidden="true">help_outline</span>
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -26,13 +26,17 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||
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.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -81,10 +85,44 @@ public class DeleteAction
|
||||
}
|
||||
}
|
||||
|
||||
DeleteOutput deleteResult = deleteInterface.execute(deleteInput);
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
|
||||
|
||||
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
return deleteResult;
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
|
||||
|
||||
return deleteOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> getRecordListForAuditIfNeeded(DeleteInput deleteInput) throws QException
|
||||
{
|
||||
List<QRecord> recordListForAudit = null;
|
||||
|
||||
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(deleteInput);
|
||||
if(AuditLevel.RECORD.equals(auditLevel) || AuditLevel.FIELD.equals(auditLevel))
|
||||
{
|
||||
List<Serializable> primaryKeyList = deleteInput.getPrimaryKeys();
|
||||
if(CollectionUtils.nullSafeIsEmpty(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
||||
{
|
||||
primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
|
||||
}
|
||||
|
||||
if(primaryKeyList != null)
|
||||
{
|
||||
recordListForAudit = primaryKeyList.stream().map(pk -> new QRecord().withValue(deleteInput.getTable().getPrimaryKeyField(), pk)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
return (recordListForAudit);
|
||||
}
|
||||
|
||||
|
||||
|
@ -32,6 +32,7 @@ import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
|
||||
@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -84,6 +86,8 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
|
||||
|
||||
if(postInsertCustomizer.isPresent())
|
||||
{
|
||||
postInsertCustomizer.get().setInsertInput(insertInput);
|
||||
@ -94,7 +98,6 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -22,15 +22,28 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
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.actions.tables.update.UpdateInput;
|
||||
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.metadata.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -39,6 +52,10 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
*******************************************************************************/
|
||||
public class UpdateAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(UpdateAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -50,16 +67,54 @@ public class UpdateAction
|
||||
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords());
|
||||
// todo - need to handle records with errors coming out of here...
|
||||
|
||||
List<QRecord> oldRecordList = getOldRecordListForAuditIfNeeded(updateInput);
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
|
||||
// todo pre-customization - just get to modify the request?
|
||||
UpdateOutput updateResult = qModule.getUpdateInterface().execute(updateInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withRecordList(updateResult.getRecords()).withOldRecordList(oldRecordList));
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> getOldRecordListForAuditIfNeeded(UpdateInput updateInput)
|
||||
{
|
||||
try
|
||||
{
|
||||
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(updateInput);
|
||||
List<QRecord> oldRecordList = null;
|
||||
if(AuditLevel.FIELD.equals(auditLevel))
|
||||
{
|
||||
String primaryKeyField = updateInput.getTable().getPrimaryKeyField();
|
||||
List<Serializable> pkeysBeingUpdated = updateInput.getRecords().stream().map(r -> r.getValue(primaryKeyField)).toList();
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(updateInput.getTableName());
|
||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, pkeysBeingUpdated)));
|
||||
// todo - need a limit? what if too many??
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
oldRecordList = queryOutput.getRecords();
|
||||
}
|
||||
return oldRecordList;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error getting old record list for audit", e, logPair("table", updateInput.getTableName()));
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** If the table being updated uses an automation-status field, populate it now.
|
||||
*******************************************************************************/
|
||||
|
@ -47,10 +47,10 @@ public class QValueFormatter
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class);
|
||||
|
||||
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a");
|
||||
private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a z");
|
||||
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a");
|
||||
private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a z");
|
||||
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static DateTimeFormatter localTimeFormatter = DateTimeFormatter.ofPattern("h:mm a");
|
||||
private static DateTimeFormatter localTimeFormatter = DateTimeFormatter.ofPattern("h:mm:ss a");
|
||||
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.context;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
@ -243,4 +244,19 @@ public class QContext
|
||||
{
|
||||
qBackendTransactionThreadLocal.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<AbstractActionInput> getFirstActionInStack()
|
||||
{
|
||||
if(actionStackThreadLocal.get() == null || actionStackThreadLocal.get().isEmpty())
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
return (Optional.of(actionStackThreadLocal.get().get(0)));
|
||||
}
|
||||
}
|
||||
|
@ -197,6 +197,20 @@ public class QInstanceEnricher
|
||||
}
|
||||
|
||||
enrichPermissionRules(table);
|
||||
enrichAuditRules(table);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void enrichAuditRules(QTableMetaData table)
|
||||
{
|
||||
if(table.getAuditRules() == null && qInstance.getDefaultAuditRules() != null)
|
||||
{
|
||||
table.setAuditRules(qInstance.getDefaultAuditRules());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class AuditSingleInput
|
||||
|
||||
private Map<String, Serializable> securityKeyValues;
|
||||
|
||||
private List<String> details;
|
||||
private List<QRecord> details;
|
||||
|
||||
|
||||
|
||||
@ -254,10 +254,12 @@ public class AuditSingleInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for details
|
||||
*******************************************************************************/
|
||||
public List<String> getDetails()
|
||||
public List<QRecord> getDetails()
|
||||
{
|
||||
return (this.details);
|
||||
}
|
||||
@ -267,7 +269,7 @@ public class AuditSingleInput
|
||||
/*******************************************************************************
|
||||
** Setter for details
|
||||
*******************************************************************************/
|
||||
public void setDetails(List<String> details)
|
||||
public void setDetails(List<QRecord> details)
|
||||
{
|
||||
this.details = details;
|
||||
}
|
||||
@ -277,21 +279,24 @@ public class AuditSingleInput
|
||||
/*******************************************************************************
|
||||
** Fluent setter for details
|
||||
*******************************************************************************/
|
||||
public AuditSingleInput withDetails(List<String> details)
|
||||
public AuditSingleInput withDetails(List<QRecord> details)
|
||||
{
|
||||
this.details = details;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addDetail(String detail)
|
||||
public void addDetail(String message)
|
||||
{
|
||||
if(this.details == null)
|
||||
{
|
||||
this.details = new ArrayList<>();
|
||||
}
|
||||
QRecord detail = new QRecord().withValue("message", message);
|
||||
this.details.add(detail);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input object for the DML audit action.
|
||||
*******************************************************************************/
|
||||
public class DMLAuditInput extends AbstractActionInput implements Serializable
|
||||
{
|
||||
private List<QRecord> recordList;
|
||||
private List<QRecord> oldRecordList;
|
||||
private AbstractTableActionInput tableActionInput;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for recordList
|
||||
*******************************************************************************/
|
||||
public List<QRecord> getRecordList()
|
||||
{
|
||||
return (this.recordList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for recordList
|
||||
*******************************************************************************/
|
||||
public void setRecordList(List<QRecord> recordList)
|
||||
{
|
||||
this.recordList = recordList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for recordList
|
||||
*******************************************************************************/
|
||||
public DMLAuditInput withRecordList(List<QRecord> recordList)
|
||||
{
|
||||
this.recordList = recordList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableActionInput
|
||||
*******************************************************************************/
|
||||
public AbstractTableActionInput getTableActionInput()
|
||||
{
|
||||
return (this.tableActionInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tableActionInput
|
||||
*******************************************************************************/
|
||||
public void setTableActionInput(AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
this.tableActionInput = tableActionInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableActionInput
|
||||
*******************************************************************************/
|
||||
public DMLAuditInput withTableActionInput(AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
this.tableActionInput = tableActionInput;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for oldRecordList
|
||||
*******************************************************************************/
|
||||
public List<QRecord> getOldRecordList()
|
||||
{
|
||||
return (this.oldRecordList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for oldRecordList
|
||||
*******************************************************************************/
|
||||
public void setOldRecordList(List<QRecord> oldRecordList)
|
||||
{
|
||||
this.oldRecordList = oldRecordList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for oldRecordList
|
||||
*******************************************************************************/
|
||||
public DMLAuditInput withOldRecordList(List<QRecord> oldRecordList)
|
||||
{
|
||||
this.oldRecordList = oldRecordList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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 com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Output object for the DML audit action.
|
||||
*******************************************************************************/
|
||||
public class DMLAuditOutput extends AbstractActionOutput implements Serializable
|
||||
{
|
||||
|
||||
}
|
@ -27,6 +27,8 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
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;
|
||||
@ -165,6 +167,7 @@ public class AuditsMetaDataProvider
|
||||
return new QTableMetaData()
|
||||
.withName(TABLE_NAME_AUDIT_TABLE)
|
||||
.withBackendName(backendName)
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("label")
|
||||
.withPrimaryKeyField("id")
|
||||
@ -186,6 +189,7 @@ public class AuditsMetaDataProvider
|
||||
return new QTableMetaData()
|
||||
.withName(TABLE_NAME_AUDIT_USER)
|
||||
.withBackendName(backendName)
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withPrimaryKeyField("id")
|
||||
@ -206,6 +210,7 @@ public class AuditsMetaDataProvider
|
||||
return new QTableMetaData()
|
||||
.withName(TABLE_NAME_AUDIT)
|
||||
.withBackendName(backendName)
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||
.withRecordLabelFormat("%s %s")
|
||||
.withRecordLabelFields("auditTableId", "recordId")
|
||||
.withPrimaryKeyField("id")
|
||||
@ -227,12 +232,16 @@ public class AuditsMetaDataProvider
|
||||
return new QTableMetaData()
|
||||
.withName(TABLE_NAME_AUDIT_DETAIL)
|
||||
.withBackendName(backendName)
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("id")
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("auditId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
|
||||
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS));
|
||||
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
|
||||
.withField(new QFieldMetaData("fieldName", QFieldType.STRING).withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
|
||||
.withField(new QFieldMetaData("oldValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
|
||||
.withField(new QFieldMetaData("newValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
|
||||
@ -93,6 +94,7 @@ public class QInstance
|
||||
private String defaultTimeZoneId = "UTC";
|
||||
|
||||
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
|
||||
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
|
||||
|
||||
// todo - lock down the object (no more changes allowed) after it's been validated?
|
||||
|
||||
@ -1042,4 +1044,35 @@ public class QInstance
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultAuditRules
|
||||
*******************************************************************************/
|
||||
public QAuditRules getDefaultAuditRules()
|
||||
{
|
||||
return (this.defaultAuditRules);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for defaultAuditRules
|
||||
*******************************************************************************/
|
||||
public void setDefaultAuditRules(QAuditRules defaultAuditRules)
|
||||
{
|
||||
this.defaultAuditRules = defaultAuditRules;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for defaultAuditRules
|
||||
*******************************************************************************/
|
||||
public QInstance withDefaultAuditRules(QAuditRules defaultAuditRules)
|
||||
{
|
||||
this.defaultAuditRules = defaultAuditRules;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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.audits;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum AuditLevel
|
||||
{
|
||||
NONE,
|
||||
RECORD,
|
||||
FIELD
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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.audits;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QAuditRules
|
||||
{
|
||||
private AuditLevel auditLevel;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QAuditRules defaultInstanceLevelNone()
|
||||
{
|
||||
return (new QAuditRules()
|
||||
.withAuditLevel(AuditLevel.NONE));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for auditLevel
|
||||
*******************************************************************************/
|
||||
public AuditLevel getAuditLevel()
|
||||
{
|
||||
return (this.auditLevel);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for auditLevel
|
||||
*******************************************************************************/
|
||||
public void setAuditLevel(AuditLevel auditLevel)
|
||||
{
|
||||
this.auditLevel = auditLevel;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for auditLevel
|
||||
*******************************************************************************/
|
||||
public QAuditRules withAuditLevel(AuditLevel auditLevel)
|
||||
{
|
||||
this.auditLevel = auditLevel;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -37,6 +37,7 @@ public class HtmlWrapper
|
||||
public static final HtmlWrapper BIG_CENTERED = new HtmlWrapper("<div style='font-size: 2rem; font-weight: 400; line-height: 1.625; text-align: center; padding-bottom: 8px;'>", "</div>");
|
||||
public static final HtmlWrapper INDENT_1 = new HtmlWrapper("<div style='padding-left: 1rem;'>", "</div>");
|
||||
public static final HtmlWrapper INDENT_2 = new HtmlWrapper("<div style='padding-left: 2rem;'>", "</div>");
|
||||
public static final HtmlWrapper FLOAT_RIGHT = new HtmlWrapper("<div style='float: right'>", "</div>");
|
||||
public static final HtmlWrapper RULE_ABOVE = new HtmlWrapper("""
|
||||
<hr style="opacity: 0.25; height: 0.0625rem; border-width: 0; margin-bottom: 1rem; background-image: linear-gradient(to right, rgba(52, 71, 103, 0), rgba(52, 71, 103, 0.4), rgba(52, 71, 103, 0));" />
|
||||
""", "");
|
||||
|
@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
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.layout.QAppChildMetaData;
|
||||
@ -74,6 +75,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
|
||||
|
||||
private List<RecordSecurityLock> recordSecurityLocks;
|
||||
private QPermissionRules permissionRules;
|
||||
private QAuditRules auditRules;
|
||||
|
||||
private QTableBackendDetails backendDetails;
|
||||
private QTableAutomationDetails automationDetails;
|
||||
@ -1142,4 +1144,35 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for auditRules
|
||||
*******************************************************************************/
|
||||
public QAuditRules getAuditRules()
|
||||
{
|
||||
return (this.auditRules);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for auditRules
|
||||
*******************************************************************************/
|
||||
public void setAuditRules(QAuditRules auditRules)
|
||||
{
|
||||
this.auditRules = auditRules;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for auditRules
|
||||
*******************************************************************************/
|
||||
public QTableMetaData withAuditRules(QAuditRules auditRules)
|
||||
{
|
||||
this.auditRules = auditRules;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -165,9 +165,9 @@ class AuditActionTest extends BaseTest
|
||||
Integer recordId2 = 1702;
|
||||
Integer recordId3 = 1703;
|
||||
AuditInput auditInput = new AuditInput();
|
||||
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId1, Map.of(), "Test Audit", List.of("Detail1", "Detail2"));
|
||||
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId1, Map.of(), "Test Audit", List.of(new QRecord().withValue("message", "Detail1"), new QRecord().withValue("message", "Detail2")));
|
||||
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_ORDER, recordId2, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 47), "Test Another Audit", null);
|
||||
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId3, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 42), "Audit 3", List.of("Detail3"));
|
||||
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId3, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 42), "Audit 3", List.of(new QRecord().withValue("message", "Detail3")));
|
||||
new AuditAction().execute(auditInput);
|
||||
|
||||
/////////////////////////////////////
|
||||
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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.actions.audits;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
|
||||
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.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for DMLAuditAction
|
||||
*******************************************************************************/
|
||||
class DMLAuditActionTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
|
||||
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
List<QRecord> recordList = List.of(new QRecord().withValue("id", 1).withValue("firstName", "Darin").withValue("noOfShoes", 5).withValue("favoriteShapeId", null).withValue("createDate", Instant.now()).withValue("modifyDate", Instant.now()));
|
||||
List<QRecord> oldRecordList = List.of(new QRecord().withValue("id", 1).withValue("firstName", "Tim").withValue("noOfShoes", null).withValue("favoriteShapeId", 1).withValue("lastName", "Simpson"));
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// set audit rules null - confirm no audits are built //
|
||||
////////////////////////////////////////////////////////
|
||||
{
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(null);
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordList));
|
||||
List<QRecord> auditList = TestUtils.queryTable("audit");
|
||||
assertTrue(auditList.isEmpty());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// set audit level NONE - confirm no audits are built //
|
||||
////////////////////////////////////////////////////////
|
||||
{
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE));
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordList));
|
||||
List<QRecord> auditList = TestUtils.queryTable("audit");
|
||||
assertTrue(auditList.isEmpty());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// set audit level RECORD - confirm only header, no detail //
|
||||
/////////////////////////////////////////////////////////////
|
||||
{
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.RECORD));
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(recordList));
|
||||
List<QRecord> auditList = TestUtils.queryTable("audit");
|
||||
assertEquals(1, auditList.size());
|
||||
assertEquals("Record was Inserted", auditList.get(0).getValueString("message"));
|
||||
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
|
||||
assertTrue(auditDetailList.isEmpty());
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// set audit level FIELD - confirm header, and detail //
|
||||
////////////////////////////////////////////////////////
|
||||
{
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withRecordList(recordList));
|
||||
List<QRecord> auditList = TestUtils.queryTable("audit");
|
||||
assertEquals(1, auditList.size());
|
||||
assertEquals("Record was Edited", auditList.get(0).getValueString("message"));
|
||||
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// since we didn't provide old-records, there should be a detail for every field in the updated record - OTHER THAN createDate and modifyDate //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(recordList.get(0).getValues().size() - 2, auditDetailList.size());
|
||||
assertTrue(auditDetailList.stream().allMatch(r -> r.getValueString("message").matches("Set.*to.*")));
|
||||
assertTrue(auditDetailList.stream().allMatch(r -> r.getValueString("fieldName") != null));
|
||||
assertTrue(auditDetailList.stream().allMatch(r -> r.getValueString("oldValue") == null));
|
||||
assertTrue(auditDetailList.stream().anyMatch(r -> r.getValueString("newValue") != null));
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// this time supply old-records to the edit //
|
||||
//////////////////////////////////////////////
|
||||
{
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_SHAPE).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE));
|
||||
TestUtils.insertDefaultShapes(qInstance);
|
||||
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withOldRecordList(oldRecordList).withRecordList(recordList));
|
||||
List<QRecord> auditList = TestUtils.queryTable("audit");
|
||||
assertEquals(1, auditList.size());
|
||||
assertEquals("Record was Edited", auditList.get(0).getValueString("message"));
|
||||
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
|
||||
|
||||
assertEquals(3, auditDetailList.size());
|
||||
|
||||
assertEquals("Removed \"Triangle\" from Favorite Shape", auditDetailList.get(0).getValueString("message"));
|
||||
assertEquals("favoriteShapeId", auditDetailList.get(0).getValueString("fieldName"));
|
||||
assertEquals("Triangle", auditDetailList.get(0).getValueString("oldValue"));
|
||||
assertNull(auditDetailList.get(0).getValueString("newValue"));
|
||||
|
||||
assertEquals("Changed First Name from \"Tim\" to \"Darin\"", auditDetailList.get(1).getValueString("message"));
|
||||
assertEquals("firstName", auditDetailList.get(1).getValueString("fieldName"));
|
||||
assertEquals("Tim", auditDetailList.get(1).getValueString("oldValue"));
|
||||
assertEquals("Darin", auditDetailList.get(1).getValueString("newValue"));
|
||||
|
||||
assertEquals("Set No Of Shoes to 5", auditDetailList.get(2).getValueString("message"));
|
||||
assertEquals("noOfShoes", auditDetailList.get(2).getValueString("fieldName"));
|
||||
assertNull(auditDetailList.get(2).getValueString("oldValue"));
|
||||
assertEquals("5", auditDetailList.get(2).getValueString("newValue"));
|
||||
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// confirm we don't log null fields on inserts //
|
||||
/////////////////////////////////////////////////
|
||||
{
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(recordList));
|
||||
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
|
||||
assertFalse(auditDetailList.isEmpty());
|
||||
assertTrue(auditDetailList.stream().noneMatch(r -> r.getValueString("message").contains("Favorite Shape")));
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -24,11 +24,21 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
|
||||
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.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@ -79,4 +89,64 @@ class DeleteActionTest extends BaseTest
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAuditsByPrimaryKey() throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("id", 1),
|
||||
new QRecord().withValue("id", 2)));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.RECORD));
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
deleteInput.setPrimaryKeys(List.of(1, 2));
|
||||
new DeleteAction().execute(deleteInput);
|
||||
|
||||
List<QRecord> audits = TestUtils.queryTable("audit");
|
||||
assertEquals(2, audits.size());
|
||||
assertTrue(audits.stream().allMatch(r -> r.getValueString("message").equals("Record was Deleted")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAuditsByFilter() throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("id", 1),
|
||||
new QRecord().withValue("id", 2)));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.RECORD));
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, 1, 2)));
|
||||
new DeleteAction().execute(deleteInput);
|
||||
|
||||
List<QRecord> audits = TestUtils.queryTable("audit");
|
||||
assertEquals(2, audits.size());
|
||||
assertTrue(audits.stream().allMatch(r -> r.getValueString("message").equals("Record was Deleted")));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -83,8 +83,8 @@ class QValueFormatterTest extends BaseTest
|
||||
assertEquals("No", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), false));
|
||||
|
||||
assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), null));
|
||||
assertEquals("5:00 AM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(5, 0)));
|
||||
assertEquals("5:00 PM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(17, 0)));
|
||||
assertEquals("5:00:00 AM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(5, 0)));
|
||||
assertEquals("5:00:47 PM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(17, 0, 47)));
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// this one flows through the exceptional cases //
|
||||
@ -196,8 +196,8 @@ class QValueFormatterTest extends BaseTest
|
||||
void testFormatDates()
|
||||
{
|
||||
assertEquals("2023-02-01", QValueFormatter.formatDate(LocalDate.of(2023, Month.FEBRUARY, 1)));
|
||||
assertEquals("2023-02-01 07:15 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15)));
|
||||
assertEquals("2023-02-01 07:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central"))));
|
||||
assertEquals("2023-02-01 07:15:00 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15, 0)));
|
||||
assertEquals("2023-02-01 07:15:47 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15, 47), ZoneId.of("US/Central"))));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user