From 249598958482d04be2aa5c9f74d440021e608ca3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 24 Apr 2023 12:11:46 -0500 Subject: [PATCH] add exposed joins to frontend metadata; checkpoing on validation & enrichment of eposed joins --- .../core/actions/metadata/JoinGraph.java | 292 ++++-------------- .../core/actions/metadata/MetaDataAction.java | 2 +- .../actions/metadata/TableMetaDataAction.java | 2 +- .../core/instances/QInstanceEnricher.java | 33 +- .../core/instances/QInstanceValidator.java | 70 ++++- .../core/model/metadata/QInstance.java | 29 ++ .../frontend/QFrontendExposedJoin.java | 178 +++++++++++ .../frontend/QFrontendTableMetaData.java | 41 ++- .../model/metadata/tables/ExposedJoin.java | 78 +++++ .../core/actions/metadata/JoinGraphTest.java | 58 ---- .../core/instances/QInstanceEnricherTest.java | 132 ++++++++ .../instances/QInstanceValidatorTest.java | 81 +++++ .../metadata/tables/ExposedJoinTest.java | 137 ++++++++ 13 files changed, 833 insertions(+), 300 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java delete mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraphTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoinTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraph.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraph.java index 26f6bccc..fc597ed7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraph.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraph.java @@ -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 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 nodes = new HashSet<>(); - private Set edges = new HashSet<>(); - - - /******************************************************************************* ** Constructor ** *******************************************************************************/ public JoinGraph(QInstance qInstance) { - Set usedJoins = new HashSet<>(); - for(QJoinMetaData join : qInstance.getJoins().values()) + Set 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 getJoins(String tableName) - { - Set rs = new HashSet<>(); - Set 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 { @@ -240,6 +214,49 @@ public class JoinGraph return (this.list.size() - that.list.size()); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public boolean matchesJoinPath(List 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 getJoinNamesAsList() + { + return (list().stream().map(jc -> jc.viaJoinName()).toList()); + } } @@ -316,181 +333,4 @@ public class JoinGraph return (false); } - - - public record JoinPath(String joinTable, List joinNames) - { - - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public Set getJoinPaths(String tableName) - { - Set rs = new HashSet<>(); - doGetJoinPaths(rs, tableName, new ArrayList<>()); - return (rs); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private void doGetJoinPaths(Set joinPaths, String tableName, List path) - { - for(Edge edge : edges) - { - if(edge.leftTable.equals(tableName) || edge.rightTable.equals(tableName)) - { - if(path.contains(edge.joinName)) - { - continue; - } - - List 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 joinPaths, List newPath) - { - for(JoinPath joinPath : joinPaths) - { - if(joinPath.joinNames().equals(newPath)) - { - return (true); - } - } - return (false); - } - - /******************************************************************************* - ** - *******************************************************************************/ - /* - public Set> getJoinPaths(String tableName) - { - Set> rs = new HashSet<>(); - doGetJoinPaths(rs, tableName, new ArrayList<>()); - return (rs); - } - */ - - /******************************************************************************* - ** - *******************************************************************************/ - /* - private void doGetJoinPaths(Set> joinPaths, String tableName, List path) - { - for(Edge edge : edges) - { - if(edge.leftTable.equals(tableName) || edge.rightTable.equals(tableName)) - { - if(path.contains(edge.joinName)) - { - continue; - } - - List 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 joinPath) - { - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public Set getJoinsBetter(String tableName) - { - Set rs = new HashSet<>(); - Set usedEdges = new HashSet<>(); - Set tables = new HashSet<>(); - tables.add(tableName); - doGetJoinsBetter(rs, tables, new ArrayList<>(), usedEdges); - - return (rs); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private void doGetJoinsBetter(Set rs, Set tables, List joinPath, Set 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)); - } - } - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index 6d2d70eb..f0bde491 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -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); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java index 223eb475..46c4f243 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java @@ -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 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 70185db0..6820042f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -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 joinConnections = joinGraph.getJoinConnections(table.getName()); for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins())) @@ -194,7 +193,7 @@ public class QInstanceEnricher List 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); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 7dfa6963..938b5944 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -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 joinConnectionsForTable = null; + Set usedLabels = new HashSet<>(); + Set> 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()); + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index be3b7074..0b9f45ee 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -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 memoizedTablePaths = new HashMap<>(); private Map 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; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java new file mode 100644 index 00000000..f89816c8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java @@ -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 . + */ + +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 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 getJoinPath() + { + return (this.joinPath); + } + + + + /******************************************************************************* + ** Setter for joinPath + *******************************************************************************/ + public void setJoinPath(List joinPath) + { + this.joinPath = joinPath; + } + + + + /******************************************************************************* + ** Fluent setter for joinPath + *******************************************************************************/ + public QFrontendExposedJoin withJoinPath(List 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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java index c53cc933..199d42a6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java @@ -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 fields; private List sections; + private List exposedJoins; + private Set 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 getExposedJoins() + { + return exposedJoins; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java index 6588cb21..f66b1b23 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java @@ -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 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 *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraphTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraphTest.java deleted file mode 100644 index 344a8c3b..00000000 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/JoinGraphTest.java +++ /dev/null @@ -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 . - */ - -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 joins = joinGraph.getJoins(table.getName()); - if(joins.isEmpty()) - { - System.out.println(table.getName() + " has no joins"); - } - else - { - System.out.println(table.getName() + " joins: " + joins); - } - } - } - -} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index ce9ffe45..4deb3559 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -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); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 361343cf..e6e50a68 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -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); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoinTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoinTest.java new file mode 100644 index 00000000..def4624d --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoinTest.java @@ -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 . + */ + +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()); + } + +} \ No newline at end of file