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.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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -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())
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -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 -> "?")
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 -> {
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user