mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 14:10:44 +00:00
Merge branch 'feature/CTLE-207-query-joins' into integration/sprint-25
# Conflicts: # qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java # qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
This commit is contained in:
@ -239,7 +239,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
// new AuditAction().executeAsync(auditInput); // todo async??? maybe get that from rules???
|
||||
new AuditAction().execute(auditInput);
|
||||
long end = System.currentTimeMillis();
|
||||
LOG.debug("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start)));
|
||||
LOG.trace("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
@ -194,13 +194,13 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(record.getValue(joinOn.getLeftField()))));
|
||||
}
|
||||
filter.setOrderBys(join.getOrderBys());
|
||||
filter.setLimit(maxRows);
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
queryInput.setLimit(maxRows);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
QTableMetaData table = input.getInstance().getTable(join.getRightTable());
|
||||
|
@ -0,0 +1,336 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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 Set<Edge> edges = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Graph edge (no graph nodes needed in here)
|
||||
*******************************************************************************/
|
||||
private record Edge(String joinName, String leftTable, String rightTable)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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;
|
||||
private String joinFieldA;
|
||||
private String joinFieldB;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public NormalizedJoin(QJoinMetaData joinMetaData)
|
||||
{
|
||||
boolean needFlip = false;
|
||||
int tableCompare = joinMetaData.getLeftTable().compareTo(joinMetaData.getRightTable());
|
||||
if(tableCompare < 0)
|
||||
{
|
||||
needFlip = true;
|
||||
}
|
||||
else if(tableCompare == 0)
|
||||
{
|
||||
int fieldCompare = joinMetaData.getJoinOns().get(0).getLeftField().compareTo(joinMetaData.getJoinOns().get(0).getRightField());
|
||||
if(fieldCompare < 0)
|
||||
{
|
||||
needFlip = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(needFlip)
|
||||
{
|
||||
joinMetaData = joinMetaData.flip();
|
||||
}
|
||||
|
||||
tableA = joinMetaData.getLeftTable();
|
||||
tableB = joinMetaData.getRightTable();
|
||||
joinFieldA = joinMetaData.getJoinOns().get(0).getLeftField();
|
||||
joinFieldB = joinMetaData.getJoinOns().get(0).getRightField();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(tableA, tableB, joinFieldA, joinFieldB);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JoinGraph(QInstance qInstance)
|
||||
{
|
||||
Set<NormalizedJoin> usedJoins = new HashSet<>();
|
||||
for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
|
||||
{
|
||||
NormalizedJoin normalizedJoin = new NormalizedJoin(join);
|
||||
if(usedJoins.contains(normalizedJoin))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
usedJoins.add(normalizedJoin);
|
||||
edges.add(new Edge(join.getName(), join.getLeftTable(), join.getRightTable()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public record JoinConnection(String joinTable, String viaJoinName) implements Comparable<JoinConnection>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int compareTo(JoinConnection that)
|
||||
{
|
||||
Comparator<JoinConnection> comparator = Comparator.comparing((JoinConnection jc) -> jc.joinTable())
|
||||
.thenComparing((JoinConnection jc) -> jc.viaJoinName());
|
||||
return (comparator.compare(this, that));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public record JoinConnectionList(List<JoinConnection> list) implements Comparable<JoinConnectionList>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JoinConnectionList copy()
|
||||
{
|
||||
return new JoinConnectionList(new ArrayList<>(list));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public int compareTo(JoinConnectionList that)
|
||||
{
|
||||
if(this.equals(that))
|
||||
{
|
||||
return (0);
|
||||
}
|
||||
|
||||
for(int i = 0; i < Math.min(this.list.size(), that.list.size()); i++)
|
||||
{
|
||||
int comp = this.list.get(i).compareTo(that.list.get(i));
|
||||
if(comp != 0)
|
||||
{
|
||||
return (comp);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Set<JoinConnectionList> getJoinConnections(String tableName)
|
||||
{
|
||||
Set<JoinConnectionList> rs = new TreeSet<>();
|
||||
doGetJoinConnections(rs, tableName, new ArrayList<>(), new JoinConnectionList(new ArrayList<>()));
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void doGetJoinConnections(Set<JoinConnectionList> joinConnections, String tableName, List<String> path, JoinConnectionList connectionList)
|
||||
{
|
||||
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(!joinConnectionsContain(joinConnections, newPath))
|
||||
{
|
||||
String otherTableName = null;
|
||||
if(!edge.leftTable.equals(tableName))
|
||||
{
|
||||
otherTableName = edge.leftTable;
|
||||
}
|
||||
else if(!edge.rightTable.equals(tableName))
|
||||
{
|
||||
otherTableName = edge.rightTable;
|
||||
}
|
||||
|
||||
if(otherTableName != null)
|
||||
{
|
||||
|
||||
JoinConnectionList newConnectionList = connectionList.copy();
|
||||
JoinConnection joinConnection = new JoinConnection(otherTableName, edge.joinName);
|
||||
newConnectionList.list.add(joinConnection);
|
||||
joinConnections.add(newConnectionList);
|
||||
doGetJoinConnections(joinConnections, otherTableName, new ArrayList<>(newPath), newConnectionList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean joinConnectionsContain(Set<JoinConnectionList> joinPaths, List<String> newPath)
|
||||
{
|
||||
for(JoinConnectionList joinConnections : joinPaths)
|
||||
{
|
||||
List<String> joinConnectionJoins = joinConnections.list.stream().map(jc -> jc.viaJoinName).toList();
|
||||
if(joinConnectionJoins.equals(newPath))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
}
|
@ -83,11 +83,14 @@ 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);
|
||||
|
||||
// addJoinsToTables(tables);
|
||||
// addJoinedTablesToTables(tables);
|
||||
|
||||
////////////////////////////////////////
|
||||
// map processes to frontend metadata //
|
||||
////////////////////////////////////////
|
||||
|
@ -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
|
||||
|
||||
|
@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
|
||||
@ -32,6 +36,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
@ -41,10 +46,13 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
@ -95,15 +103,25 @@ public class ExportAction
|
||||
///////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
|
||||
{
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
List<String> badFieldNames = new ArrayList<>();
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
Map<String, QTableMetaData> joinTableMap = getJoinTableMap(table);
|
||||
|
||||
List<String> badFieldNames = new ArrayList<>();
|
||||
for(String fieldName : exportInput.getFieldNames())
|
||||
{
|
||||
try
|
||||
{
|
||||
table.getField(fieldName);
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String[] parts = fieldName.split("\\.", 2);
|
||||
joinTableMap.get(parts[0]).getField(parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
table.getField(fieldName);
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
catch(Exception e)
|
||||
{
|
||||
badFieldNames.add(fieldName);
|
||||
}
|
||||
@ -128,6 +146,21 @@ public class ExportAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Map<String, QTableMetaData> getJoinTableMap(QTableMetaData table)
|
||||
{
|
||||
Map<String, QTableMetaData> joinTableMap = new HashMap<>();
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
|
||||
{
|
||||
joinTableMap.put(exposedJoin.getJoinTable(), QContext.getQInstance().getTable(exposedJoin.getJoinTable()));
|
||||
}
|
||||
return joinTableMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run the report.
|
||||
*******************************************************************************/
|
||||
@ -151,7 +184,33 @@ public class ExportAction
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(exportInput.getTableName());
|
||||
queryInput.setFilter(exportInput.getQueryFilter());
|
||||
queryInput.setLimit(exportInput.getLimit());
|
||||
|
||||
List<QueryJoin> queryJoins = new ArrayList<>();
|
||||
Set<String> addedJoinNames = new HashSet<>();
|
||||
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
|
||||
{
|
||||
for(String fieldName : exportInput.getFieldNames())
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String[] parts = fieldName.split("\\.", 2);
|
||||
String joinTableName = parts[0];
|
||||
if(!addedJoinNames.contains(joinTableName))
|
||||
{
|
||||
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
|
||||
addedJoinNames.add(joinTableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryInput.setQueryJoins(queryJoins);
|
||||
|
||||
if(queryInput.getFilter() == null)
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter());
|
||||
}
|
||||
queryInput.getFilter().setLimit(exportInput.getLimit());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
@ -298,11 +357,29 @@ public class ExportAction
|
||||
*******************************************************************************/
|
||||
private List<QFieldMetaData> getFields(ExportInput exportInput)
|
||||
{
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
Map<String, QTableMetaData> joinTableMap = getJoinTableMap(table);
|
||||
|
||||
List<QFieldMetaData> fieldList;
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
if(exportInput.getFieldNames() != null)
|
||||
{
|
||||
fieldList = exportInput.getFieldNames().stream().map(table::getField).toList();
|
||||
fieldList = new ArrayList<>();
|
||||
for(String fieldName : exportInput.getFieldNames())
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String[] parts = fieldName.split("\\.", 2);
|
||||
QTableMetaData joinTable = joinTableMap.get(parts[0]);
|
||||
QFieldMetaData field = joinTable.getField(parts[1]).clone();
|
||||
field.setName(fieldName);
|
||||
field.setLabel(joinTable.getLabel() + ": " + field.getLabel());
|
||||
fieldList.add(field);
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldList.add(table.getField(fieldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -156,8 +156,7 @@ public class StoreAssociatedScriptAction
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
|
||||
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
|
||||
);
|
||||
queryInput.setLimit(1);
|
||||
.withLimit(1));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
if(!queryOutput.getRecords().isEmpty())
|
||||
{
|
||||
|
@ -123,7 +123,7 @@ public class QPossibleValueTranslator
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
|
||||
LOG.trace("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
|
||||
primePvsCache(table, records, queryJoins, limitedToFieldNames);
|
||||
|
||||
for(QRecord record : records)
|
||||
@ -378,11 +378,11 @@ public class QPossibleValueTranslator
|
||||
for(String valueField : valueFields)
|
||||
{
|
||||
Object value = switch(valueField)
|
||||
{
|
||||
case "id" -> id;
|
||||
case "label" -> label;
|
||||
default -> throw new IllegalArgumentException("Unexpected value field: " + valueField);
|
||||
};
|
||||
{
|
||||
case "id" -> id;
|
||||
case "label" -> label;
|
||||
default -> throw new IllegalArgumentException("Unexpected value field: " + valueField);
|
||||
};
|
||||
values.add(Objects.requireNonNullElse(value, ""));
|
||||
}
|
||||
}
|
||||
@ -427,7 +427,7 @@ public class QPossibleValueTranslator
|
||||
int size = entry.getValue().size();
|
||||
if(size > 50_000)
|
||||
{
|
||||
LOG.debug("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size));
|
||||
LOG.info("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size));
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,7 +483,7 @@ public class QPossibleValueTranslator
|
||||
{
|
||||
if(limitedToFieldNames != null && !limitedToFieldNames.contains(fieldNamePrefix + field.getName()))
|
||||
{
|
||||
LOG.debug("Skipping cache priming for translation of possible value field [" + fieldNamePrefix + field.getName() + "] - it's not in the limitedToFieldNames set.");
|
||||
LOG.trace("Skipping cache priming for translation of possible value field [" + fieldNamePrefix + field.getName() + "] - it's not in the limitedToFieldNames set.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -556,7 +556,7 @@ public class QPossibleValueTranslator
|
||||
queryInput.setFieldsToTranslatePossibleValues(possibleValueFieldsToTranslate);
|
||||
}
|
||||
|
||||
LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
|
||||
LOG.trace("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -247,7 +247,7 @@ public class SearchPossibleValueSourceAction
|
||||
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
|
||||
|
||||
// todo - skip & limit as params
|
||||
queryInput.setLimit(250);
|
||||
queryFilter.setLimit(250);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
|
||||
|
@ -32,7 +32,9 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
@ -59,6 +61,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
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.QMiddlewareTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
@ -87,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! //
|
||||
//////////////////////////////////////////////////////////
|
||||
@ -144,6 +149,81 @@ public class QInstanceEnricher
|
||||
{
|
||||
qInstance.getWidgets().values().forEach(this::enrichWidget);
|
||||
}
|
||||
|
||||
enrichJoins();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void enrichJoins()
|
||||
{
|
||||
try
|
||||
{
|
||||
joinGraph = new JoinGraph(qInstance);
|
||||
|
||||
for(QTableMetaData table : CollectionUtils.nonNullMap(qInstance.getTables()).values())
|
||||
{
|
||||
Set<JoinGraph.JoinConnectionList> joinConnections = joinGraph.getJoinConnections(table.getName());
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// proceed with caution - remember, validator will fail the instance if things are missing/invalid //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(exposedJoin.getJoinTable() != null)
|
||||
{
|
||||
QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
|
||||
if(joinTable != null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// default the exposed join's label to the join table's label, if it wasn't set //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if(!StringUtils.hasContent(exposedJoin.getLabel()))
|
||||
{
|
||||
exposedJoin.setLabel(joinTable.getLabel());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// default the exposed join's join-path from the joinGraph, if it wasn't set //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeIsEmpty(exposedJoin.getJoinPath()))
|
||||
{
|
||||
List<JoinGraph.JoinConnectionList> eligibleJoinConnections = new ArrayList<>();
|
||||
for(JoinGraph.JoinConnectionList joinConnection : joinConnections)
|
||||
{
|
||||
if(joinTable.getName().equals(joinConnection.list().get(joinConnection.list().size() - 1).joinTable()))
|
||||
{
|
||||
eligibleJoinConnections.add(joinConnection);
|
||||
}
|
||||
}
|
||||
|
||||
if(eligibleJoinConnections.isEmpty())
|
||||
{
|
||||
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() + "]: "
|
||||
+ 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new RuntimeException("Error enriching instance joins", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1016,4 +1096,13 @@ public class QInstanceEnricher
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JoinGraph getJoinGraph()
|
||||
{
|
||||
return (this.joinGraph);
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
@ -69,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
@ -115,17 +117,26 @@ public class QInstanceValidator
|
||||
return;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the enricher will build a join graph (if there are any joins). we'd like to only do that //
|
||||
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
JoinGraph joinGraph = null;
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// before validation, enrich the object (e.g., to fill in values that the user doesn't have to //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// TODO - possible point of customization (use a different enricher, or none, or pass it options).
|
||||
new QInstanceEnricher(qInstance).enrich();
|
||||
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(qInstance);
|
||||
qInstanceEnricher.enrich();
|
||||
joinGraph = qInstanceEnricher.getJoinGraph();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
System.out.println();
|
||||
LOG.error("Error enriching instance prior to validation", e);
|
||||
System.out.println();
|
||||
throw (new QInstanceValidationException("Error enriching qInstance prior to validation.", e));
|
||||
}
|
||||
|
||||
@ -136,7 +147,7 @@ public class QInstanceValidator
|
||||
{
|
||||
validateBackends(qInstance);
|
||||
validateAutomationProviders(qInstance);
|
||||
validateTables(qInstance);
|
||||
validateTables(qInstance, joinGraph);
|
||||
validateProcesses(qInstance);
|
||||
validateReports(qInstance);
|
||||
validateApps(qInstance);
|
||||
@ -158,7 +169,9 @@ public class QInstanceValidator
|
||||
throw (new QInstanceValidationException(errors));
|
||||
}
|
||||
|
||||
qInstance.setHasBeenValidated(new QInstanceValidationKey());
|
||||
QInstanceValidationKey validationKey = new QInstanceValidationKey();
|
||||
qInstance.setHasBeenValidated(validationKey);
|
||||
qInstance.setJoinGraph(validationKey, joinGraph);
|
||||
}
|
||||
|
||||
|
||||
@ -366,7 +379,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateTables(QInstance qInstance)
|
||||
private void validateTables(QInstance qInstance, JoinGraph joinGraph)
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getTables()), "At least 1 table must be defined."))
|
||||
{
|
||||
@ -466,12 +479,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -37,7 +37,8 @@ public class CountInput extends AbstractTableActionInput
|
||||
{
|
||||
private QQueryFilter filter;
|
||||
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
private Boolean includeDistinctCount = false;
|
||||
|
||||
|
||||
|
||||
@ -120,4 +121,35 @@ public class CountInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for includeDistinctCount
|
||||
*******************************************************************************/
|
||||
public Boolean getIncludeDistinctCount()
|
||||
{
|
||||
return (this.includeDistinctCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for includeDistinctCount
|
||||
*******************************************************************************/
|
||||
public void setIncludeDistinctCount(Boolean includeDistinctCount)
|
||||
{
|
||||
this.includeDistinctCount = includeDistinctCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for includeDistinctCount
|
||||
*******************************************************************************/
|
||||
public CountInput withIncludeDistinctCount(Boolean includeDistinctCount)
|
||||
{
|
||||
this.includeDistinctCount = includeDistinctCount;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
public class CountOutput extends AbstractActionOutput
|
||||
{
|
||||
private Integer count;
|
||||
private Integer distinctCount;
|
||||
|
||||
|
||||
|
||||
@ -52,4 +53,47 @@ public class CountOutput extends AbstractActionOutput
|
||||
{
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for distinctCount
|
||||
*******************************************************************************/
|
||||
public Integer getDistinctCount()
|
||||
{
|
||||
return (this.distinctCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for distinctCount
|
||||
*******************************************************************************/
|
||||
public void setDistinctCount(Integer distinctCount)
|
||||
{
|
||||
this.distinctCount = distinctCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for distinctCount
|
||||
*******************************************************************************/
|
||||
public CountOutput withDistinctCount(Integer distinctCount)
|
||||
{
|
||||
this.distinctCount = distinctCount;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for count
|
||||
*******************************************************************************/
|
||||
public CountOutput withCount(Integer count)
|
||||
{
|
||||
this.count = count;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,12 +32,16 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -46,6 +50,8 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
*******************************************************************************/
|
||||
public class JoinsContext
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(JoinsContext.class);
|
||||
|
||||
private final QInstance instance;
|
||||
private final String mainTableName;
|
||||
private final List<QueryJoin> queryJoins;
|
||||
@ -65,7 +71,7 @@ public class JoinsContext
|
||||
{
|
||||
this.instance = instance;
|
||||
this.mainTableName = tableName;
|
||||
this.queryJoins = CollectionUtils.nonNullList(queryJoins);
|
||||
this.queryJoins = new MutableList<>(queryJoins);
|
||||
|
||||
for(QueryJoin queryJoin : this.queryJoins)
|
||||
{
|
||||
@ -123,6 +129,8 @@ public class JoinsContext
|
||||
|
||||
ensureFilterIsRepresented(filter);
|
||||
|
||||
addJoinsFromExposedJoinPaths();
|
||||
|
||||
/* todo!!
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
@ -132,7 +140,147 @@ public class JoinsContext
|
||||
// addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
|
||||
}
|
||||
}
|
||||
*/
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** If there are any joins in the context that don't have a join meta data, see
|
||||
** if we can find the JoinMetaData to use for them by looking at the main table's
|
||||
** exposed joins, and using their join paths.
|
||||
*******************************************************************************/
|
||||
private void addJoinsFromExposedJoinPaths() throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// do a double-loop, to avoid concurrent modification on the queryJoins list. //
|
||||
// that is to say, we'll loop over that list, but possibly add things to it, //
|
||||
// in which case we'll set this flag, and break the inner loop, to go again. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
boolean addedJoin;
|
||||
do
|
||||
{
|
||||
addedJoin = false;
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// if the join has joinMetaData, then we don't need to process it. //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
if(queryJoin.getJoinMetaData() == null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// try to find a direct join between the main table and this table. //
|
||||
// if one is found, then put it (the meta data) on the query join. //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
String baseTableName = Objects.requireNonNullElse(resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), mainTableName);
|
||||
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
|
||||
if(found != null)
|
||||
{
|
||||
queryJoin.setJoinMetaData(found);
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, the join must be indirect - so look for an exposedJoin that will have a joinPath that will connect us //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.debug("Looking for an exposed join...", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()));
|
||||
|
||||
QTableMetaData mainTable = instance.getTable(mainTableName);
|
||||
boolean addedAnyQueryJoins = false;
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins()))
|
||||
{
|
||||
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
|
||||
{
|
||||
LOG.debug("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// loop backward through the join path (from the joinTable back to the main table) //
|
||||
// adding joins to the table (if they aren't already in the query) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
String tmpTable = queryJoin.getJoinTable();
|
||||
for(int i = exposedJoin.getJoinPath().size() - 1; i >= 0; i--)
|
||||
{
|
||||
String joinName = exposedJoin.getJoinPath().get(i);
|
||||
QJoinMetaData joinToAdd = instance.getJoin(joinName);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// get the name from the opposite side of the join (flipping it if needed) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
String nextTable;
|
||||
if(joinToAdd.getRightTable().equals(tmpTable))
|
||||
{
|
||||
nextTable = joinToAdd.getLeftTable();
|
||||
}
|
||||
else
|
||||
{
|
||||
nextTable = joinToAdd.getRightTable();
|
||||
joinToAdd = joinToAdd.flip();
|
||||
}
|
||||
|
||||
if(doesJoinNeedAddedToQuery(joinName))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is the last element in the joinPath, then we want to set this joinMetaData on the outer queryJoin //
|
||||
// - else, we need to add a new queryJoin to this context //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(i == exposedJoin.getJoinPath().size() - 1)
|
||||
{
|
||||
if(queryJoin.getBaseTableOrAlias() == null)
|
||||
{
|
||||
queryJoin.setBaseTableOrAlias(nextTable);
|
||||
}
|
||||
queryJoin.setJoinMetaData(joinToAdd);
|
||||
}
|
||||
else
|
||||
{
|
||||
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
|
||||
queryJoinToAdd.setType(queryJoin.getType());
|
||||
addedAnyQueryJoins = true;
|
||||
this.queryJoins.add(queryJoinToAdd); // todo something else with aliases? probably.
|
||||
processQueryJoin(queryJoinToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
tmpTable = nextTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// break the inner loop (it would fail due to a concurrent modification), but continue the outer //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(addedAnyQueryJoins)
|
||||
{
|
||||
addedJoin = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while(addedJoin);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean doesJoinNeedAddedToQuery(String joinName)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look at all queryJoins already in context - if any have this join's name, then we don't need this join... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
@ -256,34 +404,54 @@ public class JoinsContext
|
||||
{
|
||||
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
|
||||
{
|
||||
boolean found = false;
|
||||
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
|
||||
{
|
||||
QueryJoin queryJoin = null;
|
||||
if(join.getLeftTable().equals(mainTableName) && join.getRightTable().equals(filterTable))
|
||||
{
|
||||
queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
}
|
||||
else
|
||||
{
|
||||
join = join.flip();
|
||||
if(join.getLeftTable().equals(mainTableName) && join.getRightTable().equals(filterTable))
|
||||
{
|
||||
queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
}
|
||||
}
|
||||
|
||||
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
|
||||
if(queryJoin != null)
|
||||
{
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
processQueryJoin(queryJoin);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!found)
|
||||
{
|
||||
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
processQueryJoin(queryJoin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QueryJoin makeQueryJoinFromJoinAndTableNames(String tableA, String tableB, QJoinMetaData join)
|
||||
{
|
||||
QueryJoin queryJoin = null;
|
||||
if(join.getLeftTable().equals(tableA) && join.getRightTable().equals(tableB))
|
||||
{
|
||||
queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
}
|
||||
else
|
||||
{
|
||||
join = join.flip();
|
||||
if(join.getLeftTable().equals(tableA) && join.getRightTable().equals(tableB))
|
||||
{
|
||||
queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
}
|
||||
}
|
||||
return queryJoin;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -47,6 +47,13 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
private BooleanOperator booleanOperator = BooleanOperator.AND;
|
||||
private List<QQueryFilter> subFilters = new ArrayList<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// skip & limit are meant to only apply to QueryAction (at least at the initial time they are added here) //
|
||||
// e.g., they are ignored in CountAction, AggregateAction, etc, where their meanings may be less obvious //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private Integer skip;
|
||||
private Integer limit;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -398,4 +405,66 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for skip
|
||||
*******************************************************************************/
|
||||
public Integer getSkip()
|
||||
{
|
||||
return (this.skip);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for skip
|
||||
*******************************************************************************/
|
||||
public void setSkip(Integer skip)
|
||||
{
|
||||
this.skip = skip;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for skip
|
||||
*******************************************************************************/
|
||||
public QQueryFilter withSkip(Integer skip)
|
||||
{
|
||||
this.skip = skip;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for limit
|
||||
*******************************************************************************/
|
||||
public Integer getLimit()
|
||||
{
|
||||
return (this.limit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for limit
|
||||
*******************************************************************************/
|
||||
public void setLimit(Integer limit)
|
||||
{
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for limit
|
||||
*******************************************************************************/
|
||||
public QQueryFilter withLimit(Integer limit)
|
||||
{
|
||||
this.limit = limit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -39,8 +39,6 @@ public class QueryInput extends AbstractTableActionInput
|
||||
{
|
||||
private QBackendTransaction transaction;
|
||||
private QQueryFilter filter;
|
||||
private Integer skip;
|
||||
private Integer limit;
|
||||
|
||||
private RecordPipe recordPipe;
|
||||
|
||||
@ -57,7 +55,8 @@ public class QueryInput extends AbstractTableActionInput
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
private Set<String> fieldsToTranslatePossibleValues;
|
||||
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
private boolean selectDistinct = false;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
|
||||
@ -100,50 +99,6 @@ public class QueryInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for skip
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Integer getSkip()
|
||||
{
|
||||
return skip;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for skip
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setSkip(Integer skip)
|
||||
{
|
||||
this.skip = skip;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for limit
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Integer getLimit()
|
||||
{
|
||||
return limit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for limit
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setLimit(Integer limit)
|
||||
{
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for recordPipe
|
||||
**
|
||||
@ -361,28 +316,6 @@ public class QueryInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for skip
|
||||
*******************************************************************************/
|
||||
public QueryInput withSkip(Integer skip)
|
||||
{
|
||||
this.skip = skip;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for limit
|
||||
*******************************************************************************/
|
||||
public QueryInput withLimit(Integer limit)
|
||||
{
|
||||
this.limit = limit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for recordPipe
|
||||
*******************************************************************************/
|
||||
@ -561,4 +494,35 @@ public class QueryInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for selectDistinct
|
||||
*******************************************************************************/
|
||||
public boolean getSelectDistinct()
|
||||
{
|
||||
return (this.selectDistinct);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for selectDistinct
|
||||
*******************************************************************************/
|
||||
public void setSelectDistinct(boolean selectDistinct)
|
||||
{
|
||||
this.selectDistinct = selectDistinct;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for selectDistinct
|
||||
*******************************************************************************/
|
||||
public QueryInput withSelectDistinct(boolean selectDistinct)
|
||||
{
|
||||
this.selectDistinct = selectDistinct;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -62,8 +62,9 @@ public class WidgetQueryField extends AbstractWidgetValueSourceWithFilter
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(getEffectiveFilter(input));
|
||||
queryInput.setLimit(1);
|
||||
QQueryFilter effectiveFilter = getEffectiveFilter(input);
|
||||
queryInput.setFilter(effectiveFilter);
|
||||
effectiveFilter.setLimit(1);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
|
||||
{
|
||||
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of an ExposedJoin for a frontend to see
|
||||
*******************************************************************************/
|
||||
public class QFrontendExposedJoin
|
||||
{
|
||||
private String label;
|
||||
private Boolean isMany;
|
||||
private QFrontendTableMetaData joinTable;
|
||||
private List<QJoinMetaData> joinPath;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for joinTable
|
||||
*******************************************************************************/
|
||||
public QFrontendTableMetaData getJoinTable()
|
||||
{
|
||||
return (this.joinTable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for joinTable
|
||||
*******************************************************************************/
|
||||
public void setJoinTable(QFrontendTableMetaData joinTable)
|
||||
{
|
||||
this.joinTable = joinTable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for joinTable
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withJoinTable(QFrontendTableMetaData joinTable)
|
||||
{
|
||||
this.joinTable = joinTable;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for joinPath
|
||||
*******************************************************************************/
|
||||
public List<QJoinMetaData> getJoinPath()
|
||||
{
|
||||
return (this.joinPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for joinPath
|
||||
*******************************************************************************/
|
||||
public void setJoinPath(List<QJoinMetaData> joinPath)
|
||||
{
|
||||
this.joinPath = joinPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for joinPath
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withJoinPath(List<QJoinMetaData> joinPath)
|
||||
{
|
||||
this.joinPath = joinPath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add one join to the join path in here
|
||||
*******************************************************************************/
|
||||
public void addJoin(QJoinMetaData join)
|
||||
{
|
||||
if(this.joinPath == null)
|
||||
{
|
||||
this.joinPath = new ArrayList<>();
|
||||
}
|
||||
this.joinPath.add(join);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isMany
|
||||
*******************************************************************************/
|
||||
public Boolean getIsMany()
|
||||
{
|
||||
return (this.isMany);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isMany
|
||||
*******************************************************************************/
|
||||
public void setIsMany(Boolean isMany)
|
||||
{
|
||||
this.isMany = isMany;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isMany
|
||||
*******************************************************************************/
|
||||
public QFrontendExposedJoin withIsMany(Boolean isMany)
|
||||
{
|
||||
this.isMany = isMany;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -32,12 +33,16 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -58,6 +63,8 @@ public class QFrontendTableMetaData
|
||||
private Map<String, QFrontendFieldMetaData> fields;
|
||||
private List<QFieldSection> sections;
|
||||
|
||||
private List<QFrontendExposedJoin> exposedJoins;
|
||||
|
||||
private Set<String> capabilities;
|
||||
|
||||
private boolean readPermission;
|
||||
@ -74,7 +81,7 @@ public class QFrontendTableMetaData
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields)
|
||||
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields, boolean includeJoins)
|
||||
{
|
||||
this.name = tableMetaData.getName();
|
||||
this.label = tableMetaData.getLabel();
|
||||
@ -92,6 +99,27 @@ public class QFrontendTableMetaData
|
||||
this.sections = tableMetaData.getSections();
|
||||
}
|
||||
|
||||
if(includeJoins)
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
this.exposedJoins = new ArrayList<>();
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(tableMetaData.getExposedJoins()))
|
||||
{
|
||||
QFrontendExposedJoin frontendExposedJoin = new QFrontendExposedJoin();
|
||||
this.exposedJoins.add(frontendExposedJoin);
|
||||
|
||||
QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
|
||||
frontendExposedJoin.setLabel(exposedJoin.getLabel());
|
||||
frontendExposedJoin.setIsMany(exposedJoin.getIsMany());
|
||||
frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFields, false));
|
||||
for(String joinName : exposedJoin.getJoinPath())
|
||||
{
|
||||
frontendExposedJoin.addJoin(qInstance.getJoin(joinName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(tableMetaData.getIcon() != null)
|
||||
{
|
||||
this.iconName = tableMetaData.getIcon().getName();
|
||||
@ -259,4 +287,15 @@ public class QFrontendTableMetaData
|
||||
{
|
||||
return deletePermission;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for exposedJoins
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<QFrontendExposedJoin> getExposedJoins()
|
||||
{
|
||||
return exposedJoins;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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.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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public ExposedJoin withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for joinTable
|
||||
*******************************************************************************/
|
||||
public String getJoinTable()
|
||||
{
|
||||
return (this.joinTable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for joinTable
|
||||
*******************************************************************************/
|
||||
public void setJoinTable(String joinTable)
|
||||
{
|
||||
this.joinTable = joinTable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for joinTable
|
||||
*******************************************************************************/
|
||||
public ExposedJoin withJoinTable(String joinTable)
|
||||
{
|
||||
this.joinTable = joinTable;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for joinPath
|
||||
*******************************************************************************/
|
||||
public List<String> getJoinPath()
|
||||
{
|
||||
return (this.joinPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for joinPath
|
||||
*******************************************************************************/
|
||||
public void setJoinPath(List<String> joinPath)
|
||||
{
|
||||
this.joinPath = joinPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for joinPath
|
||||
*******************************************************************************/
|
||||
public ExposedJoin withJoinPath(List<String> joinPath)
|
||||
{
|
||||
this.joinPath = joinPath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -101,6 +101,8 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
|
||||
|
||||
private Map<String, QMiddlewareTableMetaData> middlewareMetaData;
|
||||
|
||||
private List<ExposedJoin> exposedJoins;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -1296,4 +1298,51 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
|
||||
{
|
||||
qInstance.addTable(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for exposedJoins
|
||||
*******************************************************************************/
|
||||
public List<ExposedJoin> getExposedJoins()
|
||||
{
|
||||
return (this.exposedJoins);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for exposedJoins
|
||||
*******************************************************************************/
|
||||
public void setExposedJoins(List<ExposedJoin> exposedJoins)
|
||||
{
|
||||
this.exposedJoins = exposedJoins;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for exposedJoins
|
||||
*******************************************************************************/
|
||||
public QTableMetaData withExposedJoins(List<ExposedJoin> exposedJoins)
|
||||
{
|
||||
this.exposedJoins = exposedJoins;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for exposedJoins
|
||||
*******************************************************************************/
|
||||
public QTableMetaData withExposedJoin(ExposedJoin exposedJoin)
|
||||
{
|
||||
if(this.exposedJoins == null)
|
||||
{
|
||||
this.exposedJoins = new ArrayList<>();
|
||||
}
|
||||
this.exposedJoins.add(exposedJoin);
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ public class EnumerationQueryAction implements QueryInterface
|
||||
}
|
||||
|
||||
BackendQueryFilterUtils.sortRecordList(queryInput.getFilter(), recordList);
|
||||
recordList = BackendQueryFilterUtils.applySkipAndLimit(queryInput, recordList);
|
||||
recordList = BackendQueryFilterUtils.applySkipAndLimit(queryInput.getFilter(), recordList);
|
||||
|
||||
QueryOutput queryOutput = new QueryOutput(queryInput);
|
||||
queryOutput.addRecords(recordList);
|
||||
|
@ -38,13 +38,16 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -181,7 +184,7 @@ public class MemoryRecordStore
|
||||
}
|
||||
|
||||
BackendQueryFilterUtils.sortRecordList(input.getFilter(), records);
|
||||
records = BackendQueryFilterUtils.applySkipAndLimit(input, records);
|
||||
records = BackendQueryFilterUtils.applySkipAndLimit(input.getFilter(), records);
|
||||
|
||||
return (records);
|
||||
}
|
||||
@ -191,8 +194,11 @@ public class MemoryRecordStore
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Collection<QRecord> buildJoinCrossProduct(QueryInput input)
|
||||
private Collection<QRecord> buildJoinCrossProduct(QueryInput input) throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
JoinsContext joinsContext = new JoinsContext(qInstance, input.getTableName(), input.getQueryJoins(), input.getFilter());
|
||||
|
||||
List<QRecord> crossProduct = new ArrayList<>();
|
||||
QTableMetaData leftTable = input.getTable();
|
||||
for(QRecord record : getTableData(leftTable).values())
|
||||
@ -204,16 +210,26 @@ public class MemoryRecordStore
|
||||
|
||||
for(QueryJoin queryJoin : input.getQueryJoins())
|
||||
{
|
||||
QTableMetaData nextTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
|
||||
QTableMetaData nextTable = qInstance.getTable(queryJoin.getJoinTable());
|
||||
Collection<QRecord> nextTableRecords = getTableData(nextTable).values();
|
||||
|
||||
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
|
||||
{
|
||||
QJoinMetaData found = joinsContext.findJoinMetaData(qInstance, input.getTableName(), queryJoin.getJoinTable());
|
||||
if(found == null)
|
||||
{
|
||||
throw (new RuntimeException("Could not find a join between tables [" + input.getTableName() + "][" + queryJoin.getJoinTable() + "]"));
|
||||
}
|
||||
return (found);
|
||||
});
|
||||
|
||||
List<QRecord> nextLevelProduct = new ArrayList<>();
|
||||
for(QRecord productRecord : crossProduct)
|
||||
{
|
||||
boolean matchFound = false;
|
||||
for(QRecord nextTableRecord : nextTableRecords)
|
||||
{
|
||||
if(joinMatches(productRecord, nextTableRecord, queryJoin))
|
||||
if(joinMatches(productRecord, nextTableRecord, queryJoin, joinMetaData))
|
||||
{
|
||||
QRecord joinRecord = new QRecord(productRecord);
|
||||
addRecordToProduct(joinRecord, nextTableRecord, queryJoin.getJoinTableOrItsAlias());
|
||||
@ -239,9 +255,9 @@ public class MemoryRecordStore
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean joinMatches(QRecord productRecord, QRecord nextTableRecord, QueryJoin queryJoin)
|
||||
private boolean joinMatches(QRecord productRecord, QRecord nextTableRecord, QueryJoin queryJoin, QJoinMetaData joinMetaData)
|
||||
{
|
||||
for(JoinOn joinOn : queryJoin.getJoinMetaData().getJoinOns())
|
||||
for(JoinOn joinOn : joinMetaData.getJoinOns())
|
||||
{
|
||||
Serializable leftValue = productRecord.getValues().containsKey(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField())
|
||||
? productRecord.getValue(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField())
|
||||
|
@ -27,7 +27,6 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Month;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -36,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -59,7 +59,8 @@ public class MockQueryAction implements QueryInterface
|
||||
|
||||
QueryOutput queryOutput = new QueryOutput(queryInput);
|
||||
|
||||
int rows = Objects.requireNonNullElse(queryInput.getLimit(), 1);
|
||||
@SuppressWarnings("UnnecessaryUnboxing") // force an un-boxing, to force an NPE if it's null, to get to the "else 1"
|
||||
int rows = ObjectUtils.tryElse(() -> queryInput.getFilter().getLimit().intValue(), 1);
|
||||
for(int i = 0; i < rows; i++)
|
||||
{
|
||||
QRecord record = new QRecord();
|
||||
|
@ -33,7 +33,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
@ -129,42 +128,42 @@ public class BackendQueryFilterUtils
|
||||
public static boolean doesCriteriaMatch(QFilterCriteria criterion, String fieldName, Serializable value)
|
||||
{
|
||||
boolean criterionMatches = switch(criterion.getOperator())
|
||||
{
|
||||
case EQUALS -> testEquals(criterion, value);
|
||||
case NOT_EQUALS -> !testEquals(criterion, value);
|
||||
case IN -> testIn(criterion, value);
|
||||
case NOT_IN -> !testIn(criterion, value);
|
||||
case IS_BLANK -> testBlank(criterion, value);
|
||||
case IS_NOT_BLANK -> !testBlank(criterion, value);
|
||||
case CONTAINS -> testContains(criterion, fieldName, value);
|
||||
case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
|
||||
case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
|
||||
case LIKE -> testLike(criterion, fieldName, value);
|
||||
case NOT_LIKE -> !testLike(criterion, fieldName, value);
|
||||
case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
|
||||
case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
|
||||
case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
|
||||
case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
|
||||
case GREATER_THAN -> testGreaterThan(criterion, value);
|
||||
case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
|
||||
case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
|
||||
case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
|
||||
case BETWEEN ->
|
||||
{
|
||||
case EQUALS -> testEquals(criterion, value);
|
||||
case NOT_EQUALS -> !testEquals(criterion, value);
|
||||
case IN -> testIn(criterion, value);
|
||||
case NOT_IN -> !testIn(criterion, value);
|
||||
case IS_BLANK -> testBlank(criterion, value);
|
||||
case IS_NOT_BLANK -> !testBlank(criterion, value);
|
||||
case CONTAINS -> testContains(criterion, fieldName, value);
|
||||
case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
|
||||
case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
|
||||
case LIKE -> testLike(criterion, fieldName, value);
|
||||
case NOT_LIKE -> !testLike(criterion, fieldName, value);
|
||||
case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
|
||||
case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
|
||||
case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
|
||||
case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
|
||||
case GREATER_THAN -> testGreaterThan(criterion, value);
|
||||
case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
|
||||
case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
|
||||
case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
|
||||
case BETWEEN ->
|
||||
{
|
||||
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
|
||||
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
|
||||
criteria1.getValues().remove(0);
|
||||
yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
|
||||
}
|
||||
case NOT_BETWEEN ->
|
||||
{
|
||||
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
|
||||
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
|
||||
criteria1.getValues().remove(0);
|
||||
boolean between = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
|
||||
yield !between;
|
||||
}
|
||||
};
|
||||
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
|
||||
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
|
||||
criteria1.getValues().remove(0);
|
||||
yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
|
||||
}
|
||||
case NOT_BETWEEN ->
|
||||
{
|
||||
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
|
||||
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
|
||||
criteria1.getValues().remove(0);
|
||||
boolean between = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
|
||||
yield !between;
|
||||
}
|
||||
};
|
||||
return criterionMatches;
|
||||
}
|
||||
|
||||
@ -524,9 +523,14 @@ public class BackendQueryFilterUtils
|
||||
/*******************************************************************************
|
||||
** Apply skip & limit attributes from queryInput to a list of records.
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> applySkipAndLimit(QueryInput queryInput, List<QRecord> recordList)
|
||||
public static List<QRecord> applySkipAndLimit(QQueryFilter queryFilter, List<QRecord> recordList)
|
||||
{
|
||||
Integer skip = queryInput.getSkip();
|
||||
if(queryFilter == null)
|
||||
{
|
||||
return (recordList);
|
||||
}
|
||||
|
||||
Integer skip = queryFilter.getSkip();
|
||||
if(skip != null && skip > 0)
|
||||
{
|
||||
if(skip < recordList.size())
|
||||
@ -539,7 +543,7 @@ public class BackendQueryFilterUtils
|
||||
}
|
||||
}
|
||||
|
||||
Integer limit = queryInput.getLimit();
|
||||
Integer limit = queryFilter.getLimit();
|
||||
if(limit != null && limit >= 0 && limit < recordList.size())
|
||||
{
|
||||
recordList = recordList.subList(0, limit);
|
||||
|
@ -211,14 +211,11 @@ public class BulkEditTransformStep extends AbstractTransformStep
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
|
||||
{
|
||||
if(isForResultScreen)
|
||||
{
|
||||
okSummary.setMessage(tableLabel + " records were edited.");
|
||||
}
|
||||
else
|
||||
{
|
||||
okSummary.setMessage(tableLabel + " records will be edited.");
|
||||
}
|
||||
okSummary.setSingularFutureMessage(tableLabel + " record will be edited.");
|
||||
okSummary.setPluralFutureMessage(tableLabel + " records will be edited.");
|
||||
okSummary.setSingularPastMessage(tableLabel + " record was edited.");
|
||||
okSummary.setPluralPastMessage(tableLabel + " records were edited.");
|
||||
okSummary.pickMessage(isForResultScreen);
|
||||
|
||||
ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>();
|
||||
rs.add(okSummary);
|
||||
|
@ -29,6 +29,7 @@ import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
@ -52,6 +53,8 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
*******************************************************************************/
|
||||
public class ExtractViaQueryStep extends AbstractExtractStep
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExtractViaQueryStep.class);
|
||||
|
||||
public static final String FIELD_SOURCE_TABLE = "sourceTable";
|
||||
|
||||
private QQueryFilter queryFilter;
|
||||
@ -77,11 +80,33 @@ public class ExtractViaQueryStep extends AbstractExtractStep
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// clone the filter, since we're going to edit it (set a limit) //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
QQueryFilter filterClone = queryFilter.clone();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a limit in the extract step (e.g., the 20-record limit on the preview screen) //
|
||||
// then set that limit in the filter - UNLESS - there's already a limit in the filter for //
|
||||
// a smaller number of records. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(getLimit() != null)
|
||||
{
|
||||
if(filterClone.getLimit() != null && filterClone.getLimit() < getLimit())
|
||||
{
|
||||
LOG.trace("Using filter's limit [" + filterClone.getLimit() + "] rather than step's limit [" + getLimit() + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
filterClone.setLimit(getLimit());
|
||||
}
|
||||
}
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
|
||||
queryInput.setFilter(queryFilter);
|
||||
queryInput.setFilter(filterClone);
|
||||
queryInput.setSelectDistinct(true);
|
||||
queryInput.setRecordPipe(getRecordPipe());
|
||||
queryInput.setLimit(getLimit());
|
||||
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
|
||||
|
||||
customizeInputPreQuery(queryInput);
|
||||
@ -114,8 +139,20 @@ public class ExtractViaQueryStep extends AbstractExtractStep
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
|
||||
countInput.setFilter(queryFilter);
|
||||
countInput.setIncludeDistinctCount(true);
|
||||
CountOutput countOutput = new CountAction().execute(countInput);
|
||||
return (countOutput.getCount());
|
||||
Integer count = countOutput.getDistinctCount();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// in case the filter we're running has a limit, but the count found more than that limit, //
|
||||
// well then, just return that limit - as the process won't run on more rows than that. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(count != null & queryFilter.getLimit() != null && count > queryFilter.getLimit())
|
||||
{
|
||||
count = queryFilter.getLimit();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
|
@ -85,8 +85,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
|
||||
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
|
||||
);
|
||||
queryInput.setLimit(1);
|
||||
.withLimit(1));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
if(!queryOutput.getRecords().isEmpty())
|
||||
{
|
||||
|
@ -209,8 +209,7 @@ public class GeneralProcessUtils
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, fieldValue)));
|
||||
queryInput.setLimit(1);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, fieldValue)).withLimit(1));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
return (queryOutput.getRecords().stream().findFirst());
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
|
||||
|
||||
@ -38,8 +39,8 @@ import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
|
||||
*******************************************************************************/
|
||||
public class MutableList<T> implements List<T>
|
||||
{
|
||||
private List<T> sourceList;
|
||||
private Class<? extends List<T>> mutableTypeIfNeeded;
|
||||
private List<T> sourceList;
|
||||
private Supplier<List<T>> supplierIfNeeded;
|
||||
|
||||
|
||||
|
||||
@ -49,7 +50,7 @@ public class MutableList<T> implements List<T>
|
||||
*******************************************************************************/
|
||||
public MutableList(List<T> sourceList)
|
||||
{
|
||||
this(sourceList, (Class) ArrayList.class);
|
||||
this(sourceList, ArrayList::new);
|
||||
}
|
||||
|
||||
|
||||
@ -58,10 +59,20 @@ public class MutableList<T> implements List<T>
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MutableList(List<T> sourceList, Class<? extends List<T>> mutableTypeIfNeeded)
|
||||
public MutableList(List<T> sourceList, Supplier<List<T>> supplierIfNeeded)
|
||||
{
|
||||
this.sourceList = sourceList;
|
||||
this.mutableTypeIfNeeded = mutableTypeIfNeeded;
|
||||
this.sourceList = Objects.requireNonNullElseGet(sourceList, supplierIfNeeded);
|
||||
this.supplierIfNeeded = supplierIfNeeded;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
List<T> getUnderlyingList()
|
||||
{
|
||||
return (sourceList);
|
||||
}
|
||||
|
||||
|
||||
@ -73,13 +84,13 @@ public class MutableList<T> implements List<T>
|
||||
{
|
||||
try
|
||||
{
|
||||
List<T> replacementList = mutableTypeIfNeeded.getConstructor().newInstance();
|
||||
List<T> replacementList = supplierIfNeeded.get();
|
||||
replacementList.addAll(sourceList);
|
||||
sourceList = replacementList;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IllegalStateException("The mutable type provided for this MutableList [" + mutableTypeIfNeeded.getName() + "] could not be instantiated."));
|
||||
throw (new IllegalStateException("Error getting from the supplier provided for this MutableList.", e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +99,7 @@ public class MutableList<T> implements List<T>
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private <T> T doMutableOperationForValue(Supplier<T> supplier)
|
||||
private <V> V doMutableOperationForValue(Supplier<V> supplier)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.utils.collections;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
|
||||
@ -37,8 +38,8 @@ import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
|
||||
*******************************************************************************/
|
||||
public class MutableMap<K, V> implements Map<K, V>
|
||||
{
|
||||
private Map<K, V> sourceMap;
|
||||
private Class<? extends Map<K, V>> mutableTypeIfNeeded;
|
||||
private Map<K, V> sourceMap;
|
||||
private Supplier<Map<K, V>> supplierIfNeeded;
|
||||
|
||||
|
||||
|
||||
@ -48,7 +49,7 @@ public class MutableMap<K, V> implements Map<K, V>
|
||||
*******************************************************************************/
|
||||
public MutableMap(Map<K, V> sourceMap)
|
||||
{
|
||||
this(sourceMap, (Class) HashMap.class);
|
||||
this(sourceMap, HashMap::new);
|
||||
}
|
||||
|
||||
|
||||
@ -57,10 +58,20 @@ public class MutableMap<K, V> implements Map<K, V>
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MutableMap(Map<K, V> sourceMap, Class<? extends Map<K, V>> mutableTypeIfNeeded)
|
||||
public MutableMap(Map<K, V> sourceMap, Supplier<Map<K, V>> supplierIfNeeded)
|
||||
{
|
||||
this.sourceMap = sourceMap;
|
||||
this.mutableTypeIfNeeded = mutableTypeIfNeeded;
|
||||
this.sourceMap = Objects.requireNonNullElseGet(sourceMap, supplierIfNeeded);
|
||||
this.supplierIfNeeded = supplierIfNeeded;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
Map<K, V> getUnderlyingMap()
|
||||
{
|
||||
return (sourceMap);
|
||||
}
|
||||
|
||||
|
||||
@ -72,13 +83,13 @@ public class MutableMap<K, V> implements Map<K, V>
|
||||
{
|
||||
try
|
||||
{
|
||||
Map<K, V> replacementMap = mutableTypeIfNeeded.getConstructor().newInstance();
|
||||
Map<K, V> replacementMap = supplierIfNeeded.get();
|
||||
replacementMap.putAll(sourceMap);
|
||||
sourceMap = replacementMap;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IllegalStateException("The mutable type provided for this MutableMap [" + mutableTypeIfNeeded.getName() + "] could not be instantiated."));
|
||||
throw (new IllegalStateException("Error getting from the supplier provided for this MutableMap.", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user