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()
|
new AuditAction().execute(new AuditInput().withAuditSingleInput(new AuditSingleInput()
|
||||||
.withAuditTableName(tableName)
|
.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).
|
** 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)
|
if(auditInput == null)
|
||||||
{
|
{
|
||||||
@ -198,7 +198,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
|||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
// now look for children (auditDetails) //
|
// now look for children (auditDetails) //
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
int i = 0;
|
int i = 0;
|
||||||
List<QRecord> auditDetailRecords = new ArrayList<>();
|
List<QRecord> auditDetailRecords = new ArrayList<>();
|
||||||
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
|
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
|
||||||
{
|
{
|
||||||
@ -209,12 +209,9 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(String detail : CollectionUtils.nonNullList(auditSingleInput.getDetails()))
|
for(QRecord detail : CollectionUtils.nonNullList(auditSingleInput.getDetails()))
|
||||||
{
|
{
|
||||||
auditDetailRecords.add(new QRecord()
|
auditDetailRecords.add(detail.withValue("auditId", auditId));
|
||||||
.withValue("auditId", auditId)
|
|
||||||
.withValue("message", detail)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
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.actions.interfaces.DeleteInterface;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
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.DeleteInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
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.QueryInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
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.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;
|
||||||
@ -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
|
// 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.AbstractQActionFunction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
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.AutomationStatus;
|
||||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
|
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.actions.values.ValueBehaviorApplier;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
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.InsertInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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);
|
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
|
||||||
// todo post-customization - can do whatever w/ the result if you want
|
// 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())
|
if(postInsertCustomizer.isPresent())
|
||||||
{
|
{
|
||||||
postInsertCustomizer.get().setInsertInput(insertInput);
|
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;
|
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.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.AutomationStatus;
|
||||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
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.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.metadata.audits.AuditLevel;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
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 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
|
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());
|
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords());
|
||||||
// todo - need to handle records with errors coming out of here...
|
// todo - need to handle records with errors coming out of here...
|
||||||
|
|
||||||
|
List<QRecord> oldRecordList = getOldRecordListForAuditIfNeeded(updateInput);
|
||||||
|
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
|
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
|
||||||
// todo pre-customization - just get to modify the request?
|
// todo pre-customization - just get to modify the request?
|
||||||
UpdateOutput updateResult = qModule.getUpdateInterface().execute(updateInput);
|
UpdateOutput updateResult = qModule.getUpdateInterface().execute(updateInput);
|
||||||
// todo post-customization - can do whatever w/ the result if you want
|
// 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;
|
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.
|
** 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 final QLogger LOG = QLogger.getLogger(QValueFormatter.class);
|
||||||
|
|
||||||
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a");
|
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a");
|
||||||
private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a z");
|
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 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;
|
package com.kingsrook.qqq.backend.core.context;
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||||
@ -243,4 +244,19 @@ public class QContext
|
|||||||
{
|
{
|
||||||
qBackendTransactionThreadLocal.remove();
|
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);
|
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 Map<String, Serializable> securityKeyValues;
|
||||||
|
|
||||||
private List<String> details;
|
private List<QRecord> details;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -254,10 +254,12 @@ public class AuditSingleInput
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for details
|
** Getter for details
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public List<String> getDetails()
|
public List<QRecord> getDetails()
|
||||||
{
|
{
|
||||||
return (this.details);
|
return (this.details);
|
||||||
}
|
}
|
||||||
@ -267,7 +269,7 @@ public class AuditSingleInput
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Setter for details
|
** Setter for details
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void setDetails(List<String> details)
|
public void setDetails(List<QRecord> details)
|
||||||
{
|
{
|
||||||
this.details = details;
|
this.details = details;
|
||||||
}
|
}
|
||||||
@ -277,21 +279,24 @@ public class AuditSingleInput
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Fluent setter for details
|
** Fluent setter for details
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public AuditSingleInput withDetails(List<String> details)
|
public AuditSingleInput withDetails(List<QRecord> details)
|
||||||
{
|
{
|
||||||
this.details = details;
|
this.details = details;
|
||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void addDetail(String detail)
|
public void addDetail(String message)
|
||||||
{
|
{
|
||||||
if(this.details == null)
|
if(this.details == null)
|
||||||
{
|
{
|
||||||
this.details = new ArrayList<>();
|
this.details = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
QRecord detail = new QRecord().withValue("message", message);
|
||||||
this.details.add(detail);
|
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 java.util.function.Consumer;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
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.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.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.fields.ValueTooLongBehavior;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||||
@ -165,6 +167,7 @@ public class AuditsMetaDataProvider
|
|||||||
return new QTableMetaData()
|
return new QTableMetaData()
|
||||||
.withName(TABLE_NAME_AUDIT_TABLE)
|
.withName(TABLE_NAME_AUDIT_TABLE)
|
||||||
.withBackendName(backendName)
|
.withBackendName(backendName)
|
||||||
|
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||||
.withRecordLabelFormat("%s")
|
.withRecordLabelFormat("%s")
|
||||||
.withRecordLabelFields("label")
|
.withRecordLabelFields("label")
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
@ -186,6 +189,7 @@ public class AuditsMetaDataProvider
|
|||||||
return new QTableMetaData()
|
return new QTableMetaData()
|
||||||
.withName(TABLE_NAME_AUDIT_USER)
|
.withName(TABLE_NAME_AUDIT_USER)
|
||||||
.withBackendName(backendName)
|
.withBackendName(backendName)
|
||||||
|
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||||
.withRecordLabelFormat("%s")
|
.withRecordLabelFormat("%s")
|
||||||
.withRecordLabelFields("name")
|
.withRecordLabelFields("name")
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
@ -206,6 +210,7 @@ public class AuditsMetaDataProvider
|
|||||||
return new QTableMetaData()
|
return new QTableMetaData()
|
||||||
.withName(TABLE_NAME_AUDIT)
|
.withName(TABLE_NAME_AUDIT)
|
||||||
.withBackendName(backendName)
|
.withBackendName(backendName)
|
||||||
|
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||||
.withRecordLabelFormat("%s %s")
|
.withRecordLabelFormat("%s %s")
|
||||||
.withRecordLabelFields("auditTableId", "recordId")
|
.withRecordLabelFields("auditTableId", "recordId")
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
@ -227,12 +232,16 @@ public class AuditsMetaDataProvider
|
|||||||
return new QTableMetaData()
|
return new QTableMetaData()
|
||||||
.withName(TABLE_NAME_AUDIT_DETAIL)
|
.withName(TABLE_NAME_AUDIT_DETAIL)
|
||||||
.withBackendName(backendName)
|
.withBackendName(backendName)
|
||||||
|
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||||
.withRecordLabelFormat("%s")
|
.withRecordLabelFormat("%s")
|
||||||
.withRecordLabelFields("id")
|
.withRecordLabelFields("id")
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||||
.withField(new QFieldMetaData("auditId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
|
.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.AbstractActionInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
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.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.authentication.QAuthenticationMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
|
||||||
@ -93,6 +94,7 @@ public class QInstance
|
|||||||
private String defaultTimeZoneId = "UTC";
|
private String defaultTimeZoneId = "UTC";
|
||||||
|
|
||||||
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
|
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
|
||||||
|
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
|
||||||
|
|
||||||
// todo - lock down the object (no more changes allowed) after it's been validated?
|
// todo - lock down the object (no more changes allowed) after it's been validated?
|
||||||
|
|
||||||
@ -1042,4 +1044,35 @@ public class QInstance
|
|||||||
return (rs);
|
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 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_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 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("""
|
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));" />
|
<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.QRecordEntity;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
|
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.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.code.QCodeReference;
|
||||||
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.layout.QAppChildMetaData;
|
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 List<RecordSecurityLock> recordSecurityLocks;
|
||||||
private QPermissionRules permissionRules;
|
private QPermissionRules permissionRules;
|
||||||
|
private QAuditRules auditRules;
|
||||||
|
|
||||||
private QTableBackendDetails backendDetails;
|
private QTableBackendDetails backendDetails;
|
||||||
private QTableAutomationDetails automationDetails;
|
private QTableAutomationDetails automationDetails;
|
||||||
@ -1142,4 +1144,35 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
|
|||||||
return (this);
|
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 recordId2 = 1702;
|
||||||
Integer recordId3 = 1703;
|
Integer recordId3 = 1703;
|
||||||
AuditInput auditInput = new AuditInput();
|
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_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);
|
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 java.util.List;
|
||||||
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.exceptions.QException;
|
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.DeleteInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
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.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.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
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.assertNotNull;
|
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));
|
assertEquals("No", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), false));
|
||||||
|
|
||||||
assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), null));
|
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: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:47 PM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(17, 0, 47)));
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// this one flows through the exceptional cases //
|
// this one flows through the exceptional cases //
|
||||||
@ -196,8 +196,8 @@ class QValueFormatterTest extends BaseTest
|
|||||||
void testFormatDates()
|
void testFormatDates()
|
||||||
{
|
{
|
||||||
assertEquals("2023-02-01", QValueFormatter.formatDate(LocalDate.of(2023, Month.FEBRUARY, 1)));
|
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:00 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15, 0)));
|
||||||
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: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