Initial support for associated records (implemented insert, delete).

Include "api" on audit.
This commit is contained in:
2023-03-27 09:52:39 -05:00
parent 17d4c81cc3
commit ba805a4c92
15 changed files with 1052 additions and 85 deletions

View File

@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -106,6 +107,13 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
}
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
contextSuffix += (" via API Version: " + apiVersion);
}
AuditInput auditInput = new AuditInput();
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
{
@ -125,7 +133,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
///////////////////////////////////////////////////////////////////
// do many audits, all with field level details, for FIELD level //
///////////////////////////////////////////////////////////////////
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), qSession);
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
//////////////////////////////////////////

View File

@ -28,6 +28,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.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.DMLAuditInput;
@ -40,6 +41,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -91,6 +95,9 @@ public class DeleteAction
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
manageAssociations(deleteInput);
// todo post-customization - can do whatever w/ the result if you want
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
@ -100,6 +107,46 @@ public class DeleteAction
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(DeleteInput deleteInput) throws QException
{
QTableMetaData table = deleteInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// e.g., order -> orderLine
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
QQueryFilter filter = new QQueryFilter();
if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField()))
{
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys()));
}
else
{
throw (new QException("Join of this type is not supported for an associated delete at this time..."));
}
QTableMetaData associatedTable = QContext.getQInstance().getTable(association.getAssociatedTableName());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(association.getAssociatedTableName());
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> associatedKeys = queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).toList();
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);
DeleteOutput nextLevelDeleteOutput = new DeleteAction().execute(nextLevelDeleteInput);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -40,12 +41,16 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.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.DMLAuditInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
@ -75,15 +80,16 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
setAutomationStatusField(insertInput);
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
// todo - need to handle records with errors coming out of here...
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
setErrorsIfUniqueKeyErrors(insertInput, table);
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
manageAssociations(table, insertOutput.getRecords());
// todo post-customization - can do whatever w/ the result if you want
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
@ -98,6 +104,43 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords) throws QException
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// e.g., order -> orderLine
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
List<QRecord> nextLevelInserts = new ArrayList<>();
for(QRecord record : insertedRecords)
{
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
for(QRecord associatedRecord : CollectionUtils.nonNullList(record.getAssociatedRecords().get(association.getName())))
{
for(JoinOn joinOn : join.getJoinOns())
{
associatedRecord.setValue(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
nextLevelInserts.add(associatedRecord);
}
}
}
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -28,6 +28,7 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -65,6 +66,8 @@ public class QRecord implements Serializable
private Map<String, Serializable> backendDetails = new LinkedHashMap<>();
private List<String> errors = new ArrayList<>();
private Map<String, List<QRecord>> associatedRecords = new HashMap<>();
public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject";
@ -102,6 +105,7 @@ public class QRecord implements Serializable
this.displayValues = doDeepCopy(record.displayValues);
this.backendDetails = doDeepCopy(record.backendDetails);
this.errors = doDeepCopy(record.errors);
this.associatedRecords = doDeepCopy(record.associatedRecords);
}
@ -575,4 +579,66 @@ public class QRecord implements Serializable
return (QRecordEntity.fromQRecord(c, this));
}
/*******************************************************************************
** Getter for associatedRecords
*******************************************************************************/
public Map<String, List<QRecord>> getAssociatedRecords()
{
return (this.associatedRecords);
}
/*******************************************************************************
** Setter for associatedRecords
*******************************************************************************/
public void setAssociatedRecords(Map<String, List<QRecord>> associatedRecords)
{
this.associatedRecords = associatedRecords;
}
/*******************************************************************************
** Fluent setter for associatedRecords
*******************************************************************************/
public QRecord withAssociatedRecords(Map<String, List<QRecord>> associatedRecords)
{
this.associatedRecords = associatedRecords;
return (this);
}
/*******************************************************************************
** Fluent setter for associatedRecords
*******************************************************************************/
public QRecord withAssociatedRecords(String name, List<QRecord> associatedRecords)
{
if(this.associatedRecords == null)
{
this.associatedRecords = new HashMap<>();
}
this.associatedRecords.put(name, associatedRecords);
return (this);
}
/*******************************************************************************
** Fluent setter for associatedRecord
*******************************************************************************/
public QRecord withAssociatedRecord(String name, QRecord associatedRecord)
{
if(this.associatedRecords == null)
{
this.associatedRecords = new HashMap<>();
}
this.associatedRecords.putIfAbsent(name, new ArrayList<>());
this.associatedRecords.get(name).add(associatedRecord);
return (this);
}
}

View File

@ -0,0 +1,128 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.tables;
/*******************************************************************************
** definition of a qqq table that is "associated" with another table, e.g.,
** managed along with it - such as child-records under a parent record.
*******************************************************************************/
public class Association
{
private String name;
private String associatedTableName;
private String joinName;
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public Association withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for associatedTableName
*******************************************************************************/
public String getAssociatedTableName()
{
return (this.associatedTableName);
}
/*******************************************************************************
** Setter for associatedTableName
*******************************************************************************/
public void setAssociatedTableName(String associatedTableName)
{
this.associatedTableName = associatedTableName;
}
/*******************************************************************************
** Fluent setter for associatedTableName
*******************************************************************************/
public Association withAssociatedTableName(String associatedTableName)
{
this.associatedTableName = associatedTableName;
return (this);
}
/*******************************************************************************
** Getter for joinName
*******************************************************************************/
public String getJoinName()
{
return (this.joinName);
}
/*******************************************************************************
** Setter for joinName
*******************************************************************************/
public void setJoinName(String joinName)
{
this.joinName = joinName;
}
/*******************************************************************************
** Fluent setter for joinName
*******************************************************************************/
public Association withJoinName(String joinName)
{
this.joinName = joinName;
return (this);
}
}

View File

@ -72,6 +72,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private Map<String, QFieldMetaData> fields;
private List<UniqueKey> uniqueKeys;
private List<Association> associations;
private List<RecordSecurityLock> recordSecurityLocks;
private QPermissionRules permissionRules;
@ -1237,4 +1238,50 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
return (this);
}
/*******************************************************************************
** Getter for associations
*******************************************************************************/
public List<Association> getAssociations()
{
return (this.associations);
}
/*******************************************************************************
** Setter for associations
*******************************************************************************/
public void setAssociations(List<Association> associations)
{
this.associations = associations;
}
/*******************************************************************************
** Fluent setter for associations
*******************************************************************************/
public QTableMetaData withAssociations(List<Association> associations)
{
this.associations = associations;
return (this);
}
/*******************************************************************************
** Fluent setter for associations
*******************************************************************************/
public QTableMetaData withAssociation(Association association)
{
if(this.associations == null)
{
this.associations = new ArrayList<>();
}
this.associations.add(association);
return (this);
}
}