diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java index 67213f58..5266a9a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.audits; import java.io.Serializable; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -36,6 +37,7 @@ 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.AuditInput; import com.kingsrook.qqq.backend.core.model.actions.audits.AuditOutput; +import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; @@ -45,31 +47,78 @@ 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.model.session.QUser; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; /******************************************************************************* - ** Insert an audit (e.g., the same one) against 1 or more records. + ** Insert 1 or more audits (and optionally their children, auditDetails) ** - ** Takes care of managing the foreign key tables. + ** Takes care of managing the foreign key tables (auditTable, auditUser). ** - ** Enforces that security key values are provided, if the table has any. + ** Enforces that security key values are provided, if the table has any. Note that + ** might mean a null is given for a particular key, but at least the key must be present. *******************************************************************************/ public class AuditAction extends AbstractQActionFunction { private static final QLogger LOG = QLogger.getLogger(AuditAction.class); + private Map, Integer> cachedFetches = new HashMap<>(); + /******************************************************************************* - ** + ** Execute to insert 1 audit, with no details (child records) *******************************************************************************/ public static void execute(String tableName, Integer recordId, Map securityKeyValues, String message) { - new AuditAction().execute(new AuditInput() + execute(tableName, recordId, securityKeyValues, message, null); + } + + + + /******************************************************************************* + ** Execute to insert 1 audit, with a list of details (child records) + *******************************************************************************/ + public static void execute(String tableName, Integer recordId, Map securityKeyValues, String message, List details) + { + new AuditAction().execute(new AuditInput().withAuditSingleInput(new AuditSingleInput() .withAuditTableName(tableName) - .withRecordIdList(List.of(recordId)) + .withRecordId(recordId) .withSecurityKeyValues(securityKeyValues) - .withMessage(message)); + .withMessage(message) + .withDetails(details) + )); + } + + + + /******************************************************************************* + ** Add 1 auditSingleInput to an AuditInput object - with no details (child records). + *******************************************************************************/ + public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map securityKeyValues, String message) + { + return (appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null)); + } + + + + /******************************************************************************* + ** 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 securityKeyValues, String message, List details) + { + if(auditInput == null) + { + auditInput = new AuditInput(); + } + + return auditInput.withAuditSingleInput(new AuditSingleInput() + .withAuditTableName(tableName) + .withRecordId(recordId) + .withSecurityKeyValues(securityKeyValues) + .withMessage(message) + .withDetails(details) + ); } @@ -81,56 +130,105 @@ public class AuditAction extends AbstractQActionFunction auditRecords = new ArrayList<>(); - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) - { - if(input.getSecurityKeyValues() == null || !input.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType())) + for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList())) { - throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + input.getAuditTableName())); - } - } - - Integer auditTableId = getIdForName("auditTable", input.getAuditTableName()); - Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(input.getAuditUserName(), getSessionUserName())); - Instant timestamp = Objects.requireNonNullElse(input.getTimestamp(), Instant.now()); - - List auditRecords = new ArrayList<>(); - for(Integer recordId : input.getRecordIdList()) - { - QRecord record = new QRecord() - .withValue("auditTableId", auditTableId) - .withValue("auditUserId", auditUserId) - .withValue("timestamp", timestamp) - .withValue("message", input.getMessage()) - .withValue("recordId", recordId); - - if(input.getSecurityKeyValues() != null) - { - for(Map.Entry entry : input.getSecurityKeyValues().entrySet()) + ///////////////////////////////////////// + // validate table is known in instance // + ///////////////////////////////////////// + QTableMetaData table = QContext.getQInstance().getTable(auditSingleInput.getAuditTableName()); + if(table == null) { - record.setValue(entry.getKey(), entry.getValue()); + throw (new QException("Requested audit for an unrecognized table name: " + auditSingleInput.getAuditTableName())); + } + + /////////////////////////////////////////////////// + // validate security keys on the table are given // + /////////////////////////////////////////////////// + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + { + if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType())) + { + throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName())); + } + } + + //////////////////////////////////////////////// + // map names to ids and handle default values // + //////////////////////////////////////////////// + Integer auditTableId = getIdForName("auditTable", auditSingleInput.getAuditTableName()); + Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(auditSingleInput.getAuditUserName(), getSessionUserName())); + Instant timestamp = Objects.requireNonNullElse(auditSingleInput.getTimestamp(), Instant.now()); + + ////////////////// + // build record // + ////////////////// + QRecord record = new QRecord() + .withValue("auditTableId", auditTableId) + .withValue("auditUserId", auditUserId) + .withValue("timestamp", timestamp) + .withValue("message", auditSingleInput.getMessage()) + .withValue("recordId", auditSingleInput.getRecordId()); + + if(auditSingleInput.getSecurityKeyValues() != null) + { + for(Map.Entry entry : auditSingleInput.getSecurityKeyValues().entrySet()) + { + record.setValue(entry.getKey(), entry.getValue()); + } + } + + auditRecords.add(record); + } + + ///////////////////////////// + // do a single bulk insert // + ///////////////////////////// + InsertInput insertInput = new InsertInput(); + insertInput.setTableName("audit"); + insertInput.setRecords(auditRecords); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + + ////////////////////////////////////////// + // now look for children (auditDetails) // + ////////////////////////////////////////// + int i = 0; + List auditDetailRecords = new ArrayList<>(); + for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList())) + { + Integer auditId = insertOutput.getRecords().get(i++).getValueInteger("id"); + if(auditId == null) + { + LOG.warn("Missing an id for inserted audit - so won't be able to store its child details..."); + continue; + } + + for(String detail : CollectionUtils.nonNullList(auditSingleInput.getDetails())) + { + auditDetailRecords.add(new QRecord() + .withValue("auditId", auditId) + .withValue("message", detail) + ); } } - auditRecords.add(record); + insertInput = new InsertInput(); + insertInput.setTableName("auditDetail"); + insertInput.setRecords(auditDetailRecords); + new InsertAction().execute(insertInput); } + catch(Exception e) + { + LOG.error("Error performing an audit", e); + } + } - InsertInput insertInput = new InsertInput(); - insertInput.setTableName("audit"); - insertInput.setRecords(auditRecords); - new InsertAction().execute(insertInput); - } - catch(Exception e) - { - LOG.error("Error performing an audit", e); - } return (auditOutput); } @@ -156,54 +254,64 @@ public class AuditAction extends AbstractQActionFunction key = new Pair<>(tableName, nameValue); + if(!cachedFetches.containsKey(key)) { - return id; - } - try - { - LOG.debug("Inserting " + tableName + " named " + nameValue); - InsertInput insertInput = new InsertInput(); - insertInput.setTableName(tableName); - QRecord record = new QRecord().withValue("name", nameValue); - - if(tableName.equals("auditTable")) - { - QTableMetaData table = QContext.getQInstance().getTable(nameValue); - if(table != null) - { - record.setValue("label", table.getLabel()); - } - } - - insertInput.setRecords(List.of(record)); - InsertOutput insertOutput = new InsertAction().execute(insertInput); - id = insertOutput.getRecords().get(0).getValueInteger("id"); + Integer id = fetchIdFromName(tableName, nameValue); if(id != null) { + cachedFetches.put(key, id); return id; } - } - catch(Exception e) - { - //////////////////////////////////////////////////////////////////// - // assume this may mean a dupe-key - so - try another fetch below // - //////////////////////////////////////////////////////////////////// - LOG.debug("Caught error inserting " + tableName + " named " + nameValue + " - will try to re-fetch", e); + + try + { + LOG.debug("Inserting " + tableName + " named " + nameValue); + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(tableName); + QRecord record = new QRecord().withValue("name", nameValue); + + if(tableName.equals("auditTable")) + { + QTableMetaData table = QContext.getQInstance().getTable(nameValue); + if(table != null) + { + record.setValue("label", table.getLabel()); + } + } + + insertInput.setRecords(List.of(record)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + id = insertOutput.getRecords().get(0).getValueInteger("id"); + if(id != null) + { + cachedFetches.put(key, id); + return id; + } + } + catch(Exception e) + { + //////////////////////////////////////////////////////////////////// + // assume this may mean a dupe-key - so - try another fetch below // + //////////////////////////////////////////////////////////////////// + LOG.debug("Caught error inserting " + tableName + " named " + nameValue + " - will try to re-fetch", e); + } + + id = fetchIdFromName(tableName, nameValue); + if(id != null) + { + cachedFetches.put(key, id); + return id; + } + + ///////////// + // give up // + ///////////// + throw (new QException("Unable to get id for " + tableName + " named " + nameValue)); } - id = fetchIdFromName(tableName, nameValue); - if(id != null) - { - return id; - } - - ///////////// - // give up // - ///////////// - throw (new QException("Unable to get id for " + tableName + " named " + nameValue)); + return (cachedFetches.get(key)); } @@ -211,7 +319,7 @@ public class AuditAction extends AbstractQActionFunction recordIdList; - - private Map securityKeyValues; + private List auditSingleInputList = new ArrayList<>(); /******************************************************************************* - ** Getter for auditTableName + ** Getter for auditSingleInputList *******************************************************************************/ - public String getAuditTableName() + public List getAuditSingleInputList() { - return (this.auditTableName); + return (this.auditSingleInputList); } /******************************************************************************* - ** Setter for auditTableName + ** Setter for auditSingleInputList *******************************************************************************/ - public void setAuditTableName(String auditTableName) + public void setAuditSingleInputList(List auditSingleInputList) { - this.auditTableName = auditTableName; + this.auditSingleInputList = auditSingleInputList; } /******************************************************************************* - ** Fluent setter for auditTableName + ** Fluent setter for auditSingleInputList *******************************************************************************/ - public AuditInput withAuditTableName(String auditTableName) + public AuditInput withAuditSingleInputList(List auditSingleInputList) { - this.auditTableName = auditTableName; + this.auditSingleInputList = auditSingleInputList; return (this); } /******************************************************************************* - ** Getter for auditUserName + ** Add a single auditSingleInput *******************************************************************************/ - public String getAuditUserName() + public void addAuditSingleInput(AuditSingleInput auditSingleInput) { - return (this.auditUserName); + if(this.auditSingleInputList == null) + { + this.auditSingleInputList = new ArrayList<>(); + } + this.auditSingleInputList.add(auditSingleInput); } /******************************************************************************* - ** Setter for auditUserName + ** Fluent setter to add a single auditSingleInput *******************************************************************************/ - public void setAuditUserName(String auditUserName) + public AuditInput withAuditSingleInput(AuditSingleInput auditSingleInput) { - this.auditUserName = auditUserName; - } - - - - /******************************************************************************* - ** Fluent setter for auditUserName - *******************************************************************************/ - public AuditInput withAuditUserName(String auditUserName) - { - this.auditUserName = auditUserName; - return (this); - } - - - - /******************************************************************************* - ** Getter for timestamp - *******************************************************************************/ - public Instant getTimestamp() - { - return (this.timestamp); - } - - - - /******************************************************************************* - ** Setter for timestamp - *******************************************************************************/ - public void setTimestamp(Instant timestamp) - { - this.timestamp = timestamp; - } - - - - /******************************************************************************* - ** Fluent setter for timestamp - *******************************************************************************/ - public AuditInput withTimestamp(Instant timestamp) - { - this.timestamp = timestamp; - return (this); - } - - - - /******************************************************************************* - ** Getter for message - *******************************************************************************/ - public String getMessage() - { - return (this.message); - } - - - - /******************************************************************************* - ** Setter for message - *******************************************************************************/ - public void setMessage(String message) - { - this.message = message; - } - - - - /******************************************************************************* - ** Fluent setter for message - *******************************************************************************/ - public AuditInput withMessage(String message) - { - this.message = message; - return (this); - } - - - - /******************************************************************************* - ** Getter for recordIdList - *******************************************************************************/ - public List getRecordIdList() - { - return (this.recordIdList); - } - - - - /******************************************************************************* - ** Setter for recordIdList - *******************************************************************************/ - public void setRecordIdList(List recordIdList) - { - this.recordIdList = recordIdList; - } - - - - /******************************************************************************* - ** Fluent setter for recordIdList - *******************************************************************************/ - public AuditInput withRecordIdList(List recordIdList) - { - this.recordIdList = recordIdList; - return (this); - } - - - - /******************************************************************************* - ** Getter for securityKeyValues - *******************************************************************************/ - public Map getSecurityKeyValues() - { - return (this.securityKeyValues); - } - - - - /******************************************************************************* - ** Setter for securityKeyValues - *******************************************************************************/ - public void setSecurityKeyValues(Map securityKeyValues) - { - this.securityKeyValues = securityKeyValues; - } - - - - /******************************************************************************* - ** Fluent setter for securityKeyValues - *******************************************************************************/ - public AuditInput withSecurityKeyValues(Map securityKeyValues) - { - this.securityKeyValues = securityKeyValues; + addAuditSingleInput(auditSingleInput); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java new file mode 100644 index 00000000..1adf3582 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java @@ -0,0 +1,298 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.model.actions.audits; + + +import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +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; + + +/******************************************************************************* + ** Input data to insert a single audit record (with optional child record).. + *******************************************************************************/ +public class AuditSingleInput +{ + private String auditTableName; + private String auditUserName; + private Instant timestamp; + private String message; + private Integer recordId; + + private Map securityKeyValues; + + private List details; + + + + /******************************************************************************* + ** Getter for auditTableName + *******************************************************************************/ + public String getAuditTableName() + { + return (this.auditTableName); + } + + + + /******************************************************************************* + ** Setter for auditTableName + *******************************************************************************/ + public void setAuditTableName(String auditTableName) + { + this.auditTableName = auditTableName; + } + + + + /******************************************************************************* + ** Fluent setter for auditTableName + *******************************************************************************/ + public AuditSingleInput withAuditTableName(String auditTableName) + { + this.auditTableName = auditTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for auditUserName + *******************************************************************************/ + public String getAuditUserName() + { + return (this.auditUserName); + } + + + + /******************************************************************************* + ** Setter for auditUserName + *******************************************************************************/ + public void setAuditUserName(String auditUserName) + { + this.auditUserName = auditUserName; + } + + + + /******************************************************************************* + ** Fluent setter for auditUserName + *******************************************************************************/ + public AuditSingleInput withAuditUserName(String auditUserName) + { + this.auditUserName = auditUserName; + return (this); + } + + + + /******************************************************************************* + ** Getter for timestamp + *******************************************************************************/ + public Instant getTimestamp() + { + return (this.timestamp); + } + + + + /******************************************************************************* + ** Setter for timestamp + *******************************************************************************/ + public void setTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + } + + + + /******************************************************************************* + ** Fluent setter for timestamp + *******************************************************************************/ + public AuditSingleInput withTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for message + *******************************************************************************/ + public String getMessage() + { + return (this.message); + } + + + + /******************************************************************************* + ** Setter for message + *******************************************************************************/ + public void setMessage(String message) + { + this.message = message; + } + + + + /******************************************************************************* + ** Fluent setter for message + *******************************************************************************/ + public AuditSingleInput withMessage(String message) + { + this.message = message; + return (this); + } + + + + /******************************************************************************* + ** Getter for securityKeyValues + *******************************************************************************/ + public Map getSecurityKeyValues() + { + return (this.securityKeyValues); + } + + + + /******************************************************************************* + ** Setter for securityKeyValues + *******************************************************************************/ + public void setSecurityKeyValues(Map securityKeyValues) + { + this.securityKeyValues = securityKeyValues; + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyValues + *******************************************************************************/ + public AuditSingleInput withSecurityKeyValues(Map securityKeyValues) + { + this.securityKeyValues = securityKeyValues; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordId + *******************************************************************************/ + public Integer getRecordId() + { + return (this.recordId); + } + + + + /******************************************************************************* + ** Setter for recordId + *******************************************************************************/ + public void setRecordId(Integer recordId) + { + this.recordId = recordId; + } + + + + /******************************************************************************* + ** Fluent setter for recordId + *******************************************************************************/ + public AuditSingleInput withRecordId(Integer recordId) + { + this.recordId = recordId; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public AuditSingleInput forRecord(QTableMetaData table, QRecord record) + { + setRecordId(record.getValueInteger(table.getPrimaryKeyField())); // todo support non-integer + setAuditTableName(table.getName()); + + this.securityKeyValues = new HashMap<>(); + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + { + this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName())); + } + + return (this); + } + + /******************************************************************************* + ** Getter for details + *******************************************************************************/ + public List getDetails() + { + return (this.details); + } + + + + /******************************************************************************* + ** Setter for details + *******************************************************************************/ + public void setDetails(List details) + { + this.details = details; + } + + + + /******************************************************************************* + ** Fluent setter for details + *******************************************************************************/ + public AuditSingleInput withDetails(List details) + { + this.details = details; + return (this); + } + + /******************************************************************************* + ** + *******************************************************************************/ + public void addDetail(String detail) + { + if(this.details == null) + { + this.details = new ArrayList<>(); + } + this.details.add(detail); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java index a788fcc6..a525b7ec 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java @@ -43,9 +43,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; *******************************************************************************/ public class AuditsMetaDataProvider { - public static final String TABLE_NAME_AUDIT_TABLE = "auditTable"; - public static final String TABLE_NAME_AUDIT_USER = "auditUser"; - public static final String TABLE_NAME_AUDIT = "audit"; + public static final String TABLE_NAME_AUDIT_TABLE = "auditTable"; + public static final String TABLE_NAME_AUDIT_USER = "auditUser"; + public static final String TABLE_NAME_AUDIT = "audit"; + public static final String TABLE_NAME_AUDIT_DETAIL = "auditDetail"; @@ -80,6 +81,13 @@ public class AuditsMetaDataProvider .withType(JoinType.MANY_TO_ONE) .withJoinOn(new JoinOn("auditUserId", "id"))); + instance.addJoin(new QJoinMetaData() + .withLeftTable(TABLE_NAME_AUDIT) + .withRightTable(TABLE_NAME_AUDIT_DETAIL) + .withInferredName() + .withType(JoinType.ONE_TO_MANY) + .withJoinOn(new JoinOn("id", "auditId"))); + } @@ -111,6 +119,11 @@ public class AuditsMetaDataProvider .withName(TABLE_NAME_AUDIT_USER) .withTableName(TABLE_NAME_AUDIT_USER) ); + + instance.addPossibleValueSource(new QPossibleValueSource() + .withName(TABLE_NAME_AUDIT) + .withTableName(TABLE_NAME_AUDIT) + ); } @@ -124,6 +137,7 @@ public class AuditsMetaDataProvider rs.add(enrich(backendDetailEnricher, defineAuditUserTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineAuditDetailTable(backendName))); return (rs); } @@ -203,4 +217,22 @@ public class AuditsMetaDataProvider .withField(new QFieldMetaData("timestamp", QFieldType.DATE_TIME)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineAuditDetailTable(String backendName) + { + return new QTableMetaData() + .withName(TABLE_NAME_AUDIT_DETAIL) + .withBackendName(backendName) + .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)); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java index 3d31715a..54698ec8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java @@ -22,10 +22,15 @@ package com.kingsrook.qqq.backend.core.actions.audits; +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; 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.AuditInput; 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; @@ -34,7 +39,10 @@ import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -47,7 +55,7 @@ class AuditActionTest extends BaseTest ** *******************************************************************************/ @Test - void test() throws QException + void testSingle() throws QException { QInstance qInstance = TestUtils.defineInstance(); new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); @@ -67,4 +75,126 @@ class AuditActionTest extends BaseTest assertEquals("Test Audit", auditRecord.getValueString("message")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFailWithoutSecurityKey() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + String userName = "John Doe"; + QContext.init(qInstance, new QSession().withUser(new QUser().withFullName(userName))); + + int recordId = 1701; + AuditAction.execute(TestUtils.TABLE_NAME_ORDER, recordId, Map.of(), "Test Audit"); + + /////////////////////////////////////////////////////////////////// + // it should not throw, but it should also not insert the audit. // + /////////////////////////////////////////////////////////////////// + Optional auditRecord = GeneralProcessUtils.getRecordByField(null, "audit", "recordId", recordId); + assertFalse(auditRecord.isPresent()); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // try again with a null value in the key - that should be ok - as at least you were thinking // + // about the key and put in SOME value (null has its own semantics in security keys) // + //////////////////////////////////////////////////////////////////////////////////////////////// + Map securityKeys = new HashMap<>(); + securityKeys.put(TestUtils.SECURITY_KEY_TYPE_STORE, null); + AuditAction.execute(TestUtils.TABLE_NAME_ORDER, recordId, securityKeys, "Test Audit"); + + ///////////////////////////////////// + // now the audit should be stored. // + ///////////////////////////////////// + auditRecord = GeneralProcessUtils.getRecordByField(null, "audit", "recordId", recordId); + assertTrue(auditRecord.isPresent()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMulti() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + String userName = "John Doe"; + QContext.init(qInstance, new QSession().withUser(new QUser().withFullName(userName))); + + Integer recordId1 = 1701; + Integer recordId2 = 1702; + AuditInput auditInput = new AuditInput(); + AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId1, Map.of(), "Test Audit"); + AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_ORDER, recordId2, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 47), "Test Another Audit"); + new AuditAction().execute(auditInput); + + ///////////////////////////////////// + // make sure things can be fetched // + ///////////////////////////////////// + GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "auditTable", "name", TestUtils.TABLE_NAME_PERSON_MEMORY); + GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "auditUser", "name", userName); + QRecord auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "audit", "recordId", recordId1); + assertEquals("Test Audit", auditRecord.getValueString("message")); + + auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "audit", "recordId", recordId2); + assertEquals("Test Another Audit", auditRecord.getValueString("message")); + assertEquals(47, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMultiWithDetails() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + String userName = "John Doe"; + QContext.init(qInstance, new QSession().withUser(new QUser().withFullName(userName))); + + Integer recordId1 = 1701; + 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_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")); + new AuditAction().execute(auditInput); + + ///////////////////////////////////// + // make sure things can be fetched // + ///////////////////////////////////// + GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "auditTable", "name", TestUtils.TABLE_NAME_PERSON_MEMORY); + GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "auditUser", "name", userName); + QRecord auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "audit", "recordId", recordId1); + assertEquals("Test Audit", auditRecord.getValueString("message")); + + List auditDetails = GeneralProcessUtils.getRecordListByField(null, "auditDetail", "auditId", auditRecord.getValue("id")); + assertEquals(2, auditDetails.size()); + assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail1")); + assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail2")); + + auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "audit", "recordId", recordId2); + assertEquals("Test Another Audit", auditRecord.getValueString("message")); + assertEquals(47, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE)); + auditDetails = GeneralProcessUtils.getRecordListByField(null, "auditDetail", "auditId", auditRecord.getValue("id")); + assertEquals(0, auditDetails.size()); + + auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "audit", "recordId", recordId3); + assertEquals("Audit 3", auditRecord.getValueString("message")); + assertEquals(42, auditRecord.getValueInteger(TestUtils.SECURITY_KEY_TYPE_STORE)); + auditDetails = GeneralProcessUtils.getRecordListByField(null, "auditDetail", "auditId", auditRecord.getValue("id")); + assertEquals(1, auditDetails.size()); + assertThat(auditDetails).anyMatch(r -> r.getValueString("message").equals("Detail3")); + } + } \ No newline at end of file