add exposed joins to frontend metadata; checkpoing on validation & enrichment of eposed joins

This commit is contained in:
2023-04-24 12:11:46 -05:00
parent d086284de7
commit 2495989584
13 changed files with 833 additions and 300 deletions

View File

@ -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));
}
}
}
}

View File

@ -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);

View File

@ -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

View File

@ -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);
}
}

View File

@ -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());
}
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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
*******************************************************************************/

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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());
}
}