Initial checkin

This commit is contained in:
2023-01-19 16:31:18 -06:00
parent 6bb810c1f4
commit 4344fa472a
5 changed files with 734 additions and 0 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<AuditInput, AuditOutput>
{
private static final QLogger LOG = QLogger.getLogger(AuditAction.class);
/*******************************************************************************
**
*******************************************************************************/
public static void execute(String tableName, Integer recordId, Map<String, Serializable> 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<QRecord> 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<String, Serializable> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Integer> recordIdList;
private Map<String, Serializable> 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<Integer> getRecordIdList()
{
return (this.recordIdList);
}
/*******************************************************************************
** Setter for recordIdList
*******************************************************************************/
public void setRecordIdList(List<Integer> recordIdList)
{
this.recordIdList = recordIdList;
}
/*******************************************************************************
** Fluent setter for recordIdList
*******************************************************************************/
public AuditInput withRecordIdList(List<Integer> recordIdList)
{
this.recordIdList = recordIdList;
return (this);
}
/*******************************************************************************
** Getter for securityKeyValues
*******************************************************************************/
public Map<String, Serializable> getSecurityKeyValues()
{
return (this.securityKeyValues);
}
/*******************************************************************************
** Setter for securityKeyValues
*******************************************************************************/
public void setSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
}
/*******************************************************************************
** Fluent setter for securityKeyValues
*******************************************************************************/
public AuditInput withSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
return (this);
}
}

View File

@ -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.actions.audits;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
**
*******************************************************************************/
public class AuditOutput extends AbstractActionOutput
{
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QTableMetaData> backendDetailEnricher) throws QException
{
defineStandardAuditTables(instance, backendName, backendDetailEnricher);
defineStandardAuditPossibleValueSources(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardAuditTables(QInstance instance, String backendName, Consumer<QTableMetaData> 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<QTableMetaData> defineStandardAuditTables(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
List<QTableMetaData> 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<QTableMetaData> 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));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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"));
}
}