Checking record security locks that are more than 1 join away.

This commit is contained in:
2023-03-29 09:55:05 -05:00
parent e62d2332ac
commit ef6ccc61c3
18 changed files with 634 additions and 151 deletions

View File

@ -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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.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.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils; 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); validateSecurityFields(insertInput);
InsertOutput insertOutput = qModule.getInsertInterface().execute(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()); 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 // // 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... QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
QTableMetaData joinTable = QContext.getQInstance().getTable(join.getLeftTable()); 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)) 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=?) // // query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getLeftTable()); queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR); QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter); 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 // // 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) // // 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<>(); List<Serializable> inputRecordJoinValues = new ArrayList<>();
QQueryFilter subFilter = new QQueryFilter(); QQueryFilter subFilter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns()) for(JoinOn joinOn : rightMostJoin.getJoinOns())
{ {
Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField()); Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField());
inputRecordJoinValues.add(inputRecordValue); inputRecordJoinValues.add(inputRecordValue);
subFilter.addCriteria(inputRecordValue == null subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(joinOn.getLeftField(), QCriteriaOperator.IS_BLANK) ? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue)); : new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
} }
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues)) if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
@ -265,11 +284,16 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
for(QRecord joinRecord : queryOutput.getRecords()) for(QRecord joinRecord : queryOutput.getRecords())
{ {
List<Serializable> joinRecordValues = new ArrayList<>(); List<Serializable> joinRecordValues = new ArrayList<>();
for(JoinOn joinOn : join.getJoinOns()) for(JoinOn joinOn : rightMostJoin.getJoinOns())
{ {
Serializable inputRecordValue = joinRecord.getValue(joinOn.getLeftField()); Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
joinRecordValues.add(inputRecordValue); if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
} }
joinRecordValues.add(joinValue);
}
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord); joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
} }
@ -286,8 +310,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues); QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", ""); String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
QFieldMetaData field = joinTable.getField(fieldName); QFieldMetaData field = leftMostJoinTable.getField(fieldName);
Serializable recordSecurityValue = joinRecord.getValue(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) for(QRecord inputRecord : inputRecords)
{ {
@ -298,7 +326,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
{ {
for(QRecord inputRecord : inputRecords) 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.");
} }
} }
} }

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.instances;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -496,6 +497,7 @@ public class QInstanceValidator
{ {
String prefix = "Table " + table.getName() + " "; String prefix = "Table " + table.getName() + " ";
RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{ {
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType(); String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
@ -521,23 +523,63 @@ public class QInstanceValidator
// don't bother trying to validate field names if we know we have a bad join. // // don't bother trying to validate field names if we know we have a bad join. //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins) if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
{
if(fieldName.contains("."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{ {
List<QueryJoin> joins = new ArrayList<>(); List<QueryJoin> joins = new ArrayList<>();
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 = table;
for(String joinName : joinNameChain)
{ {
QJoinMetaData join = qInstance.getJoin(joinName); QJoinMetaData join = qInstance.getJoin(joinName);
if(join.getLeftTable().equals(table.getName())) 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)); joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
} }
else if(join.getRightTable().equals(table.getName())) else if(join.getRightTable().equals(tmpTable.getName()))
{ {
joins.add(new QueryJoin(join.flip())); 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); 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(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior"); assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
} }

View File

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

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query; 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.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -75,25 +77,46 @@ public class JoinsContext
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())) 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 ->
{ {
/////////////////////////////////////////////////////// QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
// we're good - we're already joining on this table! // 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 else
{ {
QJoinMetaData join = instance.getJoin(joinName); throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + 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);
} }
} }
} }
@ -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);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -50,6 +50,7 @@ public class QJoinMetaData
public QJoinMetaData flip() public QJoinMetaData flip()
{ {
return (new QJoinMetaData() return (new QJoinMetaData()
.withName(name) // does this need to be different?, e.g., + "Flipped"?
.withLeftTable(rightTable) .withLeftTable(rightTable)
.withRightTable(leftTable) .withRightTable(leftTable)
.withType(type.flip()) .withType(type.flip())

View File

@ -28,13 +28,18 @@ import java.util.List;
/******************************************************************************* /*******************************************************************************
** Define (for a table) a lock that applies to records in the table - e.g., ** 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. ** 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 public class RecordSecurityLock
{ {
private String securityKeyType; private String securityKeyType;
private String fieldName; private String fieldName;
private List<String> joinNameChain; // todo - add validation in validator!! private List<String> joinNameChain;
private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY; private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY;

View File

@ -24,19 +24,24 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; 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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; 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.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.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; 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.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -134,10 +139,15 @@ public class MemoryRecordStore
{ {
incrementStatistic(input); incrementStatistic(input);
Map<Serializable, QRecord> tableData = getTableData(input.getTable()); Collection<QRecord> tableData = getTableData(input.getTable()).values();
List<QRecord> records = new ArrayList<>(); 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); 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());
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -27,6 +27,7 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean; 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.QFilterCriteria;
@ -71,6 +72,21 @@ public class BackendQueryFilterUtils
{ {
String fieldName = criterion.getFieldName(); String fieldName = criterion.getFieldName();
Serializable value = qRecord.getValue(fieldName); 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); boolean criterionMatches = doesCriteriaMatch(criterion, fieldName, value);

View File

@ -291,7 +291,130 @@ class InsertActionTest extends BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @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); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);

View File

@ -1605,8 +1605,25 @@ class QInstanceValidatorTest extends BaseTest
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("notAField")), "unrecognized field"); assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("notAField")), "unrecognized field");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setNullValueBehavior(null)), "missing a nullValueBehavior"); assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setNullValueBehavior(null)), "missing a nullValueBehavior");
// todo - remove once implemented 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");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("join.field")), "does not yet support finding a field that looks like a join field"); }
/*******************************************************************************
**
*******************************************************************************/
@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");
} }

View File

@ -596,6 +596,10 @@ public class TestUtils
.withName(TABLE_NAME_LINE_ITEM_EXTRINSIC) .withName(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME) .withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id") .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("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))

View File

@ -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.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate; 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.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.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; 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.QFilterCriteria;
@ -218,7 +219,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName); String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName);
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> 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) if(found == null)
{ {
throw (new RuntimeException("Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]")); 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 ** method that sub-classes should call to make a full WHERE clause, including
** security clauses. ** security clauses.
@ -403,6 +341,16 @@ public abstract class AbstractRDBMSAction implements QActionInterface
for(QueryJoin queryJoin : CollectionUtils.nonNullList(joinsContext.getQueryJoins())) 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()); QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())) for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
{ {
@ -437,42 +385,9 @@ public abstract class AbstractRDBMSAction implements QActionInterface
} }
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName(); String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
String fieldNameWithoutTablePrefix = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
String fieldNameTablePrefix = recordSecurityLock.getFieldName().replaceFirst("\\..*", "");
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain())) if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{ {
for(String joinName : recordSecurityLock.getJoinNameChain()) fieldName = recordSecurityLock.getFieldName();
{
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;
}
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -481,7 +396,19 @@ public abstract class AbstractRDBMSAction implements QActionInterface
QQueryFilter lockFilter = new QQueryFilter(); QQueryFilter lockFilter = new QQueryFilter();
List<QFilterCriteria> lockCriteria = new ArrayList<>(); List<QFilterCriteria> lockCriteria = new ArrayList<>();
lockFilter.setCriteria(lockCriteria); 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)) if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -82,7 +82,7 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
.toList(); .toList();
String columns = insertableFields.stream() String columns = insertableFields.stream()
.map(this::getColumnName) .map(f -> "`" + getColumnName(f) + "`")
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
String questionMarks = insertableFields.stream() String questionMarks = insertableFields.stream()
.map(x -> "?") .map(x -> "?")

View File

@ -25,6 +25,11 @@ package com.kingsrook.qqq.backend.module.rdbms;
import java.io.InputStream; import java.io.InputStream;
import java.sql.Connection; import java.sql.Connection;
import java.util.List; 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.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; 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.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; 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.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.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; 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_ORDER = "order";
public static final String TABLE_NAME_ITEM = "item"; public static final String TABLE_NAME_ITEM = "item";
public static final String TABLE_NAME_ORDER_LINE = "orderLine"; 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 = "warehouse";
public static final String TABLE_NAME_WAREHOUSE_STORE_INT = "warehouseStoreInt"; public static final String TABLE_NAME_WAREHOUSE_STORE_INT = "warehouseStoreInt";
@ -231,6 +238,7 @@ public class TestUtils
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order") qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId")) .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("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("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)) .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") 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("orderId", QFieldType.INTEGER).withBackendName("order_id"))
.withField(new QFieldMetaData("sku", QFieldType.STRING)) .withField(new QFieldMetaData("sku", QFieldType.STRING))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
.withField(new QFieldMetaData("quantity", QFieldType.INTEGER)) .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") qInstance.addTable(defineBaseTable(TABLE_NAME_WAREHOUSE_STORE_INT, "warehouse_store_int")
.withField(new QFieldMetaData("warehouseId", QFieldType.INTEGER).withBackendName("warehouse_id")) .withField(new QFieldMetaData("warehouseId", QFieldType.INTEGER).withBackendName("warehouse_id"))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id"))
@ -321,6 +344,14 @@ public class TestUtils
.withJoinOn(new JoinOn("storeId", "storeId")) .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() qInstance.addPossibleValueSource(new QPossibleValueSource()
.withName("store") .withName("store")
.withType(QPossibleValueSourceType.TABLE) .withType(QPossibleValueSourceType.TABLE)
@ -349,4 +380,16 @@ public class TestUtils
.withField(new QFieldMetaData("id", QFieldType.INTEGER)); .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());
}
} }

View File

@ -329,8 +329,7 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
aggregateResult = aggregateOutput.getResults().get(0); aggregateResult = aggregateOutput.getResults().get(0);
// note - this would be 33, except for that one order line that has a contradictory store id... Assertions.assertEquals(33, aggregateResult.getAggregateValue(sumOfQuantity));
Assertions.assertEquals(32, aggregateResult.getAggregateValue(sumOfQuantity));
} }

View File

@ -24,6 +24,9 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; 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 org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; 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 private void assertAnInsertedPersonRecord(String firstName, String lastName, Integer id) throws Exception
{ {
runTestSql("SELECT * FROM person WHERE last_name = '" + lastName + "'", (rs -> { runTestSql("SELECT * FROM person WHERE last_name = '" + lastName + "'", (rs -> {

View File

@ -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.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; 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.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.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -162,6 +163,7 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest
.withDataSource(new QReportDataSource() .withDataSource(new QReportDataSource()
.withSourceTable(TestUtils.TABLE_NAME_ORDER_LINE) .withSourceTable(TestUtils.TABLE_NAME_ORDER_LINE)
.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM).withAlias("i").withSelect(true)) .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM).withAlias("i").withSelect(true))
.withQueryFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id")))
) )
.withView(new QReportView() .withView(new QReportView()
.withType(ReportType.TABLE) .withType(ReportType.TABLE)
@ -179,13 +181,13 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest
assertEquals(""" assertEquals("""
"Line Item Id","Item SKU","Item Store Id","Item Store Name" "Line Item Id","Item SKU","Item Store Id","Item Store Name"
"1","QM-1","1","Q-Mart" "1","QM-1","1","Q-Mart"
"5","QM-1","1","Q-Mart"
"2","QM-2","1","Q-Mart" "2","QM-2","1","Q-Mart"
"3","QM-3","1","Q-Mart" "3","QM-3","1","Q-Mart"
"4","QRU-1","2","QQQ 'R' Us" "4","QRU-1","2","QQQ 'R' Us"
"5","QM-1","1","Q-Mart"
"6","QRU-1","2","QQQ 'R' Us" "6","QRU-1","2","QQQ 'R' Us"
"8","QRU-1","2","QQQ 'R' Us"
"7","QRU-2","2","QQQ 'R' Us" "7","QRU-2","2","QQQ 'R' Us"
"8","QRU-1","2","QQQ 'R' Us"
"9","QD-1","3","QDepot" "9","QD-1","3","QDepot"
"10","QD-1","3","QDepot" "10","QD-1","3","QDepot"
"11","QD-1","3","QDepot" "11","QD-1","3","QDepot"

View File

@ -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 (10, 'DHL International', 'DHL', 'I');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G'); 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 order_line;
DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS item;
DROP TABLE IF EXISTS `order`; DROP TABLE IF EXISTS `order`;
@ -138,7 +139,7 @@ CREATE TABLE order_line
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT REFERENCES `order`, order_id INT REFERENCES `order`,
sku VARCHAR(80), 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 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, 1);
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 2); INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 2);
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 3); 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)
);