mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Checking record security locks that are more than 1 join away.
This commit is contained in:
@ -52,6 +52,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
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.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
@ -68,6 +69,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -101,6 +103,11 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
validateSecurityFields(insertInput);
|
||||
|
||||
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
|
||||
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
LOG.warn("Errors in insertAction", logPair("tableName", table.getName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
|
||||
}
|
||||
|
||||
manageAssociations(table, insertOutput.getRecords());
|
||||
|
||||
@ -209,8 +216,9 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData join = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0)); // todo - many joins...
|
||||
QTableMetaData joinTable = QContext.getQInstance().getTable(join.getLeftTable());
|
||||
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
|
||||
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
|
||||
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
|
||||
|
||||
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(insertInput.getRecords(), 500))
|
||||
{
|
||||
@ -219,10 +227,21 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getLeftTable());
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// we don't need the right-most join //
|
||||
///////////////////////////////////////
|
||||
if(!joinName.equals(rightMostJoin.getName()))
|
||||
{
|
||||
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
|
||||
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
|
||||
@ -235,14 +254,14 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
List<Serializable> inputRecordJoinValues = new ArrayList<>();
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField());
|
||||
inputRecordJoinValues.add(inputRecordValue);
|
||||
|
||||
subFilter.addCriteria(inputRecordValue == null
|
||||
? new QFilterCriteria(joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
}
|
||||
|
||||
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
@ -265,11 +284,16 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
for(QRecord joinRecord : queryOutput.getRecords())
|
||||
{
|
||||
List<Serializable> joinRecordValues = new ArrayList<>();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable inputRecordValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
joinRecordValues.add(inputRecordValue);
|
||||
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
|
||||
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
|
||||
{
|
||||
joinValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
}
|
||||
joinRecordValues.add(joinValue);
|
||||
}
|
||||
|
||||
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
|
||||
}
|
||||
|
||||
@ -286,8 +310,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
QFieldMetaData field = joinTable.getField(fieldName);
|
||||
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
|
||||
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
|
||||
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
|
||||
{
|
||||
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
@ -298,7 +326,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
inputRecord.addError("You do not have permission to insert this record - the referenced " + joinTable.getLabel() + " was not found.");
|
||||
inputRecord.addError("You do not have permission to insert this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.instances;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -496,6 +497,7 @@ public class QInstanceValidator
|
||||
{
|
||||
String prefix = "Table " + table.getName() + " ";
|
||||
|
||||
RECORD_SECURITY_LOCKS_LOOP:
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
|
||||
@ -522,21 +524,61 @@ public class QInstanceValidator
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
|
||||
{
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(table.getName()))
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
|
||||
{
|
||||
joins.add(new QueryJoin(join));
|
||||
}
|
||||
else if(join.getRightTable().equals(table.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = table;
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join == null)
|
||||
{
|
||||
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
|
||||
continue RECORD_SECURITY_LOCKS_LOOP;
|
||||
}
|
||||
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join));
|
||||
tmpTable = qInstance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
tmpTable = qInstance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
|
||||
continue RECORD_SECURITY_LOCKS_LOOP;
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
|
||||
{
|
||||
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
|
||||
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.actions.tables.query;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Specialization of a QueryJoin, for when the join is added to the query,
|
||||
** not by the caller, but by the framework, because it is implicitly needed
|
||||
** to provide a security lock.
|
||||
*******************************************************************************/
|
||||
public class ImplicitQueryJoinForSecurityLock extends QueryJoin
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** package-private constructor - to make so outside users should not create
|
||||
** instances.
|
||||
*******************************************************************************/
|
||||
ImplicitQueryJoinForSecurityLock()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
@ -22,6 +22,8 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -75,25 +77,46 @@ public class JoinsContext
|
||||
///////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
|
||||
{
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = instance.getTable(mainTableName);
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
if(this.queryJoins.stream().anyMatch(qj -> qj.getJoinMetaData().getName().equals(joinName)))
|
||||
if(this.queryJoins.stream().anyMatch(queryJoin ->
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// we're good - we're already joining on this table! //
|
||||
///////////////////////////////////////////////////////
|
||||
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
|
||||
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
|
||||
}))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
tmpTable = instance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
tmpTable = instance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(tableName.equals(join.getRightTable()))
|
||||
{
|
||||
join = join.flip();
|
||||
}
|
||||
|
||||
QueryJoin queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
processQueryJoin(queryJoin);
|
||||
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -297,6 +320,69 @@ public class JoinsContext
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
|
||||
{
|
||||
List<QJoinMetaData> matches = new ArrayList<>();
|
||||
if(baseTableName != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if query specified a left-table, look for a join between left & right //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
for(QJoinMetaData join : instance.getJoins().values())
|
||||
{
|
||||
if(join.getLeftTable().equals(baseTableName) && join.getRightTable().equals(joinTableName))
|
||||
{
|
||||
matches.add(join);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// look in both directions! //
|
||||
//////////////////////////////
|
||||
if(join.getRightTable().equals(baseTableName) && join.getLeftTable().equals(joinTableName))
|
||||
{
|
||||
matches.add(join.flip());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if query didn't specify a left-table, then look for any join to the right table //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QJoinMetaData join : instance.getJoins().values())
|
||||
{
|
||||
if(join.getRightTable().equals(joinTableName) && this.hasTable(join.getLeftTable()))
|
||||
{
|
||||
matches.add(join);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// look in both directions! //
|
||||
//////////////////////////////
|
||||
if(join.getLeftTable().equals(joinTableName) && this.hasTable(join.getRightTable()))
|
||||
{
|
||||
matches.add(join.flip());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(matches.size() == 1)
|
||||
{
|
||||
return (matches.get(0));
|
||||
}
|
||||
else if(matches.size() > 1)
|
||||
{
|
||||
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -50,6 +50,7 @@ public class QJoinMetaData
|
||||
public QJoinMetaData flip()
|
||||
{
|
||||
return (new QJoinMetaData()
|
||||
.withName(name) // does this need to be different?, e.g., + "Flipped"?
|
||||
.withLeftTable(rightTable)
|
||||
.withRightTable(leftTable)
|
||||
.withType(type.flip())
|
||||
|
@ -28,13 +28,18 @@ import java.util.List;
|
||||
/*******************************************************************************
|
||||
** Define (for a table) a lock that applies to records in the table - e.g.,
|
||||
** a key type, and a field that has values for that key.
|
||||
*
|
||||
**
|
||||
** Here's an example of how the joinNameChain should be set up:
|
||||
** given a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is):
|
||||
** - recordSecurityLock.fieldName = order.clientId
|
||||
** - recordSecurityLock.joinNameChain = [orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic]
|
||||
** that is - what's the chain that takes us FROM the security fieldName TO the table with the lock.
|
||||
*******************************************************************************/
|
||||
public class RecordSecurityLock
|
||||
{
|
||||
private String securityKeyType;
|
||||
private String fieldName;
|
||||
private List<String> joinNameChain; // todo - add validation in validator!!
|
||||
private List<String> joinNameChain;
|
||||
private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY;
|
||||
|
||||
|
||||
|
@ -24,19 +24,24 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.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.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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -134,10 +139,15 @@ public class MemoryRecordStore
|
||||
{
|
||||
incrementStatistic(input);
|
||||
|
||||
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
|
||||
List<QRecord> records = new ArrayList<>();
|
||||
Collection<QRecord> tableData = getTableData(input.getTable()).values();
|
||||
List<QRecord> records = new ArrayList<>();
|
||||
|
||||
for(QRecord qRecord : tableData.values())
|
||||
if(CollectionUtils.nullSafeHasContents(input.getQueryJoins()))
|
||||
{
|
||||
tableData = buildJoinCrossProduct(input);
|
||||
}
|
||||
|
||||
for(QRecord qRecord : tableData)
|
||||
{
|
||||
boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord);
|
||||
|
||||
@ -155,6 +165,87 @@ public class MemoryRecordStore
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Collection<QRecord> buildJoinCrossProduct(QueryInput input)
|
||||
{
|
||||
List<QRecord> crossProduct = new ArrayList<>();
|
||||
QTableMetaData leftTable = input.getTable();
|
||||
for(QRecord record : getTableData(leftTable).values())
|
||||
{
|
||||
QRecord productRecord = new QRecord();
|
||||
addRecordToProduct(productRecord, record, leftTable.getName());
|
||||
crossProduct.add(productRecord);
|
||||
}
|
||||
|
||||
for(QueryJoin queryJoin : input.getQueryJoins())
|
||||
{
|
||||
QTableMetaData nextTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
|
||||
Collection<QRecord> nextTableRecords = getTableData(nextTable).values();
|
||||
|
||||
List<QRecord> nextLevelProduct = new ArrayList<>();
|
||||
for(QRecord productRecord : crossProduct)
|
||||
{
|
||||
boolean matchFound = false;
|
||||
for(QRecord nextTableRecord : nextTableRecords)
|
||||
{
|
||||
if(joinMatches(productRecord, nextTableRecord, queryJoin))
|
||||
{
|
||||
QRecord joinRecord = new QRecord(productRecord);
|
||||
addRecordToProduct(joinRecord, nextTableRecord, queryJoin.getJoinTableOrItsAlias());
|
||||
nextLevelProduct.add(joinRecord);
|
||||
matchFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!matchFound)
|
||||
{
|
||||
// todo - Left & Right joins
|
||||
}
|
||||
}
|
||||
|
||||
crossProduct = nextLevelProduct;
|
||||
}
|
||||
|
||||
return (crossProduct);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean joinMatches(QRecord productRecord, QRecord nextTableRecord, QueryJoin queryJoin)
|
||||
{
|
||||
for(JoinOn joinOn : queryJoin.getJoinMetaData().getJoinOns())
|
||||
{
|
||||
Serializable leftValue = productRecord.getValue(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField());
|
||||
Serializable rightValue = nextTableRecord.getValue(joinOn.getRightField());
|
||||
if(!Objects.equals(leftValue, rightValue))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToProduct(QRecord productRecord, QRecord record, String tableNameOrAlias)
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : record.getValues().entrySet())
|
||||
{
|
||||
productRecord.withValue(tableNameOrAlias + "." + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -27,6 +27,7 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
@ -71,6 +72,21 @@ public class BackendQueryFilterUtils
|
||||
{
|
||||
String fieldName = criterion.getFieldName();
|
||||
Serializable value = qRecord.getValue(fieldName);
|
||||
if(value == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the value isn't in the record - check, if it looks like a table.fieldName, but none of the //
|
||||
// field names in the record are fully qualified, then just use the field-name portion... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
Map<String, Serializable> values = qRecord.getValues();
|
||||
if(values.keySet().stream().noneMatch(n -> n.contains(".")))
|
||||
{
|
||||
value = qRecord.getValue(fieldName.substring(fieldName.indexOf(".") + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean criterionMatches = doesCriteriaMatch(criterion, fieldName, value);
|
||||
|
||||
|
@ -291,7 +291,130 @@ class InsertActionTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInsertSecurityJoins() throws QException
|
||||
void testInsertMultiLevelSecurityJoins() throws QException
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// null value in the foreign key to the join-table that provides the security value //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", null).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", 1701).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// insert an order and lineItem with storeId=2 - then, reset our session to only have storeId=1 in it - and try to insert an order-line referencing that order. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(2));
|
||||
InsertInput insertOrderInput = new InsertInput();
|
||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 42).withValue("storeId", 2)));
|
||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
||||
assertEquals(42, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
InsertInput insertLineItemInput = new InsertInput();
|
||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4200).withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 24)));
|
||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
||||
assertEquals(4200, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
||||
InsertInput insertLineItemExtrinsicInput = new InsertInput();
|
||||
insertLineItemExtrinsicInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertLineItemExtrinsicInput.setRecords(List.of(new QRecord().withValue("lineItemId", 4200).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
||||
InsertOutput insertLineItemExtrinsicOutput = new InsertAction().execute(insertLineItemExtrinsicInput);
|
||||
assertEquals("You do not have permission to insert this record.", insertLineItemExtrinsicOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
||||
InsertInput insertOrderInput = new InsertInput();
|
||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("storeId", 1)));
|
||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
||||
assertEquals(47, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
InsertInput insertLineItemInput = new InsertInput();
|
||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4700).withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 74)));
|
||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
||||
assertEquals(4700, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// combine all the above, plus one record that works //
|
||||
///////////////////////////////////////////////////////
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
|
||||
));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
||||
}
|
||||
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// one more time, but with multiple input records referencing each foreign key //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
|
||||
));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(4).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(5).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(6).getErrors().get(0));
|
||||
assertEquals(0, insertOutput.getRecords().get(7).getErrors().size());
|
||||
assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInsertSingleLevelSecurityJoins() throws QException
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
|
||||
|
@ -1605,8 +1605,25 @@ class QInstanceValidatorTest extends BaseTest
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("notAField")), "unrecognized field");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setNullValueBehavior(null)), "missing a nullValueBehavior");
|
||||
|
||||
// todo - remove once implemented
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("join.field")), "does not yet support finding a field that looks like a join field");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("join.field")), "Table order recordSecurityLock (of key type store) field name join.field looks like a join (has a dot), but no joinNameChain was given");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRecordSecurityLockJoinChains()
|
||||
{
|
||||
Function<QInstance, RecordSecurityLock> lockExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).getRecordSecurityLocks().get(0);
|
||||
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(null)), "looks like a join (has a dot), but no joinNameChain was given");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(new ArrayList<>())), "looks like a join (has a dot), but no joinNameChain was given");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("storeId")), "does not look like a join (does not have a dot), but a joinNameChain was given");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("order.wrongId")), "unrecognized fieldName: order.wrongId");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(List.of("notAJoin"))), "an unrecognized join");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(List.of("orderLineItem"))), "joinNameChain could not be followed through join");
|
||||
}
|
||||
|
||||
|
||||
|
@ -596,6 +596,10 @@ public class TestUtils
|
||||
.withName(TABLE_NAME_LINE_ITEM_EXTRINSIC)
|
||||
.withBackendName(MEMORY_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withRecordSecurityLock(new RecordSecurityLock()
|
||||
.withSecurityKeyType(SECURITY_KEY_TYPE_STORE)
|
||||
.withFieldName("order.storeId")
|
||||
.withJoinNameChain(List.of("orderLineItem", "lineItemLineItemExtrinsic")))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
|
@ -47,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.ImplicitQueryJoinForSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
@ -218,7 +219,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName);
|
||||
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
|
||||
{
|
||||
QJoinMetaData found = findJoinMetaData(instance, joinsContext, baseTableName, queryJoin.getJoinTable());
|
||||
QJoinMetaData found = joinsContext.findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
|
||||
if(found == null)
|
||||
{
|
||||
throw (new RuntimeException("Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]"));
|
||||
@ -263,69 +264,6 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QJoinMetaData findJoinMetaData(QInstance instance, JoinsContext joinsContext, String baseTableName, String joinTableName)
|
||||
{
|
||||
List<QJoinMetaData> matches = new ArrayList<>();
|
||||
if(baseTableName != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if query specified a left-table, look for a join between left & right //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
for(QJoinMetaData join : instance.getJoins().values())
|
||||
{
|
||||
if(join.getLeftTable().equals(baseTableName) && join.getRightTable().equals(joinTableName))
|
||||
{
|
||||
matches.add(join);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// look in both directions! //
|
||||
//////////////////////////////
|
||||
if(join.getRightTable().equals(baseTableName) && join.getLeftTable().equals(joinTableName))
|
||||
{
|
||||
matches.add(join.flip());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if query didn't specify a left-table, then look for any join to the right table //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QJoinMetaData join : instance.getJoins().values())
|
||||
{
|
||||
if(join.getRightTable().equals(joinTableName) && joinsContext.hasTable(join.getLeftTable()))
|
||||
{
|
||||
matches.add(join);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// look in both directions! //
|
||||
//////////////////////////////
|
||||
if(join.getLeftTable().equals(joinTableName) && joinsContext.hasTable(join.getRightTable()))
|
||||
{
|
||||
matches.add(join.flip());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(matches.size() == 1)
|
||||
{
|
||||
return (matches.get(0));
|
||||
}
|
||||
else if(matches.size() > 1)
|
||||
{
|
||||
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** method that sub-classes should call to make a full WHERE clause, including
|
||||
** security clauses.
|
||||
@ -403,6 +341,16 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
|
||||
for(QueryJoin queryJoin : CollectionUtils.nonNullList(joinsContext.getQueryJoins()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for user-added joins, we want to add their security-locks to the query //
|
||||
// but if a join was implicitly added because it's needed to find a security lock on table being queried, //
|
||||
// don't add additional layers of locks for each join table. that's the idea here at least. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryJoin instanceof ImplicitQueryJoinForSecurityLock)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
|
||||
{
|
||||
@ -436,43 +384,10 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
}
|
||||
}
|
||||
|
||||
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
|
||||
String fieldNameWithoutTablePrefix = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
String fieldNameTablePrefix = recordSecurityLock.getFieldName().replaceFirst("\\..*", "");
|
||||
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
|
||||
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
{
|
||||
QJoinMetaData joinMetaData = instance.getJoin(joinName);
|
||||
|
||||
/*
|
||||
for(QueryJoin queryJoin : joinsContext.getQueryJoins())
|
||||
{
|
||||
if(queryJoin.getJoinMetaData().getName().equals(joinName))
|
||||
{
|
||||
joinMetaData = queryJoin.getJoinMetaData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if(joinMetaData == null)
|
||||
{
|
||||
throw (new RuntimeException("Could not find joinMetaData for recordSecurityLock with joinChain member [" + joinName + "]"));
|
||||
}
|
||||
|
||||
if(fieldNameTablePrefix.equals(joinMetaData.getLeftTable()))
|
||||
{
|
||||
table = instance.getTable(joinMetaData.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
table = instance.getTable(joinMetaData.getRightTable());
|
||||
}
|
||||
|
||||
tableNameOrAlias = table.getName();
|
||||
fieldName = tableNameOrAlias + "." + fieldNameWithoutTablePrefix;
|
||||
}
|
||||
fieldName = recordSecurityLock.getFieldName();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -481,7 +396,19 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
QQueryFilter lockFilter = new QQueryFilter();
|
||||
List<QFilterCriteria> lockCriteria = new ArrayList<>();
|
||||
lockFilter.setCriteria(lockCriteria);
|
||||
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), table.getField(fieldNameWithoutTablePrefix).getType());
|
||||
|
||||
QFieldType type = QFieldType.INTEGER;
|
||||
try
|
||||
{
|
||||
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(fieldName);
|
||||
type = fieldAndTableNameOrAlias.field().getType();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error getting field type... Trying Integer", e);
|
||||
}
|
||||
|
||||
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
|
||||
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -82,7 +82,7 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
|
||||
.toList();
|
||||
|
||||
String columns = insertableFields.stream()
|
||||
.map(this::getColumnName)
|
||||
.map(f -> "`" + getColumnName(f) + "`")
|
||||
.collect(Collectors.joining(", "));
|
||||
String questionMarks = insertableFields.stream()
|
||||
.map(x -> "?")
|
||||
|
@ -25,6 +25,11 @@ package com.kingsrook.qqq.backend.module.rdbms;
|
||||
import java.io.InputStream;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
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.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
@ -38,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
@ -61,6 +67,7 @@ public class TestUtils
|
||||
public static final String TABLE_NAME_ORDER = "order";
|
||||
public static final String TABLE_NAME_ITEM = "item";
|
||||
public static final String TABLE_NAME_ORDER_LINE = "orderLine";
|
||||
public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic";
|
||||
public static final String TABLE_NAME_WAREHOUSE = "warehouse";
|
||||
public static final String TABLE_NAME_WAREHOUSE_STORE_INT = "warehouseStoreInt";
|
||||
|
||||
@ -231,6 +238,7 @@ public class TestUtils
|
||||
|
||||
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order")
|
||||
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
|
||||
.withAssociation(new Association().withName("orderLine").withAssociatedTableName(TABLE_NAME_ORDER_LINE).withJoinName("orderJoinOrderLine"))
|
||||
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
|
||||
.withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
|
||||
.withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
|
||||
@ -243,13 +251,28 @@ public class TestUtils
|
||||
);
|
||||
|
||||
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_LINE, "order_line")
|
||||
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
|
||||
.withRecordSecurityLock(new RecordSecurityLock()
|
||||
.withSecurityKeyType(TABLE_NAME_STORE)
|
||||
.withFieldName("order.storeId")
|
||||
.withJoinNameChain(List.of("orderJoinOrderLine")))
|
||||
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("orderLineJoinLineItemExtrinsic"))
|
||||
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id"))
|
||||
.withField(new QFieldMetaData("sku", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
|
||||
.withField(new QFieldMetaData("quantity", QFieldType.INTEGER))
|
||||
);
|
||||
|
||||
qInstance.addTable(defineBaseTable(TABLE_NAME_LINE_ITEM_EXTRINSIC, "line_item_extrinsic")
|
||||
.withRecordSecurityLock(new RecordSecurityLock()
|
||||
.withSecurityKeyType(TABLE_NAME_STORE)
|
||||
.withFieldName("order.storeId")
|
||||
.withJoinNameChain(List.of("orderJoinOrderLine", "orderLineJoinLineItemExtrinsic")))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("orderLineId", QFieldType.INTEGER).withBackendName("order_line_id"))
|
||||
.withField(new QFieldMetaData("key", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("value", QFieldType.STRING))
|
||||
);
|
||||
|
||||
qInstance.addTable(defineBaseTable(TABLE_NAME_WAREHOUSE_STORE_INT, "warehouse_store_int")
|
||||
.withField(new QFieldMetaData("warehouseId", QFieldType.INTEGER).withBackendName("warehouse_id"))
|
||||
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id"))
|
||||
@ -321,6 +344,14 @@ public class TestUtils
|
||||
.withJoinOn(new JoinOn("storeId", "storeId"))
|
||||
);
|
||||
|
||||
qInstance.addJoin(new QJoinMetaData()
|
||||
.withName("orderLineJoinLineItemExtrinsic")
|
||||
.withLeftTable(TABLE_NAME_ORDER_LINE)
|
||||
.withRightTable(TABLE_NAME_LINE_ITEM_EXTRINSIC)
|
||||
.withType(JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn("id", "orderLineId"))
|
||||
);
|
||||
|
||||
qInstance.addPossibleValueSource(new QPossibleValueSource()
|
||||
.withName("store")
|
||||
.withType(QPossibleValueSourceType.TABLE)
|
||||
@ -349,4 +380,16 @@ public class TestUtils
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> queryTable(String tableName) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
return (queryOutput.getRecords());
|
||||
}
|
||||
}
|
||||
|
@ -329,8 +329,7 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest
|
||||
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
|
||||
aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
|
||||
aggregateResult = aggregateOutput.getResults().get(0);
|
||||
// note - this would be 33, except for that one order line that has a contradictory store id...
|
||||
Assertions.assertEquals(32, aggregateResult.getAggregateValue(sumOfQuantity));
|
||||
Assertions.assertEquals(33, aggregateResult.getAggregateValue(sumOfQuantity));
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,6 +24,9 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
@ -34,6 +37,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -140,6 +144,50 @@ public class RDBMSInsertActionTest extends RDBMSActionTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInsertAssociations() throws QException
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1);
|
||||
|
||||
int originalNoOfOrderLineExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size();
|
||||
int originalNoOfOrderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_LINE).size();
|
||||
int originalNoOfOrders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size();
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("storeId", 1).withValue("billToPersonId", 100).withValue("shipToPersonId", 200)
|
||||
|
||||
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1)
|
||||
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1")))
|
||||
|
||||
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC2").withValue("quantity", 2)
|
||||
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2"))
|
||||
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3")))
|
||||
));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
List<QRecord> orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER);
|
||||
assertEquals(originalNoOfOrders + 1, orders.size());
|
||||
assertTrue(orders.stream().anyMatch(r -> Objects.equals(r.getValue("billToPersonId"), 100) && Objects.equals(r.getValue("shipToPersonId"), 200)));
|
||||
|
||||
List<QRecord> orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_LINE);
|
||||
assertEquals(originalNoOfOrderLines + 2, orderLines.size());
|
||||
assertTrue(orderLines.stream().anyMatch(r -> Objects.equals(r.getValue("sku"), "BASIC1") && Objects.equals(r.getValue("quantity"), 1)));
|
||||
assertTrue(orderLines.stream().anyMatch(r -> Objects.equals(r.getValue("sku"), "BASIC2") && Objects.equals(r.getValue("quantity"), 2)));
|
||||
|
||||
List<QRecord> lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
assertEquals(originalNoOfOrderLineExtrinsics + 3, lineItemExtrinsics.size());
|
||||
assertTrue(lineItemExtrinsics.stream().anyMatch(r -> Objects.equals(r.getValue("key"), "LINE-EXT-1.1") && Objects.equals(r.getValue("value"), "LINE-VAL-1")));
|
||||
assertTrue(lineItemExtrinsics.stream().anyMatch(r -> Objects.equals(r.getValue("key"), "LINE-EXT-2.1") && Objects.equals(r.getValue("value"), "LINE-VAL-2")));
|
||||
assertTrue(lineItemExtrinsics.stream().anyMatch(r -> Objects.equals(r.getValue("key"), "LINE-EXT-2.2") && Objects.equals(r.getValue("value"), "LINE-VAL-3")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void assertAnInsertedPersonRecord(String firstName, String lastName, Integer id) throws Exception
|
||||
{
|
||||
runTestSql("SELECT * FROM person WHERE last_name = '" + lastName + "'", (rs -> {
|
||||
|
@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
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.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
@ -162,6 +163,7 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest
|
||||
.withDataSource(new QReportDataSource()
|
||||
.withSourceTable(TestUtils.TABLE_NAME_ORDER_LINE)
|
||||
.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM).withAlias("i").withSelect(true))
|
||||
.withQueryFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id")))
|
||||
)
|
||||
.withView(new QReportView()
|
||||
.withType(ReportType.TABLE)
|
||||
@ -179,13 +181,13 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest
|
||||
assertEquals("""
|
||||
"Line Item Id","Item SKU","Item Store Id","Item Store Name"
|
||||
"1","QM-1","1","Q-Mart"
|
||||
"5","QM-1","1","Q-Mart"
|
||||
"2","QM-2","1","Q-Mart"
|
||||
"3","QM-3","1","Q-Mart"
|
||||
"4","QRU-1","2","QQQ 'R' Us"
|
||||
"5","QM-1","1","Q-Mart"
|
||||
"6","QRU-1","2","QQQ 'R' Us"
|
||||
"8","QRU-1","2","QQQ 'R' Us"
|
||||
"7","QRU-2","2","QQQ 'R' Us"
|
||||
"8","QRU-1","2","QQQ 'R' Us"
|
||||
"9","QD-1","3","QDepot"
|
||||
"10","QD-1","3","QDepot"
|
||||
"11","QD-1","3","QDepot"
|
||||
|
@ -79,6 +79,7 @@ INSERT INTO carrier (id, name, company_code, service_level) VALUES (9, 'USPS Sup
|
||||
INSERT INTO carrier (id, name, company_code, service_level) VALUES (10, 'DHL International', 'DHL', 'I');
|
||||
INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G');
|
||||
|
||||
DROP TABLE IF EXISTS line_item_extrinsic;
|
||||
DROP TABLE IF EXISTS order_line;
|
||||
DROP TABLE IF EXISTS item;
|
||||
DROP TABLE IF EXISTS `order`;
|
||||
@ -138,7 +139,7 @@ CREATE TABLE order_line
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
order_id INT REFERENCES `order`,
|
||||
sku VARCHAR(80),
|
||||
store_id INT REFERENCES store, -- todo - as a challenge, if this field wasn't here, so we had to join through order...
|
||||
store_id INT REFERENCES store,
|
||||
quantity INT
|
||||
);
|
||||
|
||||
@ -177,3 +178,12 @@ CREATE TABLE warehouse_store_int
|
||||
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 1);
|
||||
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 2);
|
||||
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 3);
|
||||
|
||||
CREATE TABLE line_item_extrinsic
|
||||
(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
order_line_id INT REFERENCES order_line,
|
||||
`key` VARCHAR(80),
|
||||
`value` VARCHAR(80)
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user