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
new file mode 100644
index 00000000..67213f58
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java
@@ -0,0 +1,227 @@
+/*
+ * 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.actions.audits;
+
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+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.audits.AuditInput;
+import com.kingsrook.qqq.backend.core.model.actions.audits.AuditOutput;
+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;
+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.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;
+
+
+/*******************************************************************************
+ ** Insert an audit (e.g., the same one) against 1 or more records.
+ **
+ ** Takes care of managing the foreign key tables.
+ **
+ ** Enforces that security key values are provided, if the table has any.
+ *******************************************************************************/
+public class AuditAction extends AbstractQActionFunction
+{
+ private static final QLogger LOG = QLogger.getLogger(AuditAction.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void execute(String tableName, Integer recordId, Map securityKeyValues, String message)
+ {
+ new AuditAction().execute(new AuditInput()
+ .withAuditTableName(tableName)
+ .withRecordIdList(List.of(recordId))
+ .withSecurityKeyValues(securityKeyValues)
+ .withMessage(message));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public AuditOutput execute(AuditInput input)
+ {
+ AuditOutput auditOutput = new AuditOutput();
+ try
+ {
+ QTableMetaData table = QContext.getQInstance().getTable(input.getAuditTableName());
+ if(table == null)
+ {
+ throw (new QException("Requested audit for an unrecognized table name: " + input.getAuditTableName()));
+ }
+
+ for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
+ {
+ if(input.getSecurityKeyValues() == null || !input.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
+ {
+ 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())
+ {
+ record.setValue(entry.getKey(), entry.getValue());
+ }
+ }
+
+ auditRecords.add(record);
+ }
+
+ 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);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static String getSessionUserName()
+ {
+ QUser user = QContext.getQSession().getUser();
+ if(user == null)
+ {
+ return ("Unknown");
+ }
+ return (user.getFullName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Integer getIdForName(String tableName, String nameValue) throws QException
+ {
+ Integer id = fetchIdFromName(tableName, nameValue);
+ if(id != null)
+ {
+ 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");
+ if(id != null)
+ {
+ 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)
+ {
+ return id;
+ }
+
+ /////////////
+ // give up //
+ /////////////
+ throw (new QException("Unable to get id for " + tableName + " named " + nameValue));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static Integer fetchIdFromName(String tableName, String nameValue) throws QException
+ {
+ GetInput getInput = new GetInput();
+ getInput.setTableName(tableName);
+ getInput.setUniqueKey(Map.of("name", nameValue));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ if(getOutput.getRecord() != null)
+ {
+ return (getOutput.getRecord().getValueInteger("id"));
+ }
+ return null;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java
new file mode 100644
index 00000000..1e487f0d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java
@@ -0,0 +1,231 @@
+/*
+ * 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.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuditInput extends AbstractActionInput
+{
+ private String auditTableName;
+ private String auditUserName;
+ private Instant timestamp;
+ private String message;
+ private List recordIdList;
+
+ private Map securityKeyValues;
+
+
+
+ /*******************************************************************************
+ ** 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 AuditInput 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 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;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java
new file mode 100644
index 00000000..dc8ccefb
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java
@@ -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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.audits;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuditOutput extends AbstractActionOutput
+{
+}
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
new file mode 100644
index 00000000..fb1a0887
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java
@@ -0,0 +1,173 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.audits;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AuditsMetaDataProvider
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ defineStandardAuditTables(instance, backendName, backendDetailEnricher);
+ defineStandardAuditPossibleValueSources(instance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineStandardAuditTables(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ for(QTableMetaData tableMetaData : defineStandardAuditTables(backendName, backendDetailEnricher))
+ {
+ instance.addTable(tableMetaData);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineStandardAuditPossibleValueSources(QInstance instance)
+ {
+ instance.addPossibleValueSource(new QPossibleValueSource()
+ .withName("auditTable")
+ .withTableName("auditTable")
+ );
+
+ instance.addPossibleValueSource(new QPossibleValueSource()
+ .withName("auditUser")
+ .withTableName("auditUser")
+ );
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List defineStandardAuditTables(String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ List rs = new ArrayList<>();
+ rs.add(enrich(backendDetailEnricher, defineAuditUserTable(backendName)));
+ rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName)));
+ rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName)));
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData enrich(Consumer backendDetailEnricher, QTableMetaData table)
+ {
+ if(backendDetailEnricher != null)
+ {
+ backendDetailEnricher.accept(table);
+ }
+ return (table);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineAuditTableTable(String backendName)
+ {
+ return new QTableMetaData()
+ .withName("auditTable")
+ .withBackendName(backendName)
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields("label")
+ .withPrimaryKeyField("id")
+ .withUniqueKey(new UniqueKey("name"))
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.STRING))
+ .withField(new QFieldMetaData("label", QFieldType.STRING))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineAuditUserTable(String backendName)
+ {
+ return new QTableMetaData()
+ .withName("auditUser")
+ .withBackendName(backendName)
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields("name")
+ .withPrimaryKeyField("id")
+ .withUniqueKey(new UniqueKey("name"))
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.STRING))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineAuditTable(String backendName)
+ {
+ return new QTableMetaData()
+ .withName("audit")
+ .withBackendName(backendName)
+ .withRecordLabelFormat("%s")
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName("auditTable"))
+ .withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName("auditUser"))
+ .withField(new QFieldMetaData("recordId", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
+ .withField(new QFieldMetaData("timestamp", QFieldType.DATE_TIME));
+ }
+
+}
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
new file mode 100644
index 00000000..3d31715a
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.actions.audits;
+
+
+import java.util.Map;
+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.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.session.QSession;
+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.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for AuditAction
+ *******************************************************************************/
+class AuditActionTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() 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 recordId = 1701;
+ AuditAction.execute(TestUtils.TABLE_NAME_PERSON_MEMORY, recordId, Map.of(), "Test Audit");
+
+ /////////////////////////////////////
+ // 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", recordId);
+ assertEquals("Test Audit", auditRecord.getValueString("message"));
+ }
+
+}
\ No newline at end of file