mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +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
|
||||
*******************************************************************************/
|
||||
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class JoinGraphTest extends BaseTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
JoinGraph joinGraph = new JoinGraph(QContext.getQInstance());
|
||||
for(QTableMetaData table : QContext.getQInstance().getTables().values())
|
||||
{
|
||||
Set<String> joins = joinGraph.getJoins(table.getName());
|
||||
if(joins.isEmpty())
|
||||
{
|
||||
System.out.println(table.getName() + " has no joins");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.out.println(table.getName() + " joins: " + joins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -32,7 +32,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
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.joins.JoinOn;
|
||||
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.model.metadata.layout.QAppMetaData;
|
||||
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;
|
||||
@ -41,6 +45,7 @@ import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.utils.TestUtils.APP_NAME_GREETINGS;
|
||||
import static com.kingsrook.qqq.backend.core.utils.TestUtils.APP_NAME_MISCELLANEOUS;
|
||||
import static com.kingsrook.qqq.backend.core.utils.TestUtils.APP_NAME_PEOPLE;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
@ -304,4 +309,131 @@ class QInstanceEnricherTest extends BaseTest
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testExposedJoinPaths()
|
||||
{
|
||||
//////////////////////////
|
||||
// no join path => fail //
|
||||
//////////////////////////
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B")));
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
assertThatThrownBy(() -> new QInstanceEnricher(qInstance).enrich())
|
||||
.rootCause()
|
||||
.hasMessageContaining("Could not infer a joinPath for table [A], exposedJoin to [B]")
|
||||
.hasMessageContaining("No join connections between these tables exist in this instance.");
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// multiple join paths => fail //
|
||||
/////////////////////////////////
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B")));
|
||||
qInstance.addTable(newTable("B", "id", "aId1", "aId2"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB1").withJoinOn(new JoinOn("id", "aId1")).withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB2").withJoinOn(new JoinOn("id", "aId2")).withType(JoinType.ONE_TO_ONE));
|
||||
assertThatThrownBy(() -> new QInstanceEnricher(qInstance).enrich())
|
||||
.rootCause()
|
||||
.hasMessageContaining("Could not infer a joinPath for table [A], exposedJoin to [B]")
|
||||
.hasMessageContaining("2 join connections exist")
|
||||
.hasMessageContaining("\nAB1\n")
|
||||
.hasMessageContaining("\nAB2.");
|
||||
|
||||
////////////////////////////////////////////
|
||||
// but if you specify a path, you're good //
|
||||
////////////////////////////////////////////
|
||||
qInstance.getTable("A").getExposedJoins().get(0).setJoinPath(List.of("AB2"));
|
||||
new QInstanceEnricher(qInstance).enrich();
|
||||
assertEquals("B", qInstance.getTable("A").getExposedJoins().get(0).getLabel());
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// multiple join paths => fail //
|
||||
/////////////////////////////////
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("C")));
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
qInstance.addTable(newTable("C", "id", "bId", "aId"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withJoinOn(new JoinOn("id", "aId")).withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("B").withRightTable("C").withName("BC").withJoinOn(new JoinOn("id", "bId")).withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("C").withName("AC").withJoinOn(new JoinOn("id", "aId")).withType(JoinType.ONE_TO_ONE));
|
||||
assertThatThrownBy(() -> new QInstanceEnricher(qInstance).enrich())
|
||||
.rootCause()
|
||||
.hasMessageContaining("Could not infer a joinPath for table [A], exposedJoin to [C]")
|
||||
.hasMessageContaining("2 join connections exist")
|
||||
.hasMessageContaining("\nAB, BC\n")
|
||||
.hasMessageContaining("\nAC.");
|
||||
|
||||
////////////////////////////////////////////
|
||||
// but if you specify a path, you're good //
|
||||
////////////////////////////////////////////
|
||||
qInstance.getTable("A").getExposedJoins().get(0).setJoinPath(List.of("AB", "BC"));
|
||||
new QInstanceEnricher(qInstance).enrich();
|
||||
assertEquals("C", qInstance.getTable("A").getExposedJoins().get(0).getLabel());
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// even if you specify a bogus path, Enricher doesn't care - see validator to care. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("not-a-join"))));
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
new QInstanceEnricher(qInstance).enrich();
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// one join path => great success //
|
||||
////////////////////////////////////
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
qInstance.addTable(newTable("A", "id")
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable("B"))
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable("C")));
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
qInstance.addTable(newTable("C", "id", "bId"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withJoinOn(new JoinOn("id", "aId")).withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("B").withRightTable("C").withName("BC").withJoinOn(new JoinOn("id", "bId")).withType(JoinType.ONE_TO_ONE));
|
||||
|
||||
new QInstanceEnricher(qInstance).enrich();
|
||||
|
||||
ExposedJoin exposedJoinAB = qInstance.getTable("A").getExposedJoins().get(0);
|
||||
assertEquals("B", exposedJoinAB.getLabel());
|
||||
assertEquals(List.of("AB"), exposedJoinAB.getJoinPath());
|
||||
|
||||
ExposedJoin exposedJoinAC = qInstance.getTable("A").getExposedJoins().get(1);
|
||||
assertEquals("C", exposedJoinAC.getLabel());
|
||||
assertEquals(List.of("AB", "BC"), exposedJoinAC.getJoinPath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QTableMetaData newTable(String tableName, String... fieldNames)
|
||||
{
|
||||
QTableMetaData tableMetaData = new QTableMetaData()
|
||||
.withName(tableName)
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withPrimaryKeyField(fieldNames[0]);
|
||||
|
||||
for(String fieldName : fieldNames)
|
||||
{
|
||||
tableMetaData.addField(new QFieldMetaData(fieldName, QFieldType.INTEGER));
|
||||
}
|
||||
|
||||
return (tableMetaData);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
|
||||
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.joins.JoinOn;
|
||||
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.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
@ -65,6 +68,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
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;
|
||||
@ -1671,6 +1675,83 @@ class QInstanceValidatorTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testExposedJoinPaths()
|
||||
{
|
||||
assertValidationFailureReasons(qInstance -> qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin())),
|
||||
"Table A has an exposedJoin that is missing a joinTable name",
|
||||
"Table A exposedJoin [missingJoinTableName] is missing a label");
|
||||
|
||||
assertValidationFailureReasons(qInstance -> qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B"))),
|
||||
"Table A exposedJoin B is referencing an unrecognized table",
|
||||
"Table A exposedJoin B is missing a label");
|
||||
|
||||
assertValidationFailureReasons(qInstance ->
|
||||
{
|
||||
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B").withJoinPath(List.of("notAJoin"))));
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
|
||||
},
|
||||
"does not match a valid join connection in the instance");
|
||||
|
||||
assertValidationFailureReasons(qInstance ->
|
||||
{
|
||||
qInstance.addTable(newTable("A", "id")
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("foo").withJoinPath(List.of("AB")))
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable("C").withLabel("foo").withJoinPath(List.of("AC")))
|
||||
);
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
qInstance.addTable(newTable("C", "id", "aId"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("C").withName("AC").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
|
||||
},
|
||||
"more than one join labeled: foo");
|
||||
|
||||
assertValidationFailureReasons(qInstance ->
|
||||
{
|
||||
qInstance.addTable(newTable("A", "id")
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B1").withJoinPath(List.of("AB")))
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B2").withJoinPath(List.of("AB")))
|
||||
);
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
|
||||
},
|
||||
"than one join with the joinPath: [AB]");
|
||||
|
||||
assertValidationSuccess(qInstance ->
|
||||
{
|
||||
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B").withJoinPath(List.of("AB"))));
|
||||
qInstance.addTable(newTable("B", "id", "aId"));
|
||||
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QTableMetaData newTable(String tableName, String... fieldNames)
|
||||
{
|
||||
QTableMetaData tableMetaData = new QTableMetaData()
|
||||
.withName(tableName)
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withPrimaryKeyField(fieldNames[0]);
|
||||
|
||||
for(String fieldName : fieldNames)
|
||||
{
|
||||
tableMetaData.addField(new QFieldMetaData(fieldName, QFieldType.INTEGER));
|
||||
}
|
||||
|
||||
return (tableMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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 org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for ExposedJoin
|
||||
*******************************************************************************/
|
||||
class ExposedJoinTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testIsManyOneToOne()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("AB"))));
|
||||
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("A").withJoinPath(List.of("AB"))));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_ONE));
|
||||
|
||||
assertFalse(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
|
||||
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testIsManyOneToMany()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("AB"))));
|
||||
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("A").withJoinPath(List.of("AB"))));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_MANY));
|
||||
|
||||
assertTrue(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
|
||||
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testIsManyManyToOne()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("AB"))));
|
||||
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("A").withJoinPath(List.of("AB"))));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.MANY_TO_ONE));
|
||||
|
||||
assertFalse(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
|
||||
assertTrue(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testIsManyOneToOneThroughChain()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("G").withJoinPath(List.of("AB", "BC", "CD", "DE", "EF", "FG"))));
|
||||
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("G").withJoinPath(List.of("BC", "CD", "DE", "EF", "FG"))));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("BC").withLeftTable("B").withRightTable("C").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("CD").withLeftTable("C").withRightTable("D").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("DE").withLeftTable("D").withRightTable("E").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("EF").withLeftTable("E").withRightTable("F").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("FG").withLeftTable("F").withRightTable("G").withType(JoinType.ONE_TO_ONE));
|
||||
|
||||
assertFalse(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
|
||||
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testIsManyOneToManyThroughChain()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("G").withJoinPath(List.of("AB", "BC", "CD", "DE", "EF", "FG"))));
|
||||
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("E").withJoinPath(List.of("BC", "CD", "DE"))));
|
||||
qInstance.addTable(new QTableMetaData().withName("F").withExposedJoin(new ExposedJoin().withJoinTable("C").withJoinPath(List.of("FG", "EF", "DE", "CD"))));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("BC").withLeftTable("B").withRightTable("C").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("CD").withLeftTable("C").withRightTable("D").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("DE").withLeftTable("D").withRightTable("E").withType(JoinType.ONE_TO_ONE));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("EF").withLeftTable("E").withRightTable("F").withType(JoinType.ONE_TO_MANY));
|
||||
qInstance.addJoin(new QJoinMetaData().withName("FG").withLeftTable("F").withRightTable("G").withType(JoinType.ONE_TO_ONE));
|
||||
|
||||
assertTrue(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
|
||||
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
|
||||
assertFalse(qInstance.getTable("F").getExposedJoins().get(0).getIsMany());
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user