mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-19 21:50:45 +00:00
add exposed joins to frontend metadata; checkpoing on validation & enrichment of eposed joins
This commit is contained in:
@ -31,26 +31,37 @@ import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Object to represent the graph of joins in a QQQ Instance. e.g., all of the
|
||||
** connections among tables through joins.
|
||||
*******************************************************************************/
|
||||
public class JoinGraph
|
||||
{
|
||||
private record Node(String tableName)
|
||||
{
|
||||
}
|
||||
|
||||
private Set<Edge> edges = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Graph edge (no graph nodes needed in here)
|
||||
*******************************************************************************/
|
||||
private record Edge(String joinName, String leftTable, String rightTable)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static class CanonicalJoin
|
||||
/*******************************************************************************
|
||||
** In this class, we are treating joins as non-directional graph edges - so -
|
||||
** use this class to "normalize" what may otherwise be duplicated joins in the
|
||||
** qInstance (e.g., A -> B and B -> A -- in the instance, those are valid, but
|
||||
** in our graph here, we want to consider those the same).
|
||||
*******************************************************************************/
|
||||
private static class NormalizedJoin
|
||||
{
|
||||
private String tableA;
|
||||
private String tableB;
|
||||
@ -62,7 +73,7 @@ public class JoinGraph
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CanonicalJoin(QJoinMetaData joinMetaData)
|
||||
public NormalizedJoin(QJoinMetaData joinMetaData)
|
||||
{
|
||||
boolean needFlip = false;
|
||||
int tableCompare = joinMetaData.getLeftTable().compareTo(joinMetaData.getRightTable());
|
||||
@ -106,7 +117,7 @@ public class JoinGraph
|
||||
{
|
||||
return false;
|
||||
}
|
||||
CanonicalJoin that = (CanonicalJoin) o;
|
||||
NormalizedJoin that = (NormalizedJoin) o;
|
||||
return Objects.equals(tableA, that.tableA) && Objects.equals(tableB, that.tableB) && Objects.equals(joinFieldA, that.joinFieldA) && Objects.equals(joinFieldB, that.joinFieldB);
|
||||
}
|
||||
|
||||
@ -124,29 +135,22 @@ public class JoinGraph
|
||||
|
||||
|
||||
|
||||
private Set<Node> nodes = new HashSet<>();
|
||||
private Set<Edge> edges = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JoinGraph(QInstance qInstance)
|
||||
{
|
||||
Set<CanonicalJoin> usedJoins = new HashSet<>();
|
||||
for(QJoinMetaData join : qInstance.getJoins().values())
|
||||
Set<NormalizedJoin> usedJoins = new HashSet<>();
|
||||
for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
|
||||
{
|
||||
CanonicalJoin canonicalJoin = new CanonicalJoin(join);
|
||||
if(usedJoins.contains(canonicalJoin))
|
||||
NormalizedJoin normalizedJoin = new NormalizedJoin(join);
|
||||
if(usedJoins.contains(normalizedJoin))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
usedJoins.add(canonicalJoin);
|
||||
nodes.add(new Node(join.getLeftTable()));
|
||||
nodes.add(new Node(join.getRightTable()));
|
||||
usedJoins.add(normalizedJoin);
|
||||
edges.add(new Edge(join.getName(), join.getLeftTable(), join.getRightTable()));
|
||||
}
|
||||
}
|
||||
@ -156,36 +160,6 @@ public class JoinGraph
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Set<String> getJoins(String tableName)
|
||||
{
|
||||
Set<String> rs = new HashSet<>();
|
||||
Set<String> tables = new HashSet<>();
|
||||
tables.add(tableName);
|
||||
|
||||
boolean keepGoing = true;
|
||||
while(keepGoing)
|
||||
{
|
||||
keepGoing = false;
|
||||
for(Edge edge : edges)
|
||||
{
|
||||
if(tables.contains(edge.leftTable) || tables.contains(edge.rightTable))
|
||||
{
|
||||
if(!rs.contains(edge.joinName))
|
||||
{
|
||||
tables.add(edge.leftTable);
|
||||
tables.add(edge.rightTable);
|
||||
rs.add(edge.joinName);
|
||||
keepGoing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public record JoinConnection(String joinTable, String viaJoinName) implements Comparable<JoinConnection>
|
||||
{
|
||||
|
||||
@ -240,6 +214,49 @@ public class JoinGraph
|
||||
|
||||
return (this.list.size() - that.list.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean matchesJoinPath(List<String> joinPath)
|
||||
{
|
||||
if(list.size() != joinPath.size())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
for(int i = 0; i < list.size(); i++)
|
||||
{
|
||||
if(!list.get(i).viaJoinName().equals(joinPath.get(i)))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getJoinNamesAsString()
|
||||
{
|
||||
return (StringUtils.join(", ", list().stream().map(jc -> jc.viaJoinName()).toList()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<String> getJoinNamesAsList()
|
||||
{
|
||||
return (list().stream().map(jc -> jc.viaJoinName()).toList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -316,181 +333,4 @@ public class JoinGraph
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public record JoinPath(String joinTable, List<String> joinNames)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Set<JoinPath> getJoinPaths(String tableName)
|
||||
{
|
||||
Set<JoinPath> rs = new HashSet<>();
|
||||
doGetJoinPaths(rs, tableName, new ArrayList<>());
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void doGetJoinPaths(Set<JoinPath> joinPaths, String tableName, List<String> path)
|
||||
{
|
||||
for(Edge edge : edges)
|
||||
{
|
||||
if(edge.leftTable.equals(tableName) || edge.rightTable.equals(tableName))
|
||||
{
|
||||
if(path.contains(edge.joinName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> newPath = new ArrayList<>(path);
|
||||
newPath.add(edge.joinName);
|
||||
if(!joinPathsContain(joinPaths, newPath))
|
||||
{
|
||||
String otherTableName = null;
|
||||
if(!edge.leftTable.equals(tableName))
|
||||
{
|
||||
otherTableName = edge.leftTable;
|
||||
}
|
||||
else if(!edge.rightTable.equals(tableName))
|
||||
{
|
||||
otherTableName = edge.rightTable;
|
||||
}
|
||||
|
||||
if(otherTableName != null)
|
||||
{
|
||||
joinPaths.add(new JoinPath(otherTableName, newPath));
|
||||
doGetJoinPaths(joinPaths, otherTableName, new ArrayList<>(newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean joinPathsContain(Set<JoinPath> joinPaths, List<String> newPath)
|
||||
{
|
||||
for(JoinPath joinPath : joinPaths)
|
||||
{
|
||||
if(joinPath.joinNames().equals(newPath))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
/*
|
||||
public Set<List<String>> getJoinPaths(String tableName)
|
||||
{
|
||||
Set<List<String>> rs = new HashSet<>();
|
||||
doGetJoinPaths(rs, tableName, new ArrayList<>());
|
||||
return (rs);
|
||||
}
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
/*
|
||||
private void doGetJoinPaths(Set<List<String>> joinPaths, String tableName, List<String> path)
|
||||
{
|
||||
for(Edge edge : edges)
|
||||
{
|
||||
if(edge.leftTable.equals(tableName) || edge.rightTable.equals(tableName))
|
||||
{
|
||||
if(path.contains(edge.joinName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> newPath = new ArrayList<>(path);
|
||||
newPath.add(edge.joinName);
|
||||
if(!joinPaths.contains(newPath))
|
||||
{
|
||||
joinPaths.add(newPath);
|
||||
|
||||
String otherTableName = null;
|
||||
if(!edge.leftTable.equals(tableName))
|
||||
{
|
||||
otherTableName = edge.leftTable;
|
||||
}
|
||||
else if(!edge.rightTable.equals(tableName))
|
||||
{
|
||||
otherTableName = edge.rightTable;
|
||||
}
|
||||
|
||||
if(otherTableName != null)
|
||||
{
|
||||
doGetJoinPaths(joinPaths, otherTableName, new ArrayList<>(newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public record Something(String joinTable, List<String> joinPath)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Set<Something> getJoinsBetter(String tableName)
|
||||
{
|
||||
Set<Something> rs = new HashSet<>();
|
||||
Set<String> usedEdges = new HashSet<>();
|
||||
Set<String> tables = new HashSet<>();
|
||||
tables.add(tableName);
|
||||
doGetJoinsBetter(rs, tables, new ArrayList<>(), usedEdges);
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void doGetJoinsBetter(Set<Something> rs, Set<String> tables, List<String> joinPath, Set<String> usedEdges)
|
||||
{
|
||||
for(Edge edge : edges)
|
||||
{
|
||||
if(usedEdges.contains(edge.joinName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(tables.contains(edge.leftTable) || tables.contains(edge.rightTable))
|
||||
{
|
||||
usedEdges.add(edge.joinName);
|
||||
// todo - clone list here, then recurisiv call
|
||||
rs.add(new Something(tables.contains(edge.leftTable) ? edge.rightTable : edge.leftTable, joinPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ public class MetaDataAction
|
||||
}
|
||||
|
||||
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
|
||||
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false));
|
||||
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false));
|
||||
treeNodes.put(tableName, new AppTreeNode(table));
|
||||
}
|
||||
metaDataOutput.setTables(tables);
|
||||
|
@ -54,7 +54,7 @@ public class TableMetaDataAction
|
||||
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
|
||||
}
|
||||
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
|
||||
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true));
|
||||
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true));
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
|
@ -90,6 +90,8 @@ public class QInstanceEnricher
|
||||
|
||||
private final QInstance qInstance;
|
||||
|
||||
private JoinGraph joinGraph;
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// todo - come up w/ a way for app devs to set configs! //
|
||||
//////////////////////////////////////////////////////////
|
||||
@ -148,10 +150,7 @@ public class QInstanceEnricher
|
||||
qInstance.getWidgets().values().forEach(this::enrichWidget);
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(qInstance.getJoins()))
|
||||
{
|
||||
//todo! enrichJoins();
|
||||
}
|
||||
enrichJoins();
|
||||
}
|
||||
|
||||
|
||||
@ -163,9 +162,9 @@ public class QInstanceEnricher
|
||||
{
|
||||
try
|
||||
{
|
||||
JoinGraph joinGraph = new JoinGraph(qInstance);
|
||||
joinGraph = new JoinGraph(qInstance);
|
||||
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
for(QTableMetaData table : CollectionUtils.nonNullMap(qInstance.getTables()).values())
|
||||
{
|
||||
Set<JoinGraph.JoinConnectionList> joinConnections = joinGraph.getJoinConnections(table.getName());
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
|
||||
@ -194,7 +193,7 @@ public class QInstanceEnricher
|
||||
List<JoinGraph.JoinConnectionList> eligibleJoinConnections = new ArrayList<>();
|
||||
for(JoinGraph.JoinConnectionList joinConnection : joinConnections)
|
||||
{
|
||||
if(joinTable.getName().equals(joinConnection.list().get(0).joinTable()))
|
||||
if(joinTable.getName().equals(joinConnection.list().get(joinConnection.list().size() - 1).joinTable()))
|
||||
{
|
||||
eligibleJoinConnections.add(joinConnection);
|
||||
}
|
||||
@ -202,11 +201,18 @@ public class QInstanceEnricher
|
||||
|
||||
if(eligibleJoinConnections.isEmpty())
|
||||
{
|
||||
throw (new QException("Could not infer a joinPath for table [" + table.getName() + "], exposedJoin to [" + exposedJoin.getJoinTable() + "] - no join connections exist in this instance."));
|
||||
throw (new QException("Could not infer a joinPath for table [" + table.getName() + "], exposedJoin to [" + exposedJoin.getJoinTable() + "]: No join connections between these tables exist in this instance."));
|
||||
}
|
||||
else if(eligibleJoinConnections.size() > 1)
|
||||
{
|
||||
throw (new QException("Could not infer a joinPath for table [" + table.getName() + "], exposedJoin to [" + exposedJoin.getJoinTable() + "] - multiple possible join connections exist in this instance: ")); // todo - list the paths so user can choose one!
|
||||
throw (new QException("Could not infer a joinPath for table [" + table.getName() + "], exposedJoin to [" + exposedJoin.getJoinTable() + "]: "
|
||||
+ eligibleJoinConnections.size() + " join connections exist between these tables. You need to specify one:\n"
|
||||
+ StringUtils.join("\n", eligibleJoinConnections.stream().map(jcl -> jcl.getJoinNamesAsString()).toList()) + "."
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
exposedJoin.setJoinPath(eligibleJoinConnections.get(0).getJoinNamesAsList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1089,4 +1095,13 @@ public class QInstanceEnricher
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JoinGraph getJoinGraph()
|
||||
{
|
||||
return (this.joinGraph);
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
@ -69,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
@ -115,17 +117,26 @@ public class QInstanceValidator
|
||||
return;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the enricher will build a join graph (if there are any joins). we'd like to only do that //
|
||||
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
JoinGraph joinGraph = null;
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// before validation, enrich the object (e.g., to fill in values that the user doesn't have to //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// TODO - possible point of customization (use a different enricher, or none, or pass it options).
|
||||
new QInstanceEnricher(qInstance).enrich();
|
||||
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(qInstance);
|
||||
qInstanceEnricher.enrich();
|
||||
joinGraph = qInstanceEnricher.getJoinGraph();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
System.out.println();
|
||||
LOG.error("Error enriching instance prior to validation", e);
|
||||
System.out.println();
|
||||
throw (new QInstanceValidationException("Error enriching qInstance prior to validation.", e));
|
||||
}
|
||||
|
||||
@ -136,7 +147,7 @@ public class QInstanceValidator
|
||||
{
|
||||
validateBackends(qInstance);
|
||||
validateAutomationProviders(qInstance);
|
||||
validateTables(qInstance);
|
||||
validateTables(qInstance, joinGraph);
|
||||
validateProcesses(qInstance);
|
||||
validateReports(qInstance);
|
||||
validateApps(qInstance);
|
||||
@ -158,7 +169,9 @@ public class QInstanceValidator
|
||||
throw (new QInstanceValidationException(errors));
|
||||
}
|
||||
|
||||
qInstance.setHasBeenValidated(new QInstanceValidationKey());
|
||||
QInstanceValidationKey validationKey = new QInstanceValidationKey();
|
||||
qInstance.setHasBeenValidated(validationKey);
|
||||
qInstance.setJoinGraph(validationKey, joinGraph);
|
||||
}
|
||||
|
||||
|
||||
@ -366,7 +379,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateTables(QInstance qInstance)
|
||||
private void validateTables(QInstance qInstance, JoinGraph joinGraph)
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getTables()), "At least 1 table must be defined."))
|
||||
{
|
||||
@ -459,12 +472,61 @@ public class QInstanceValidator
|
||||
validateTableCacheOf(qInstance, table);
|
||||
validateTableRecordSecurityLocks(qInstance, table);
|
||||
validateTableAssociations(qInstance, table);
|
||||
validateExposedJoins(qInstance, joinGraph, table);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateExposedJoins(QInstance qInstance, JoinGraph joinGraph, QTableMetaData table)
|
||||
{
|
||||
Set<JoinGraph.JoinConnectionList> joinConnectionsForTable = null;
|
||||
Set<String> usedLabels = new HashSet<>();
|
||||
Set<List<String>> usedJoinPaths = new HashSet<>();
|
||||
|
||||
String tablePrefix = "Table " + table.getName() + " ";
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
|
||||
{
|
||||
String joinPrefix = tablePrefix + "exposedJoin [missingJoinTableName] ";
|
||||
if(assertCondition(StringUtils.hasContent(exposedJoin.getJoinTable()), tablePrefix + "has an exposedJoin that is missing a joinTable name."))
|
||||
{
|
||||
joinPrefix = tablePrefix + "exposedJoin " + exposedJoin.getJoinTable() + " ";
|
||||
if(assertCondition(qInstance.getTable(exposedJoin.getJoinTable()) != null, joinPrefix + "is referencing an unrecognized table"))
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath."))
|
||||
{
|
||||
joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
|
||||
|
||||
boolean foundJoinConnection = false;
|
||||
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
|
||||
{
|
||||
if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
|
||||
{
|
||||
foundJoinConnection = true;
|
||||
}
|
||||
}
|
||||
assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
|
||||
|
||||
assertCondition(!usedJoinPaths.contains(exposedJoin.getJoinPath()), tablePrefix + "has more than one join with the joinPath: " + exposedJoin.getJoinPath());
|
||||
usedJoinPaths.add(exposedJoin.getJoinPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(assertCondition(StringUtils.hasContent(exposedJoin.getLabel()), joinPrefix + "is missing a label."))
|
||||
{
|
||||
assertCondition(!usedLabels.contains(exposedJoin.getLabel()), tablePrefix + "has more than one join labeled: " + exposedJoin.getLabel());
|
||||
usedLabels.add(exposedJoin.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -30,6 +30,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
|
||||
@ -106,6 +107,8 @@ public class QInstance
|
||||
private Map<String, String> memoizedTablePaths = new HashMap<>();
|
||||
private Map<String, String> memoizedProcessPaths = new HashMap<>();
|
||||
|
||||
private JoinGraph joinGraph;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -1136,4 +1139,30 @@ public class QInstance
|
||||
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JoinGraph getJoinGraph()
|
||||
{
|
||||
return (this.joinGraph);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Only the validation (and enrichment) code should set the instance's joinGraph
|
||||
** so, we take a package-only-constructable validation key as a param along with
|
||||
** the joinGraph - and we throw IllegalArgumentException if a non-null key is given.
|
||||
*******************************************************************************/
|
||||
public void setJoinGraph(QInstanceValidationKey key, JoinGraph joinGraph) throws IllegalArgumentException
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw (new IllegalArgumentException("A ValidationKey must be provided"));
|
||||
}
|
||||
this.joinGraph = joinGraph;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of an ExposedJoin for a frontend to see
|
||||
*******************************************************************************/
|
||||
public class QFrontendExposedJoin
|
||||
{
|
||||
private String label;
|
||||
private Boolean isMany;
|
||||
private QFrontendTableMetaData joinTable;
|
||||
private List<QJoinMetaData> joinPath;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for joinTable
|
||||
*******************************************************************************/
|
||||
public QFrontendTableMetaData getJoinTable()
|
||||
{
|
||||
return (this.joinTable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for joinTable
|
||||
*******************************************************************************/
|
||||
public void setJoinTable(QFrontendTableMetaData joinTable)
|
||||
{
|
||||
this.joinTable = joinTable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for joinTable
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withJoinTable(QFrontendTableMetaData joinTable)
|
||||
{
|
||||
this.joinTable = joinTable;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for joinPath
|
||||
*******************************************************************************/
|
||||
public List<QJoinMetaData> getJoinPath()
|
||||
{
|
||||
return (this.joinPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for joinPath
|
||||
*******************************************************************************/
|
||||
public void setJoinPath(List<QJoinMetaData> joinPath)
|
||||
{
|
||||
this.joinPath = joinPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for joinPath
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withJoinPath(List<QJoinMetaData> joinPath)
|
||||
{
|
||||
this.joinPath = joinPath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add one join to the join path in here
|
||||
*******************************************************************************/
|
||||
public void addJoin(QJoinMetaData join)
|
||||
{
|
||||
if(this.joinPath == null)
|
||||
{
|
||||
this.joinPath = new ArrayList<>();
|
||||
}
|
||||
this.joinPath.add(join);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isMany
|
||||
*******************************************************************************/
|
||||
public Boolean getIsMany()
|
||||
{
|
||||
return (this.isMany);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isMany
|
||||
*******************************************************************************/
|
||||
public void setIsMany(Boolean isMany)
|
||||
{
|
||||
this.isMany = isMany;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isMany
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withIsMany(Boolean isMany)
|
||||
{
|
||||
this.isMany = isMany;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -32,12 +33,16 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
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.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -58,6 +63,8 @@ public class QFrontendTableMetaData
|
||||
private Map<String, QFrontendFieldMetaData> fields;
|
||||
private List<QFieldSection> sections;
|
||||
|
||||
private List<QFrontendExposedJoin> exposedJoins;
|
||||
|
||||
private Set<String> capabilities;
|
||||
|
||||
private boolean readPermission;
|
||||
@ -74,7 +81,7 @@ public class QFrontendTableMetaData
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields)
|
||||
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields, boolean includeJoins)
|
||||
{
|
||||
this.name = tableMetaData.getName();
|
||||
this.label = tableMetaData.getLabel();
|
||||
@ -92,6 +99,27 @@ public class QFrontendTableMetaData
|
||||
this.sections = tableMetaData.getSections();
|
||||
}
|
||||
|
||||
if(includeJoins)
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
this.exposedJoins = new ArrayList<>();
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(tableMetaData.getExposedJoins()))
|
||||
{
|
||||
QFrontendExposedJoin frontendExposedJoin = new QFrontendExposedJoin();
|
||||
this.exposedJoins.add(frontendExposedJoin);
|
||||
|
||||
QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
|
||||
frontendExposedJoin.setLabel(exposedJoin.getLabel());
|
||||
frontendExposedJoin.setIsMany(exposedJoin.getIsMany());
|
||||
frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFields, false));
|
||||
for(String joinName : exposedJoin.getJoinPath())
|
||||
{
|
||||
frontendExposedJoin.addJoin(qInstance.getJoin(joinName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(tableMetaData.getIcon() != null)
|
||||
{
|
||||
this.iconName = tableMetaData.getIcon().getName();
|
||||
@ -259,4 +287,15 @@ public class QFrontendTableMetaData
|
||||
{
|
||||
return deletePermission;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for exposedJoins
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<QFrontendExposedJoin> getExposedJoins()
|
||||
{
|
||||
return exposedJoins;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,12 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -30,10 +36,17 @@ import java.util.List;
|
||||
*******************************************************************************/
|
||||
public class ExposedJoin
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExposedJoin.class);
|
||||
|
||||
private String label;
|
||||
private String joinTable;
|
||||
private List<String> joinPath;
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// no setter for this - derive it the first time it's requested //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
private Boolean isMany = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -46,6 +59,71 @@ public class ExposedJoin
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Boolean getIsMany()
|
||||
{
|
||||
if(isMany == null)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(joinPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// loop backward through the joinPath, starting at the join table (since we don't know the table that this exposedJoin is attached to!) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String currentTable = joinTable;
|
||||
for(int i = joinPath.size() - 1; i >= 0; i--)
|
||||
{
|
||||
String joinName = joinPath.get(i);
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join.getRightTable().equals(currentTable))
|
||||
{
|
||||
currentTable = join.getLeftTable();
|
||||
if(join.getType().equals(JoinType.ONE_TO_MANY) || join.getType().equals(JoinType.MANY_TO_MANY))
|
||||
{
|
||||
isMany = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(join.getLeftTable().equals(currentTable))
|
||||
{
|
||||
currentTable = join.getRightTable();
|
||||
if(join.getType().equals(JoinType.MANY_TO_ONE) || join.getType().equals(JoinType.MANY_TO_MANY))
|
||||
{
|
||||
isMany = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new IllegalStateException("Current join table [" + currentTable + "] in path traversal was not found at element [" + joinName + "]"));
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we successfully got through the loop, and never found a reason to mark this join as "many", then it must not be, so set isMany to false //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(isMany == null)
|
||||
{
|
||||
isMany = false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error deriving if ExposedJoin through [" + joinPath + "] to [" + joinTable + "] isMany", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (isMany);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
|
Reference in New Issue
Block a user