Merged feature/CE-882-add-functionality-of-sharing into feature/CE-1068-add-basic-functionality-of

This commit is contained in:
2024-04-30 10:36:24 -05:00
81 changed files with 8823 additions and 1457 deletions

View File

@ -149,7 +149,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
// sort the field names by their labels //
//////////////////////////////////////////
List<String> sortedFieldNames = table.getFields().keySet().stream()
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
.sorted(Comparator.comparing(fieldName -> Objects.requireNonNullElse(table.getFields().get(fieldName).getLabel(), fieldName)))
.toList();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());

View File

@ -0,0 +1,146 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
** Subclass of record pipe that ony allows through distinct records, based on
** the set of fields specified in the constructor as a uniqueKey.
*******************************************************************************/
public class DistinctFilteringRecordPipe extends RecordPipe
{
private UniqueKey uniqueKey;
private Set<Serializable> seenValues = new HashSet<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DistinctFilteringRecordPipe(UniqueKey uniqueKey)
{
this.uniqueKey = uniqueKey;
}
/*******************************************************************************
** Constructor that accepts pipe's overrideCapacity (allowed to be null)
**
*******************************************************************************/
public DistinctFilteringRecordPipe(UniqueKey uniqueKey, Integer overrideCapacity)
{
super(overrideCapacity);
this.uniqueKey = uniqueKey;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> records) throws QException
{
List<QRecord> recordsToAdd = new ArrayList<>();
for(QRecord record : records)
{
if(!seenBefore(record))
{
recordsToAdd.add(record);
}
}
if(recordsToAdd.isEmpty())
{
return;
}
super.addRecords(recordsToAdd);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecord(QRecord record) throws QException
{
if(seenBefore(record))
{
return;
}
super.addRecord(record);
}
/*******************************************************************************
** return true if we've seen this record before (based on the unique key) -
** also - update the set of seen values!
*******************************************************************************/
private boolean seenBefore(QRecord record)
{
Serializable ukValues = extractUKValues(record);
if(seenValues.contains(ukValues))
{
return true;
}
seenValues.add(ukValues);
return false;
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable extractUKValues(QRecord record)
{
if(uniqueKey.getFieldNames().size() == 1)
{
return (record.getValue(uniqueKey.getFieldNames().get(0)));
}
else
{
ArrayList<Serializable> rs = new ArrayList<>();
for(String fieldName : uniqueKey.getFieldNames())
{
rs.add(record.getValue(fieldName));
}
return (rs);
}
}
}

View File

@ -190,6 +190,9 @@ public class ExportAction
Set<String> addedJoinNames = new HashSet<>();
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
/////////////////////////////////////////////////////////////////////////////////////////////
// make sure that any tables being selected from are included as (LEFT) joins in the query //
/////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : exportInput.getFieldNames())
{
if(fieldName.contains("."))
@ -198,27 +201,7 @@ public class ExportAction
String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName))
{
QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true);
queryJoins.add(queryJoin);
/////////////////////////////////////////////////////////////////////////////////////////////
// in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
// This code basically mirrors what QFMD is doing right now, so it's better - //
// but shouldn't all of this just be in JoinsContext? it does some of this... //
/////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = exportInput.getTable();
Optional<ExposedJoin> exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
if(exposedJoinOptional.isEmpty())
{
throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
}
ExposedJoin exposedJoin = exposedJoinOptional.get();
if(exposedJoin.getJoinPath().size() == 1)
{
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
}
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
addedJoinNames.add(joinTableName);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -43,8 +44,9 @@ public class RecordPipe
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
private static final int DEFAULT_CAPACITY = 1_000;
private int capacity = 1_000;
private int capacity = DEFAULT_CAPACITY;
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(capacity);
private boolean isTerminated = false;
@ -70,11 +72,13 @@ public class RecordPipe
/*******************************************************************************
** Construct a record pipe, with an alternative capacity for the internal queue.
**
** overrideCapacity is allowed to be null - in which case, DEFAULT_CAPACITY is used.
*******************************************************************************/
public RecordPipe(Integer overrideCapacity)
{
this.capacity = overrideCapacity;
queue = new ArrayBlockingQueue<>(overrideCapacity);
this.capacity = Objects.requireNonNullElse(overrideCapacity, DEFAULT_CAPACITY);
queue = new ArrayBlockingQueue<>(this.capacity);
}

View File

@ -470,7 +470,7 @@ public class DeleteAction
QRecord recordWithError = new QRecord();
recordsWithErrors.add(recordWithError);
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + primaryKeyField.getLabel() + " = " + primaryKeyValue));
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + Objects.requireNonNullElse(primaryKeyField.getLabel(), primaryKeyField.getName()) + " = " + primaryKeyValue));
}
}
}

View File

@ -62,6 +62,7 @@ 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.UniqueKey;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.DuplicateKeyBadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -414,7 +415,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
{
record.addError(new BadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
record.addError(new DuplicateKeyBadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
foundDupe = true;
break;
}

View File

@ -68,6 +68,7 @@ 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.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -393,7 +394,12 @@ public class UpdateAction
QRecord oldRecord = lookedUpRecords.get(value);
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
}
}
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -42,13 +43,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
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.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -81,160 +85,273 @@ public class ValidateRecordSecurityLockHelper
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
{
return;
}
//////////////////////////////////////////////////////////////////////////////////////////
// we will be relying on primary keys being set in records - but (at least for inserts) //
// we might not have pkeys - so make them up (and clear them out at the end) //
//////////////////////////////////////////////////////////////////////////////////////////
Map<Serializable, QRecord> madeUpPrimaryKeys = makeUpPrimaryKeysIfNeeded(records, table);
////////////////////////////////
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>());
/////////////////////////////////
// propagate errors to records //
/////////////////////////////////
for(RecordWithErrors recordWithErrors : errorRecords.values())
{
recordWithErrors.propagateErrorsToRecord(locksToCheck);
}
/////////////////////////////////
// remove made-up primary keys //
/////////////////////////////////
String primaryKeyField = table.getPrimaryKeyField();
for(QRecord record : madeUpPrimaryKeys.values())
{
record.setValue(primaryKeyField, null);
}
}
/*******************************************************************************
** For a list of `records` from a `table`, and a given `action`, evaluate a
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
** of `errorRecords` - key'ed by primary key value (real or made up), with
** error messages existing in a tree, with positions matching the multi-lock
** tree that we're navigating, as tracked by `treePosition`.
**
** Recursively processes multi-locks (and the top-level call is always with a
** multi-lock - as the table's recordLocks list converted to an AND-multi-lock).
**
** Of note - for the case of READ_WRITE locks, we're only evaluating the values
** on the record, to see if they're allowed for us to store (because if we didn't
** have the key, we wouldn't have been able to read the value (which is verified
** outside of here, in UpdateAction/DeleteAction).
**
** BUT - WRITE locks - in their case, we read the record no matter what, and in
** here we need to verify we have a key that allows us to WRITE the record.
*******************************************************************************/
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
/////////////////////////////////////////////
// for multi-locks, make recursive descent //
/////////////////////////////////////////////
int i = 0;
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition);
treePosition.remove(treePosition.size() - 1);
i++;
}
return;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return;
}
////////////////////////////////
// actually check lock values //
// proceed w/ non-multi locks //
////////////////////////////////
for(RecordSecurityLock recordSecurityLock : locksToCheck)
String primaryKeyField = table.getPrimaryKeyField();
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
{
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
//////////////////////////////////////////////////////////////////////////////////
// handle the value being in the table we're inserting/updating (e.g., no join) //
//////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
for(QRecord record : records)
{
//////////////////////////////////////////////////////////////////////////////////
// handle the value being in the table we're inserting/updating (e.g., no join) //
//////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
for(QRecord record : records)
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// So if we're not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// So if we're not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
Serializable recordSecurityValue = record.getValue(field.getName());
validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action);
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
}
}
else
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
////////////////////////////////
// todo probably, but more... //
////////////////////////////////
// if(leftMostJoin.getLeftTable().equals(table.getName()))
// {
// leftMostJoin = leftMostJoin.flip();
// }
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
////////////////////////////////////////////////////////////////////////////////////////////////
// set up a query for joined records //
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
for(String joinName : recordSecurityLock.getJoinNameChain())
{
////////////////////////////////////////////////////////////////////////////////////////////////
// set up a query for joined records //
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);
for(String joinName : recordSecurityLock.getJoinNameChain())
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
{
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
// also build up the query's sub-filters here (only adding them if they're unique). //
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
///////////////////////////////////////////////////////////////////////////////////////////////////
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
for(QRecord inputRecord : inputRecordPage)
{
List<Serializable> inputRecordJoinValues = new ArrayList<>();
QQueryFilter subFilter = new QQueryFilter();
boolean updatingAnyLockJoinFields = false;
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
inputRecordJoinValues.add(inputRecordValue);
// if we have a value in this field (and it's not the primary key), then it means we're updating part of the lock
if(inputRecordValue != null && !joinOn.getRightField().equals(table.getPrimaryKeyField()))
{
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
updatingAnyLockJoinFields = true;
}
subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// 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) //
// also build up the query's sub-filters here (only adding them if they're unique). //
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
///////////////////////////////////////////////////////////////////////////////////////////////////
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
for(QRecord inputRecord : inputRecordPage)
//////////////////////////////////
// todo maybe, some version of? //
//////////////////////////////////
// if(action.equals(Action.UPDATE) && !updatingAnyLockJoinFields && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
// {
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
// // if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// // So if we're not updating the security field, then no error can come from it! //
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
// continue;
// }
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
List<Serializable> inputRecordJoinValues = new ArrayList<>();
QQueryFilter subFilter = new QQueryFilter();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
inputRecordJoinValues.add(inputRecordValue);
subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
}
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
////////////////////////////////////////////////////////////////////////////////
// only add this sub-filter if it's for a list of keys we haven't seen before //
////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(subFilter);
}
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
////////////////////////////////////////////////////////////////////////////////
// only add this sub-filter if it's for a list of keys we haven't seen before //
////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(subFilter);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
for(QRecord joinRecord : queryOutput.getRecords())
{
List<Serializable> joinRecordValues = new ArrayList<>();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
}
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
for(QRecord joinRecord : queryOutput.getRecords())
{
List<Serializable> joinRecordValues = new ArrayList<>();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
// isn't allowed. if it is found, then validate its value matches this session's security keys //
//////////////////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
// isn't allowed. if it is found, then validate its value matches this session's security keys //
//////////////////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
{
List<Serializable> inputRecordJoinValues = entry.getKey();
List<QRecord> inputRecords = entry.getValue();
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
List<Serializable> inputRecordJoinValues = entry.getKey();
List<QRecord> inputRecords = entry.getValue();
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
{
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
}
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
}
for(QRecord inputRecord : inputRecords)
{
validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action);
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
}
}
else
}
else
{
for(QRecord inputRecord : inputRecords)
{
for(QRecord inputRecord : inputRecords)
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
}
PermissionDeniedMessage error = new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).add(error, treePosition);
}
}
}
@ -246,45 +363,78 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
** for tracking errors, we use primary keys. add "made up" ones to records
** if needed (e.g., insert use-case).
*******************************************************************************/
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
private static Map<Serializable, QRecord> makeUpPrimaryKeysIfNeeded(List<QRecord> records, QTableMetaData table)
{
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
recordSecurityLocks = switch(action)
String primaryKeyField = table.getPrimaryKeyField();
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
Integer madeUpPrimaryKey = -1;
for(QRecord record : records)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
if(record.getValue(primaryKeyField) == null)
{
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
record.setValue(primaryKeyField, madeUpPrimaryKey);
madeUpPrimaryKey--;
}
}
return madeUpPrimaryKeys;
}
/*******************************************************************************
** For a given table & action type, convert the table's record locks to a
** MultiRecordSecurityLock, with only the appropriate lock-scopes being included
** (e.g., read-locks for selects, write-locks for insert/update/delete).
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
static MultiRecordSecurityLock getRecordSecurityLocks(QTableMetaData table, Action action)
{
List<RecordSecurityLock> allLocksOnTable = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
MultiRecordSecurityLock locksOfType = switch(action)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLockTree(allLocksOnTable);
case SELECT -> RecordSecurityLockFilters.filterForReadLockTree(allLocksOnTable);
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
};
if(action.equals(Action.UPDATE))
{
////////////////////////////////////////////////////////
// when doing an update, convert all OR's to AND's... //
////////////////////////////////////////////////////////
updateOperators(locksOfType, MultiRecordSecurityLock.BooleanOperator.AND);
}
////////////////////////////////////////
// if there are no locks, just return //
////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks))
if(locksOfType == null || CollectionUtils.nullSafeIsEmpty(locksOfType.getLocks()))
{
return (null);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
return (locksOfType);
}
/*******************************************************************************
** for a full multi-lock tree, set all of the boolean operators to the specified one.
*******************************************************************************/
private static void updateOperators(MultiRecordSecurityLock multiLock, MultiRecordSecurityLock.BooleanOperator operator)
{
multiLock.setOperator(operator);
for(RecordSecurityLock childLock : multiLock.getLocks())
{
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
if(childLock instanceof MultiRecordSecurityLock childMultiLock)
{
LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
}
else
{
locksToCheck.add(recordSecurityLock);
updateOperators(childMultiLock, operator);
}
}
return (locksToCheck);
}
@ -292,7 +442,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{
if(recordSecurityValue == null)
{
@ -302,7 +452,7 @@ public class ValidateRecordSecurityLockHelper
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)));
}
}
else
@ -314,15 +464,305 @@ public class ValidateRecordSecurityLockHelper
///////////////////////////////////////////////////////////////////////////////////////////////
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
///////////////////////////////////////////////////////////////////////////////////////////////
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record."));
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record.")));
}
else
{
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()));
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel())));
}
}
}
return (Collections.emptyList());
}
/*******************************************************************************
** Class to track errors that we're associating with a record.
**
** More complex than it first seems to be needed, because as we're evaluating
** locks, we might find some, but based on the boolean condition associated with
** them, they might not actually be record-level errors.
**
** e.g., two locks with an OR relationship - as long as one passes, the record
** should have no errors. And so-on through the tree of locks/multi-locks.
**
** Stores the errors in a tree of ErrorTreeNode objects.
**
** References into that tree are achieved via a List of Integer called "tree positions"
** where each entry in the list denotes the index of the tree node at that level.
**
** e.g., given this tree:
** <pre>
** A B
** / \ /|\
** C D E F G
** |
** H
** </pre>
**
** The positions of each node would be:
** <pre>
** A: [0]
** B: [1]
** C: [0,0]
** D: [0,1]
** E: [1,0]
** F: [1,1]
** G: [1,2]
** H: [0,1,0]
** </pre>
*******************************************************************************/
static class RecordWithErrors
{
private QRecord record;
private ErrorTreeNode errorTree;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RecordWithErrors(QRecord record)
{
this.record = record;
}
/*******************************************************************************
** add a list of errors, for a given list of tree positions
*******************************************************************************/
public void addAll(List<QErrorMessage> recordErrors, List<Integer> treePositions)
{
if(errorTree == null)
{
errorTree = new ErrorTreeNode();
}
ErrorTreeNode node = errorTree;
for(Integer treePosition : treePositions)
{
if(node.children == null)
{
node.children = new ArrayList<>(treePosition);
}
while(treePosition >= node.children.size())
{
node.children.add(null);
}
if(node.children.get(treePosition) == null)
{
node.children.set(treePosition, new ErrorTreeNode());
}
node = node.children.get(treePosition);
}
if(node.errors == null)
{
node.errors = new ArrayList<>();
}
node.errors.addAll(recordErrors);
}
/*******************************************************************************
** add a single error to a given tree-position
*******************************************************************************/
public void add(QErrorMessage error, List<Integer> treePositions)
{
addAll(List.of(error), treePositions);
}
/*******************************************************************************
** after the tree of errors has been built - walk a lock-tree (locksToCheck)
** and resolve boolean operations, to get a final list of errors (possibly empty)
** to put on the record.
*******************************************************************************/
public void propagateErrorsToRecord(MultiRecordSecurityLock locksToCheck)
{
List<QErrorMessage> errors = recursivePropagation(locksToCheck, new ArrayList<>());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
}
}
/*******************************************************************************
** recursive implementation of the propagation method - e.g., walk tree applying
** boolean logic.
*******************************************************************************/
private List<QErrorMessage> recursivePropagation(MultiRecordSecurityLock locksToCheck, List<Integer> treePositions)
{
//////////////////////////////////////////////////////////////////
// build a list of errors at this level (and deeper levels too) //
//////////////////////////////////////////////////////////////////
List<QErrorMessage> errorsFromThisLevel = new ArrayList<>();
int i = 0;
for(RecordSecurityLock lock : locksToCheck.getLocks())
{
List<QErrorMessage> errorsFromThisLock;
treePositions.add(i);
if(lock instanceof MultiRecordSecurityLock childMultiLock)
{
errorsFromThisLock = recursivePropagation(childMultiLock, treePositions);
}
else
{
errorsFromThisLock = getErrorsFromTree(treePositions);
}
errorsFromThisLevel.addAll(errorsFromThisLock);
treePositions.remove(treePositions.size() - 1);
i++;
}
if(MultiRecordSecurityLock.BooleanOperator.AND.equals(locksToCheck.getOperator()))
{
//////////////////////////////////////////////////////////////
// for an AND - if there were ANY errors, then return them. //
//////////////////////////////////////////////////////////////
if(!errorsFromThisLevel.isEmpty())
{
return (errorsFromThisLevel);
}
}
else // OR
{
//////////////////////////////////////////////////////////
// for an OR - only return if ALL conditions had errors //
//////////////////////////////////////////////////////////
if(errorsFromThisLevel.size() == locksToCheck.getLocks().size())
{
return (errorsFromThisLevel); // todo something smarter?
}
}
///////////////////////////////////
// else - no errors - empty list //
///////////////////////////////////
return Collections.emptyList();
}
/*******************************************************************************
**
*******************************************************************************/
private List<QErrorMessage> getErrorsFromTree(List<Integer> treePositions)
{
ErrorTreeNode node = errorTree;
for(Integer treePosition : treePositions)
{
if(node.children == null)
{
return Collections.emptyList();
}
if(treePosition >= node.children.size())
{
return Collections.emptyList();
}
if(node.children.get(treePosition) == null)
{
return Collections.emptyList();
}
node = node.children.get(treePosition);
}
if(node.errors == null)
{
return Collections.emptyList();
}
return node.errors;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
try
{
return JsonUtils.toPrettyJson(this);
}
catch(Exception e)
{
return "error in toString";
}
}
}
/*******************************************************************************
** tree node used by RecordWithErrors
*******************************************************************************/
static class ErrorTreeNode
{
private List<QErrorMessage> errors;
private ArrayList<ErrorTreeNode> children;
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
try
{
return JsonUtils.toPrettyJson(this);
}
catch(Exception e)
{
return "error in toString";
}
}
/*******************************************************************************
** Getter for errors - only here for Jackson/toString
**
*******************************************************************************/
public List<QErrorMessage> getErrors()
{
return errors;
}
/*******************************************************************************
** Getter for children - only here for Jackson/toString
**
*******************************************************************************/
public ArrayList<ErrorTreeNode> getChildren()
{
return children;
}
}
}

View File

@ -560,20 +560,47 @@ public class QPossibleValueTranslator
*******************************************************************************/
private void primePvsCache(String tableName, List<QPossibleValueSource> possibleValueSources, Collection<Serializable> values)
{
String idField = null;
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
String thisPvsIdField;
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
{
thisPvsIdField = possibleValueSource.getOverrideIdField();
}
else
{
thisPvsIdField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
}
if(idField == null)
{
idField = thisPvsIdField;
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// does this ever happen? maybe not... because, like, the list of values probably wouldn't make sense for //
// more than one field in the table... //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!idField.equals(thisPvsIdField))
{
for(QPossibleValueSource valueSource : possibleValueSources)
{
primePvsCache(tableName, List.of(valueSource), values);
}
}
}
}
try
{
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page)));
queryInput.setTransaction(getTransaction(tableName));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -618,7 +645,7 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////////////////////////////////////////
for(QRecord record : queryOutput.getRecords())
{
Serializable pkeyValue = record.getValue(primaryKeyField);
Serializable pkeyValue = record.getValue(idField);
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
QPossibleValue<?> possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel());

View File

@ -275,8 +275,19 @@ public class SearchPossibleValueSourceAction
queryInput.setFilter(queryFilter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
QueryOutput queryOutput = new QueryAction().execute(queryInput);
String fieldName;
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
{
fieldName = possibleValueSource.getOverrideIdField();
}
else
{
fieldName = table.getPrimaryKeyField();
}
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
output.setResults(qPossibleValues);

View File

@ -86,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
@ -624,6 +625,11 @@ public class QInstanceValidator
supplementalTableMetaData.validate(qInstance, table, this);
}
if(table.getShareableTableMetaData() != null)
{
table.getShareableTableMetaData().validate(qInstance, table, this);
}
runPlugins(QTableMetaData.class, table, qInstance);
});
}
@ -711,7 +717,6 @@ public class QInstanceValidator
{
String prefix = "Table " + table.getName() + " ";
RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
@ -719,91 +724,129 @@ public class QInstanceValidator
continue;
}
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix);
}
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
else
{
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
hasAnyBadJoins = true;
}
validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix);
}
}
}
String fieldName = recordSecurityLock.getFieldName();
////////////////////////////////////////////////////////////////////////////////
// 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)
/*******************************************************************************
**
*******************************************************************************/
private void validateMultiRecordSecurityLock(QInstance qInstance, QTableMetaData table, MultiRecordSecurityLock multiRecordSecurityLock, String prefix)
{
assertCondition(multiRecordSecurityLock.getOperator() != null, prefix + "has a MultiRecordSecurityLock that is missing an operator");
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
validateRecordSecurityLock(qInstance, table, lock, prefix);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateRecordSecurityLock(QInstance qInstance, QTableMetaData table, RecordSecurityLock recordSecurityLock, String prefix)
{
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
{
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
}
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
if(fieldName.contains("."))
hasAnyBadJoins = true;
}
}
String fieldName = recordSecurityLock.getFieldName();
////////////////////////////////////////////////////////////////////////////////
// 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(fieldName.contains("."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
String[] split = fieldName.split("\\.");
String joinTableName = split[0];
String joinFieldName = split[1];
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
{
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
return;
}
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);
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
return;
}
}
assertCondition(Objects.equals(tmpTable.getName(), joinTableName), prefix + "has a joinNameChain doesn't end in the expected table [" + joinTableName + "] (was: " + tmpTable.getName() + ")");
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

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -139,6 +140,24 @@ public class DeleteInput extends AbstractTableActionInput
/*******************************************************************************
** Fluently add 1 primary key to the delete input
**
*******************************************************************************/
public DeleteInput withPrimaryKey(Serializable primaryKey)
{
if(primaryKeys == null)
{
primaryKeys = new ArrayList<>();
}
primaryKeys.add(primaryKey);
return (this);
}
/*******************************************************************************
** Setter for ids
**

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -37,12 +38,17 @@ import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
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.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -60,11 +66,23 @@ public class JoinsContext
private final String mainTableName;
private final List<QueryJoin> queryJoins;
private final QQueryFilter securityFilter;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// pointer either at securityFilter, or at a sub-filter within it, for when we're doing a recursive build-out of multi-locks //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private QQueryFilter securityFilterCursor;
////////////////////////////////////////////////////////////////
// note - will have entries for all tables, not just aliases. //
////////////////////////////////////////////////////////////////
private final Map<String, String> aliasToTableNameMap = new HashMap<>();
private Level logLevel = Level.OFF;
/////////////////////////////////////////////////////////////////////////////
// we will get a TON of more output if this gets turned up, so be cautious //
/////////////////////////////////////////////////////////////////////////////
private Level logLevel = Level.OFF;
private Level logLevelForFilter = Level.OFF;
@ -74,54 +92,225 @@ public class JoinsContext
*******************************************************************************/
public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
{
log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
this.instance = instance;
this.mainTableName = tableName;
this.queryJoins = new MutableList<>(queryJoins);
this.securityFilter = new QQueryFilter();
this.securityFilterCursor = this.securityFilter;
// log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
dumpDebug(true, false);
for(QueryJoin queryJoin : this.queryJoins)
{
log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
processQueryJoin(queryJoin);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// make sure that all tables specified in filter columns are being brought into the query as joins //
/////////////////////////////////////////////////////////////////////////////////////////////////////
ensureFilterIsRepresented(filter);
logFilter("After ensureFilterIsRepresented:", securityFilter);
///////////////////////////////////////////////////////////////////////////////////////
// ensure that any record locks on the main table, which require a join, are present //
///////////////////////////////////////////////////////////////////////////////////////
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()));
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
ensureRecordSecurityLockIsRepresented(tableName, tableName, lock, null);
logFilter("After ensureRecordSecurityLockIsRepresented[fieldName=" + lock.getFieldName() + "]:", securityFilter);
}
///////////////////////////////////////////////////////////////////////////////////
// make sure that all joins in the query have meta data specified //
// e.g., a user-added join may just specify the join-table //
// or a join implicitly added from a filter may also not have its join meta data //
///////////////////////////////////////////////////////////////////////////////////
fillInMissingJoinMetaData();
logFilter("After fillInMissingJoinMetaData:", securityFilter);
///////////////////////////////////////////////////////////////
// ensure any joins that contribute a recordLock are present //
///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
{
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
}
ensureAllJoinRecordSecurityLocksAreRepresented(instance);
logFilter("After ensureAllJoinRecordSecurityLocksAreRepresented:", securityFilter);
ensureFilterIsRepresented(filter);
addJoinsFromExposedJoinPaths();
/* todo!!
for(QueryJoin queryJoin : queryJoins)
{
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
{
// addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
}
}
*/
////////////////////////////////////////////////////////////////////////////////////
// if there were any security filters built, then put those into the input filter //
////////////////////////////////////////////////////////////////////////////////////
addSecurityFiltersToInputFilter(filter);
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
log("--- END ------------------------------------------------------------------------");
log("", logPair("securityFilter", securityFilter));
log("", logPair("fullFilter", filter));
dumpDebug(false, true);
// log("--- END ------------------------------------------------------------------------");
}
/*******************************************************************************
**
** Update the input filter with any security filters that were built.
*******************************************************************************/
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
private void addSecurityFiltersToInputFilter(QQueryFilter filter)
{
////////////////////////////////////////////////////////////////////////////////////
// if there's no security filter criteria (including sub-filters), return w/ noop //
////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(securityFilter.getSubFilters()))
{
return;
}
///////////////////////////////////////////////////////////////////////
// if the input filter is an OR we need to replace it with a new AND //
///////////////////////////////////////////////////////////////////////
if(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.OR))
{
List<QFilterCriteria> originalCriteria = filter.getCriteria();
List<QQueryFilter> originalSubFilters = filter.getSubFilters();
QQueryFilter replacementFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
replacementFilter.setCriteria(originalCriteria);
replacementFilter.setSubFilters(originalSubFilters);
filter.setCriteria(new ArrayList<>());
filter.setSubFilters(new ArrayList<>());
filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
filter.addSubFilter(replacementFilter);
}
filter.addSubFilter(securityFilter);
}
/*******************************************************************************
** In case we've added any joins to the query that have security locks which
** weren't previously added to the query, add them now. basically, this is
** calling ensureRecordSecurityLockIsRepresented for each queryJoin.
*******************************************************************************/
private void ensureAllJoinRecordSecurityLocksAreRepresented(QInstance instance) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// avoid concurrent modification exceptions by doing a double-loop and breaking the inner any time anything gets added //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Set<QueryJoin> processedQueryJoins = new HashSet<>();
boolean addedAnyThisIteration = true;
while(addedAnyThisIteration)
{
addedAnyThisIteration = false;
for(QueryJoin queryJoin : this.queryJoins)
{
boolean addedAnyForThisJoin = false;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// avoid double-processing the same query join //
// or adding security filters for a join who was only added to the query so that we could add locks (an ImplicitQueryJoinForSecurityLock) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(processedQueryJoins.contains(queryJoin) || queryJoin instanceof ImplicitQueryJoinForSecurityLock)
{
continue;
}
processedQueryJoins.add(queryJoin);
//////////////////////////////////////////////////////////////////////////////////////////
// process all locks on this join's join-table. keep track if any new joins were added //
//////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()));
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
List<QueryJoin> addedQueryJoins = ensureRecordSecurityLockIsRepresented(joinTable.getName(), queryJoin.getJoinTableOrItsAlias(), lock, queryJoin);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if any joins were added by this call, add them to the set of processed ones, so they don't get re-processed. //
// also mark the flag that any were added for this join, to manage the double-looping //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(addedQueryJoins))
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make all new joins added in that method be of the same type (inner/left/etc) as the query join they are connected to //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin addedQueryJoin : addedQueryJoins)
{
addedQueryJoin.setType(queryJoin.getType());
}
processedQueryJoins.addAll(addedQueryJoins);
addedAnyForThisJoin = true;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
// if any new joins were added, we need to break the inner-loop, and continue the outer loop //
// e.g., to process the next query join (but we can't just go back to the foreach queryJoin, //
// because it would fail with concurrent modification) //
///////////////////////////////////////////////////////////////////////////////////////////////
if(addedAnyForThisJoin)
{
addedAnyThisIteration = true;
break;
}
}
}
}
/*******************************************************************************
** For a given recordSecurityLock on a given table (with a possible alias),
** make sure that if any joins are needed to get to the lock, that they are in the query.
**
** returns the list of query joins that were added, if any were added
*******************************************************************************/
private List<QueryJoin> ensureRecordSecurityLockIsRepresented(String tableName, String tableNameOrAlias, RecordSecurityLock recordSecurityLock, QueryJoin sourceQueryJoin) throws QException
{
List<QueryJoin> addedQueryJoins = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////
// if this lock is a multi-lock, then recursively process its child-locks //
////////////////////////////////////////////////////////////////////////////
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
log("Processing MultiRecordSecurityLock...");
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// make a new level in the filter-tree - storing old cursor, and updating cursor to point at new level //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter oldSecurityFilterCursor = this.securityFilterCursor;
QQueryFilter nextLevelSecurityFilter = new QQueryFilter();
this.securityFilterCursor.addSubFilter(nextLevelSecurityFilter);
this.securityFilterCursor = nextLevelSecurityFilter;
///////////////////////////////////////
// set the boolean operator to match //
///////////////////////////////////////
nextLevelSecurityFilter.setBooleanOperator(multiRecordSecurityLock.getOperator().toFilterOperator());
//////////////////////
// process children //
//////////////////////
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
log(" - Recursive call for childLock: " + childLock);
addedQueryJoins.addAll(ensureRecordSecurityLockIsRepresented(tableName, tableNameOrAlias, childLock, sourceQueryJoin));
}
////////////////////
// restore cursor //
////////////////////
this.securityFilterCursor = oldSecurityFilterCursor;
return addedQueryJoins;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// 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): //
// A join name chain is going to look like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where its 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, //
@ -129,30 +318,30 @@ public class JoinsContext
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
log("Evaluating recordSecurityLock. Join name chain is of length: " + joinNameChain.size(), logPair("tableNameOrAlias", tableNameOrAlias), logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
QTableMetaData tmpTable = instance.getTable(mainTableName);
QTableMetaData tmpTable = instance.getTable(tableName);
String securityFieldTableAlias = tableNameOrAlias;
String baseTableOrAlias = tableNameOrAlias;
boolean chainIsInner = true;
if(sourceQueryJoin != null && QueryJoin.Type.isOuter(sourceQueryJoin.getType()))
{
chainIsInner = false;
}
for(String joinName : joinNameChain)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// check the joins currently in the query - if any are for this table, then we don't need to add one //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> matchingJoins = this.queryJoins.stream().filter(queryJoin ->
//////////////////////////////////////////////////////////////////////////////////////////////////
// check the joins currently in the query - if any are THIS join, then we don't need to add one //
//////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> matchingQueryJoins = this.queryJoins.stream().filter(queryJoin ->
{
QJoinMetaData joinMetaData = null;
if(queryJoin.getJoinMetaData() != null)
{
joinMetaData = queryJoin.getJoinMetaData();
}
else
{
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
}
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}).toList();
if(CollectionUtils.nullSafeHasContents(matchingJoins))
if(CollectionUtils.nullSafeHasContents(matchingQueryJoins))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
@ -160,11 +349,40 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
log("- skipping join already in the query", logPair("joinName", joinName));
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
QueryJoin matchedQueryJoin = matchingQueryJoins.get(0);
if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
{
chainIsInner = false;
}
/* ?? todo ??
if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
{
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
matchingJoins.get(0).setType(QueryJoin.Type.INNER);
matchedQueryJoin.setType(QueryJoin.Type.INNER);
}
*/
//////////////////////////////////////////////////////////////////////////////////////////////////////
// as we're walking from tmpTable to the table which ultimately has the security key field, //
// if the queryJoin we just found is joining out to tmpTable, then we need to advance tmpTable back //
// to the queryJoin's base table - else, tmpTable advances to the matched queryJoin's joinTable //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if(tmpTable.getName().equals(matchedQueryJoin.getJoinTable()))
{
securityFieldTableAlias = Objects.requireNonNullElse(matchedQueryJoin.getBaseTableOrAlias(), mainTableName);
}
else
{
securityFieldTableAlias = matchedQueryJoin.getJoinTableOrItsAlias();
}
tmpTable = instance.getTable(securityFieldTableAlias);
////////////////////////////////////////////////////////////////////////////////////////
// set the baseTableOrAlias for the next iteration to be this join's joinTableOrAlias //
////////////////////////////////////////////////////////////////////////////////////////
baseTableOrAlias = securityFieldTableAlias;
continue;
}
@ -172,20 +390,233 @@ public class JoinsContext
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
securityFieldTableAlias = join.getRightTable() + "_forSecurityJoin_" + join.getName();
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
.withJoinMetaData(join)
.withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
.withBaseTableOrAlias(baseTableOrAlias)
.withAlias(securityFieldTableAlias);
if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
securityFieldTableAlias = join.getLeftTable() + "_forSecurityJoin_" + join.getName();
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
.withJoinMetaData(join.flip())
.withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
.withBaseTableOrAlias(baseTableOrAlias)
.withAlias(securityFieldTableAlias);
if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
dumpDebug(false, true);
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for the next iteration of the loop, set the next join's baseTableOrAlias to be the alias we just created //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
baseTableOrAlias = securityFieldTableAlias;
}
////////////////////////////////////////////////////////////////////////////////////
// now that we know the joins/tables are in the query, add to the security filter //
////////////////////////////////////////////////////////////////////////////////////
QueryJoin lastAddedQueryJoin = addedQueryJoins.isEmpty() ? null : addedQueryJoins.get(addedQueryJoins.size() - 1);
if(sourceQueryJoin != null && lastAddedQueryJoin == null)
{
lastAddedQueryJoin = sourceQueryJoin;
}
addSubFilterForRecordSecurityLock(recordSecurityLock, tmpTable, securityFieldTableAlias, !chainIsInner, lastAddedQueryJoin);
log("Finished evaluating recordSecurityLock");
return (addedQueryJoins);
}
/*******************************************************************************
**
*******************************************************************************/
private void addSubFilterForRecordSecurityLock(RecordSecurityLock recordSecurityLock, QTableMetaData table, String tableNameOrAlias, boolean isOuter, QueryJoin sourceQueryJoin)
{
QSession session = QContext.getQSession();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
boolean haveAllAccessKey = false;
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have all-access on this key, then we don't need a criterion for it (as long as we're in an AND filter) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
haveAllAccessKey = true;
if(sourceQueryJoin != null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it //
// this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sourceQueryJoin.withSecurityCriteria(new ArrayList<>());
}
////////////////////////////////////////////////////////////////////////////////////////
// if we're in an AND filter, then we don't need a criteria for this lock, so return. //
////////////////////////////////////////////////////////////////////////////////////////
boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
if(inAnAndFilter)
{
return;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////
// for locks w/o a join chain, the lock fieldName will simply be a field on the table. //
// so just prepend that with the tableNameOrAlias. //
/////////////////////////////////////////////////////////////////////////////////////////
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{
/////////////////////////////////////////////////////////////////////////////////
// else, expect a "table.field" in the lock fieldName - but we want to replace //
// the table name part with a possible alias that we took in. //
/////////////////////////////////////////////////////////////////////////////////
String[] parts = recordSecurityLock.getFieldName().split("\\.");
if(parts.length != 2)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Mal-formatted recordSecurityLock fieldName for lock with joinNameChain in query: " + fieldName);
}
fieldName = tableNameOrAlias + "." + parts[1];
}
///////////////////////////////////////////////////////////////////////////////////////////
// else - get the key values from the session and decide what kind of criterion to build //
///////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter lockFilter = new QQueryFilter();
List<QFilterCriteria> lockCriteria = new ArrayList<>();
lockFilter.setCriteria(lockCriteria);
QFieldType type = QFieldType.INTEGER;
try
{
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = getFieldAndTableNameOrAlias(fieldName);
type = fieldAndTableNameOrAlias.field().getType();
}
catch(Exception e)
{
LOG.debug("Error getting field type... Trying Integer", e);
}
if(haveAllAccessKey)
{
////////////////////////////////////////////////////////////////////////////////////////////
// if we have an all access key (but we got here because we're part of an OR query), then //
// write a criterion that will always be true - e.g., field=field //
////////////////////////////////////////////////////////////////////////////////////////////
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.TRUE));
}
else
{
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. //
// todo - maybe avoid running the whole query - as you're not allowed ANY records (based on boolean tree down to this point) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.FALSE));
}
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if user/session has some values, build an IN rule - //
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
}
else
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a sourceQueryJoin, then set the lockCriteria on that join - so it gets written into the JOIN ... ON clause //
// ... unless we're writing an OR filter. then we need the condition in the WHERE clause //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
boolean doNotPutCriteriaInJoinOn = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR;
if(sourceQueryJoin != null && !doNotPutCriteriaInJoinOn)
{
sourceQueryJoin.withSecurityCriteria(lockCriteria);
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we used to add an OR IS NULL for cases of an outer-join - but instead, this is now handled by putting the lockCriteria //
// into the join (see above) - so this check is probably deprecated. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this field is on the outer side of an outer join, then if we do a straight filter on it, then we're basically //
// nullifying the outer join... so for an outer join use-case, OR the security field criteria with a primary-key IS NULL //
// which will make missing rows from the join be found. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(isOuter)
{
if(table == null)
{
table = QContext.getQInstance().getTable(aliasToTableNameMap.get(tableNameOrAlias));
}
lockFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
lockFilter.addCriteria(new QFilterCriteria(tableNameOrAlias + "." + table.getPrimaryKeyField(), QCriteriaOperator.IS_BLANK));
}
*/
/////////////////////////////////////////////////////////////////////////////////////////////////////
// If this filter isn't for a queryJoin, then just add it to the main list of security sub-filters //
/////////////////////////////////////////////////////////////////////////////////////////////////////
this.securityFilterCursor.addSubFilter(lockFilter);
}
}
@ -197,9 +628,9 @@ public class JoinsContext
** use this method to add to the list, instead of ever adding directly, as it's
** important do to that process step (and we've had bugs when it wasn't done).
*******************************************************************************/
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
private void addQueryJoin(QueryJoin queryJoin, String reason, String logPrefix) throws QException
{
log("Adding query join to context",
log(Objects.requireNonNullElse(logPrefix, "") + "Adding query join to context",
logPair("reason", reason),
logPair("joinTable", queryJoin.getJoinTable()),
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
@ -208,34 +639,46 @@ public class JoinsContext
);
this.queryJoins.add(queryJoin);
processQueryJoin(queryJoin);
dumpDebug(false, false);
}
/*******************************************************************************
** If there are any joins in the context that don't have a join meta data, see
** if we can find the JoinMetaData to use for them by looking at the main table's
** exposed joins, and using their join paths.
** if we can find the JoinMetaData to use for them by looking at all joins in the
** instance, or at the main table's exposed joins, and using their join paths.
*******************************************************************************/
private void addJoinsFromExposedJoinPaths() throws QException
private void fillInMissingJoinMetaData() throws QException
{
log("Begin adding missing join meta data");
////////////////////////////////////////////////////////////////////////////////
// do a double-loop, to avoid concurrent modification on the queryJoins list. //
// that is to say, we'll loop over that list, but possibly add things to it, //
// in which case we'll set this flag, and break the inner loop, to go again. //
////////////////////////////////////////////////////////////////////////////////
boolean addedJoin;
Set<QueryJoin> processedQueryJoins = new HashSet<>();
boolean addedJoin;
do
{
addedJoin = false;
for(QueryJoin queryJoin : queryJoins)
{
if(processedQueryJoins.contains(queryJoin))
{
continue;
}
processedQueryJoins.add(queryJoin);
///////////////////////////////////////////////////////////////////////////////////////////////
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped //
///////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
if(joinMetaData != null)
{
log("- QueryJoin already has joinMetaData", logPair("joinMetaDataName", joinMetaData.getName()));
boolean isJoinLeftTableInQuery = false;
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
if(joinMetaDataLeftTable.equals(mainTableName))
@ -265,7 +708,7 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////
if(!isJoinLeftTableInQuery)
{
log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
log("- - Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
queryJoin.setJoinMetaData(joinMetaData.flip());
}
}
@ -275,11 +718,13 @@ public class JoinsContext
// try to find a direct join between the main table and this table. //
// if one is found, then put it (the meta data) on the query join. //
//////////////////////////////////////////////////////////////////////
log("- QueryJoin doesn't have metaData - looking for it", logPair("joinTableOrItsAlias", queryJoin.getJoinTableOrItsAlias()));
String baseTableName = Objects.requireNonNullElse(resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), mainTableName);
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
QJoinMetaData found = findJoinMetaData(baseTableName, queryJoin.getJoinTable(), true);
if(found != null)
{
log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
log("- - Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
queryJoin.setJoinMetaData(found);
}
else
@ -293,7 +738,7 @@ public class JoinsContext
{
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
{
log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
log("- - Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
/////////////////////////////////////////////////////////////////////////////////////
// loop backward through the join path (from the joinTable back to the main table) //
@ -304,6 +749,7 @@ public class JoinsContext
{
String joinName = exposedJoin.getJoinPath().get(i);
QJoinMetaData joinToAdd = instance.getJoin(joinName);
log("- - - evaluating joinPath element", logPair("i", i), logPair("joinName", joinName));
/////////////////////////////////////////////////////////////////////////////
// get the name from the opposite side of the join (flipping it if needed) //
@ -332,15 +778,22 @@ public class JoinsContext
queryJoin.setBaseTableOrAlias(nextTable);
}
queryJoin.setJoinMetaData(joinToAdd);
log("- - - - this is the last element in the join path, so setting this joinMetaData on the original queryJoin");
}
else
{
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
queryJoinToAdd.setType(queryJoin.getType());
addedAnyQueryJoins = true;
this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
log("- - - - this is not the last element in the join path, so adding a new query join:");
addQueryJoin(queryJoinToAdd, "forExposedJoin", "- - - - - - ");
dumpDebug(false, false);
}
}
else
{
log("- - - - join doesn't need added to the query");
}
tmpTable = nextTable;
}
@ -361,6 +814,7 @@ public class JoinsContext
}
while(addedJoin);
log("Done adding missing join meta data");
}
@ -370,12 +824,12 @@ public class JoinsContext
*******************************************************************************/
private boolean doesJoinNeedAddedToQuery(String joinName)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all queryJoins already in context - if any have this join's name, then we don't need this join... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all queryJoins already in context - if any have this join's name, and aren't implicit-security joins, then we don't need this join... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin queryJoin : queryJoins)
{
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName) && !(queryJoin instanceof ImplicitQueryJoinForSecurityLock))
{
return (false);
}
@ -395,6 +849,7 @@ public class JoinsContext
String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias();
if(aliasToTableNameMap.containsKey(tableNameOrAlias))
{
dumpDebug(false, true);
throw (new QException("Duplicate table name or alias: " + tableNameOrAlias));
}
aliasToTableNameMap.put(tableNameOrAlias, joinTable.getName());
@ -439,6 +894,7 @@ public class JoinsContext
String[] parts = fieldName.split("\\.");
if(parts.length != 2)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Mal-formatted field name in query: " + fieldName);
}
@ -449,6 +905,7 @@ public class JoinsContext
QTableMetaData table = instance.getTable(tableName);
if(table == null)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Could not find table [" + tableName + "] in instance for query");
}
return new FieldAndTableNameOrAlias(table.getField(baseFieldName), tableOrAlias);
@ -503,17 +960,17 @@ public class JoinsContext
for(String filterTable : filterTables)
{
log("Evaluating filterTable", logPair("filterTable", filterTable));
log("Evaluating filter", logPair("filterTable", filterTable));
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{
log("- table not in query - adding it", logPair("filterTable", filterTable));
log("- table not in query - adding a join for it", logPair("filterTable", filterTable));
boolean found = false;
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
{
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
if(queryJoin != null)
{
this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
addQueryJoin(queryJoin, "forFilter (join found in instance)", "- - ");
found = true;
break;
}
@ -522,9 +979,13 @@ public class JoinsContext
if(!found)
{
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
addQueryJoin(queryJoin, "forFilter (join not found in instance)", "- - ");
}
}
else
{
log("- table is already in query - not adding any joins", logPair("filterTable", filterTable));
}
}
}
@ -566,6 +1027,11 @@ public class JoinsContext
getTableNameFromFieldNameAndAddToSet(criteria.getOtherFieldName(), filterTables);
}
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(filter.getOrderBys()))
{
getTableNameFromFieldNameAndAddToSet(orderBy.getFieldName(), filterTables);
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
{
populateFilterTablesSet(subFilter, filterTables);
@ -592,7 +1058,7 @@ public class JoinsContext
/*******************************************************************************
**
*******************************************************************************/
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
public QJoinMetaData findJoinMetaData(String baseTableName, String joinTableName, boolean useExposedJoins)
{
List<QJoinMetaData> matches = new ArrayList<>();
if(baseTableName != null)
@ -644,7 +1110,29 @@ public class JoinsContext
}
else if(matches.size() > 1)
{
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
////////////////////////////////////////////////////////////////////////////////
// if we found more than one join, but we're allowed to useExposedJoins, then //
// see if we can tell which match to used based on the table's exposed joins //
////////////////////////////////////////////////////////////////////////////////
if(useExposedJoins)
{
QTableMetaData mainTable = QContext.getQInstance().getTable(mainTableName);
for(ExposedJoin exposedJoin : mainTable.getExposedJoins())
{
if(exposedJoin.getJoinTable().equals(joinTableName))
{
// todo ... is it wrong to always use 0??
return instance.getJoin(exposedJoin.getJoinPath().get(0));
}
}
}
///////////////////////////////////////////////
// if we couldn't figure it out, then throw. //
///////////////////////////////////////////////
dumpDebug(false, true);
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "] "
+ (useExposedJoins ? "(and exposed joins didn't clarify which one to use). " : "") + "Specify which one in your QueryJoin."));
}
return (null);
@ -669,4 +1157,79 @@ public class JoinsContext
LOG.log(logLevel, message, null, logPairs);
}
/*******************************************************************************
**
*******************************************************************************/
private void logFilter(String message, QQueryFilter filter)
{
if(logLevelForFilter.equals(Level.OFF))
{
return;
}
System.out.println(message + "\n" + filter);
}
/*******************************************************************************
** Print (to stdout, for easier reading) the object in a big table format for
** debugging. Happens any time logLevel is > OFF. Not meant for loggly.
*******************************************************************************/
private void dumpDebug(boolean isStart, boolean isEnd)
{
if(logLevel.equals(Level.OFF))
{
return;
}
int sm = 8;
int md = 30;
int lg = 50;
int overhead = 14;
int full = sm + 3 * md + lg + overhead;
if(isStart)
{
System.out.println("\n" + StringUtils.safeTruncate("--- Start [main table: " + this.mainTableName + "] " + "-".repeat(full), full));
}
StringBuilder rs = new StringBuilder();
String formatString = "| %-" + md + "s | %-" + md + "s %-" + md + "s | %-" + lg + "s | %-" + sm + "s |\n";
rs.append(String.format(formatString, "Base Table", "Join Table", "(Alias)", "Join Meta Data", "Type"));
String dashesLg = "-".repeat(lg);
String dashesMd = "-".repeat(md);
String dashesSm = "-".repeat(sm);
rs.append(String.format(formatString, dashesMd, dashesMd, dashesMd, dashesLg, dashesSm));
if(CollectionUtils.nullSafeHasContents(queryJoins))
{
for(QueryJoin queryJoin : queryJoins)
{
rs.append(String.format(
formatString,
StringUtils.hasContent(queryJoin.getBaseTableOrAlias()) ? StringUtils.safeTruncate(queryJoin.getBaseTableOrAlias(), md) : "--",
StringUtils.safeTruncate(queryJoin.getJoinTable(), md),
(StringUtils.hasContent(queryJoin.getAlias()) ? "(" + StringUtils.safeTruncate(queryJoin.getAlias(), md - 2) + ")" : ""),
queryJoin.getJoinMetaData() == null ? "--" : StringUtils.safeTruncate(queryJoin.getJoinMetaData().getName(), lg),
queryJoin.getType()));
}
}
else
{
rs.append(String.format(formatString, "-empty-", "", "", "", ""));
}
System.out.print(rs);
System.out.println(securityFilter);
if(isEnd)
{
System.out.println(StringUtils.safeTruncate("--- End " + "-".repeat(full), full) + "\n");
}
else
{
System.out.println(StringUtils.safeTruncate("-".repeat(full), full));
}
}
}

View File

@ -49,5 +49,7 @@ public enum QCriteriaOperator
IS_BLANK,
IS_NOT_BLANK,
BETWEEN,
NOT_BETWEEN
NOT_BETWEEN,
TRUE,
FALSE
}

View File

@ -306,6 +306,11 @@ public class QFilterCriteria implements Serializable, Cloneable
@Override
public String toString()
{
if(fieldName == null)
{
return ("<null-field-criteria>");
}
StringBuilder rs = new StringBuilder(fieldName);
try
{

View File

@ -138,7 +138,7 @@ public class QQueryFilter implements Serializable, Cloneable
/*******************************************************************************
**
** recursively look at both this filter, and any sub-filters it may have.
*******************************************************************************/
public boolean hasAnyCriteria()
{
@ -151,7 +151,7 @@ public class QQueryFilter implements Serializable, Cloneable
{
for(QQueryFilter subFilter : subFilters)
{
if(subFilter.hasAnyCriteria())
if(subFilter != null && subFilter.hasAnyCriteria())
{
return (true);
}
@ -361,23 +361,44 @@ public class QQueryFilter implements Serializable, Cloneable
StringBuilder rs = new StringBuilder("(");
try
{
int criteriaIndex = 0;
for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
{
rs.append(criterion).append(" ").append(getBooleanOperator()).append(" ");
if(criteriaIndex > 0)
{
rs.append(" ").append(getBooleanOperator()).append(" ");
}
rs.append(criterion);
criteriaIndex++;
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
if(CollectionUtils.nullSafeHasContents(subFilters))
{
rs.append(subFilter);
rs.append("Sub:{");
int subIndex = 0;
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
{
if(subIndex > 0)
{
rs.append(" ").append(getBooleanOperator()).append(" ");
}
rs.append(subFilter);
subIndex++;
}
rs.append("}");
}
rs.append(")");
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
if(CollectionUtils.nullSafeHasContents(orderBys))
{
rs.append(orderBy).append(",");
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
{
rs.append(orderBy).append(",");
}
rs.append("]");
}
rs.append("]");
}
catch(Exception e)
{

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -49,6 +51,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** specific joinMetaData to use must be set. The joinMetaData field can also be
** used instead of specify joinTable and baseTableOrAlias, but only for cases
** where the baseTable is not an alias.
**
** The securityCriteria member, in general, is meant to be populated when a
** JoinsContext is constructed before executing a query, and not meant to be set
** by users.
*******************************************************************************/
public class QueryJoin
{
@ -59,13 +65,30 @@ public class QueryJoin
private boolean select = false;
private Type type = Type.INNER;
private List<QFilterCriteria> securityCriteria = new ArrayList<>();
/*******************************************************************************
**
** define the types of joins - INNER, LEFT, RIGHT, or FULL.
*******************************************************************************/
public enum Type
{INNER, LEFT, RIGHT, FULL}
{
INNER,
LEFT,
RIGHT,
FULL;
/*******************************************************************************
** check if a join is an OUTER type (LEFT or RIGHT).
*******************************************************************************/
public static boolean isOuter(Type type)
{
return (LEFT == type || RIGHT == type);
}
}
@ -348,4 +371,50 @@ public class QueryJoin
return (this);
}
/*******************************************************************************
** Getter for securityCriteria
*******************************************************************************/
public List<QFilterCriteria> getSecurityCriteria()
{
return (this.securityCriteria);
}
/*******************************************************************************
** Setter for securityCriteria
*******************************************************************************/
public void setSecurityCriteria(List<QFilterCriteria> securityCriteria)
{
this.securityCriteria = securityCriteria;
}
/*******************************************************************************
** Fluent setter for securityCriteria
*******************************************************************************/
public QueryJoin withSecurityCriteria(List<QFilterCriteria> securityCriteria)
{
this.securityCriteria = securityCriteria;
return (this);
}
/*******************************************************************************
** Fluent setter for securityCriteria
*******************************************************************************/
public QueryJoin withSecurityCriteria(QFilterCriteria securityCriteria)
{
if(this.securityCriteria == null)
{
this.securityCriteria = new ArrayList<>();
}
this.securityCriteria.add(securityCriteria);
return (this);
}
}

View File

@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
@ -75,6 +76,8 @@ public class QFrontendTableMetaData
private boolean usesVariants;
private String variantTableLabel;
private ShareableTableMetaData shareableTableMetaData;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
//////////////////////////////////////////////////////////////////////////////////
@ -104,6 +107,8 @@ public class QFrontendTableMetaData
}
this.sections = tableMetaData.getSections();
this.shareableTableMetaData = tableMetaData.getShareableTableMetaData();
}
if(includeJoins)
@ -367,4 +372,14 @@ public class QFrontendTableMetaData
return (this.variantTableLabel);
}
/*******************************************************************************
** Getter for shareableTableMetaData
**
*******************************************************************************/
public ShareableTableMetaData getShareableTableMetaData()
{
return shareableTableMetaData;
}
}

View File

@ -53,6 +53,7 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
// for type = TABLE //
//////////////////////
private String tableName;
private String overrideIdField;
private List<String> searchFields;
private List<QFilterOrderBy> orderByFields;
@ -630,4 +631,35 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
qInstance.addPossibleValueSource(this);
}
/*******************************************************************************
** Getter for overrideIdField
*******************************************************************************/
public String getOverrideIdField()
{
return (this.overrideIdField);
}
/*******************************************************************************
** Setter for overrideIdField
*******************************************************************************/
public void setOverrideIdField(String overrideIdField)
{
this.overrideIdField = overrideIdField;
}
/*******************************************************************************
** Fluent setter for overrideIdField
*******************************************************************************/
public QPossibleValueSource withOverrideIdField(String overrideIdField)
{
this.overrideIdField = overrideIdField;
return (this);
}
}

View File

@ -0,0 +1,198 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Subclass of RecordSecurityLock, for combining multiple locks using a boolean
** (AND/OR) condition. Note that the combined locks can themselves also be
** Multi-locks, thus creating a tree of locks.
*******************************************************************************/
public class MultiRecordSecurityLock extends RecordSecurityLock implements Cloneable
{
private List<RecordSecurityLock> locks = new ArrayList<>();
private BooleanOperator operator;
/*******************************************************************************
**
*******************************************************************************/
@Override
protected MultiRecordSecurityLock clone() throws CloneNotSupportedException
{
MultiRecordSecurityLock clone = (MultiRecordSecurityLock) super.clone();
/////////////////////////
// deep-clone the list //
/////////////////////////
if(locks != null)
{
clone.locks = new ArrayList<>();
for(RecordSecurityLock lock : locks)
{
clone.locks.add(lock.clone());
}
}
return (clone);
}
/*******************************************************************************
**
*******************************************************************************/
public enum BooleanOperator
{
AND,
OR;
/*******************************************************************************
**
*******************************************************************************/
public QQueryFilter.BooleanOperator toFilterOperator()
{
return switch(this)
{
case AND -> QQueryFilter.BooleanOperator.AND;
case OR -> QQueryFilter.BooleanOperator.OR;
};
}
}
////////////////////////////////
// todo - remove, this is POC //
////////////////////////////////
static
{
new QTableMetaData()
.withName("savedReport")
.withRecordSecurityLock(new MultiRecordSecurityLock()
.withLocks(List.of(
new RecordSecurityLock()
.withFieldName("userId")
.withSecurityKeyType("user")
.withNullValueBehavior(NullValueBehavior.DENY)
.withLockScope(LockScope.READ_AND_WRITE),
new RecordSecurityLock()
.withFieldName("sharedReport.userId")
.withJoinNameChain(List.of("reportJoinSharedReport"))
.withSecurityKeyType("user")
.withNullValueBehavior(NullValueBehavior.DENY)
.withLockScope(LockScope.READ_AND_WRITE), // dynamic, from a value...
new RecordSecurityLock()
.withFieldName("sharedReport.groupId")
.withJoinNameChain(List.of("reportJoinSharedReport"))
.withSecurityKeyType("group")
.withNullValueBehavior(NullValueBehavior.DENY)
.withLockScope(LockScope.READ_AND_WRITE) // dynamic, from a value...
)));
}
/*******************************************************************************
** Getter for locks
*******************************************************************************/
public List<RecordSecurityLock> getLocks()
{
return (this.locks);
}
/*******************************************************************************
** Setter for locks
*******************************************************************************/
public void setLocks(List<RecordSecurityLock> locks)
{
this.locks = locks;
}
/*******************************************************************************
** Fluent setter for locks
*******************************************************************************/
public MultiRecordSecurityLock withLocks(List<RecordSecurityLock> locks)
{
this.locks = locks;
return (this);
}
/*******************************************************************************
** Fluently add one lock
*******************************************************************************/
public MultiRecordSecurityLock withLock(RecordSecurityLock lock)
{
if(this.locks == null)
{
this.locks = new ArrayList<>();
}
this.locks.add(lock);
return (this);
}
/*******************************************************************************
** Getter for operator
*******************************************************************************/
public BooleanOperator getOperator()
{
return (this.operator);
}
/*******************************************************************************
** Setter for operator
*******************************************************************************/
public void setOperator(BooleanOperator operator)
{
this.operator = operator;
}
/*******************************************************************************
** Fluent setter for operator
*******************************************************************************/
public MultiRecordSecurityLock withOperator(BooleanOperator operator)
{
this.operator = operator;
return (this);
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -41,7 +42,7 @@ import java.util.Map;
** - READ_AND_WRITE means that users cannot read or write records without a valid key.
** - WRITE means that users cannot write records without a valid key (but they can read them).
*******************************************************************************/
public class RecordSecurityLock
public class RecordSecurityLock implements Cloneable
{
private String securityKeyType;
private String fieldName;
@ -52,6 +53,28 @@ public class RecordSecurityLock
/*******************************************************************************
**
*******************************************************************************/
@Override
protected RecordSecurityLock clone() throws CloneNotSupportedException
{
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
/////////////////////////
// deep-clone the list //
/////////////////////////
if(joinNameChain != null)
{
clone.joinNameChain = new ArrayList<>();
clone.joinNameChain.addAll(joinNameChain);
}
return (clone);
}
/*******************************************************************************
** Constructor
**
@ -106,8 +129,9 @@ public class RecordSecurityLock
*******************************************************************************/
public enum LockScope
{
READ_AND_WRITE,
WRITE
READ_AND_WRITE, // lock both reads and writes
WRITE, // only lock writes
READ // only lock reads
}
@ -265,4 +289,22 @@ public class RecordSecurityLock
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "RecordSecurityLock{"
+ "securityKeyType='" + securityKeyType + '\''
+ ", fieldName='" + fieldName + '\''
+ ", joinNameChain=" + joinNameChain
+ ", nullValueBehavior=" + nullValueBehavior
+ ", lockScope=" + lockScope
+ '}';
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.List;
import java.util.Set;
/*******************************************************************************
@ -46,6 +47,65 @@ public class RecordSecurityLockFilters
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to reads.
*******************************************************************************/
public static MultiRecordSecurityLock filterForReadLockTree(List<RecordSecurityLock> recordSecurityLocks)
{
return filterForLockTree(recordSecurityLocks, Set.of(RecordSecurityLock.LockScope.READ_AND_WRITE, RecordSecurityLock.LockScope.READ));
}
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to writes.
*******************************************************************************/
public static MultiRecordSecurityLock filterForWriteLockTree(List<RecordSecurityLock> recordSecurityLocks)
{
return filterForLockTree(recordSecurityLocks, Set.of(RecordSecurityLock.LockScope.READ_AND_WRITE, RecordSecurityLock.LockScope.WRITE));
}
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to any of the
** input set of scopes.
*******************************************************************************/
private static MultiRecordSecurityLock filterForLockTree(List<RecordSecurityLock> recordSecurityLocks, Set<RecordSecurityLock.LockScope> allowedScopes)
{
if(recordSecurityLocks == null)
{
return (null);
}
//////////////////////////////////////////////////////////////
// at the top-level we build a multi-lock with AND operator //
//////////////////////////////////////////////////////////////
MultiRecordSecurityLock result = new MultiRecordSecurityLock();
result.setOperator(MultiRecordSecurityLock.BooleanOperator.AND);
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
MultiRecordSecurityLock filteredSubLock = filterForLockTree(multiRecordSecurityLock.getLocks(), allowedScopes);
filteredSubLock.setOperator(multiRecordSecurityLock.getOperator());
result.withLock(filteredSubLock);
}
else
{
if(allowedScopes.contains(recordSecurityLock.getLockScope()))
{
result.withLock(recordSecurityLock);
}
}
}
return (result);
}
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to writes.
*******************************************************************************/

View File

@ -0,0 +1,48 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.sharing;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.processes.implementations.sharing.ShareScope;
/*******************************************************************************
**
*******************************************************************************/
public class ShareScopePossibleValueMetaDataProducer implements MetaDataProducerInterface<QPossibleValueSource>
{
public static final String NAME = "shareScope";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValueSource produce(QInstance qInstance) throws QException
{
return QPossibleValueSource.newForEnum(NAME, ShareScope.values());
}
}

View File

@ -0,0 +1,186 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.sharing;
import java.io.Serializable;
/*******************************************************************************
** As a component of a ShareableTableMetaData instance, define details about
** one particular audience type.
**
** e.g., if a table can be shared to users and groups, there'd be 2 instances of
** this object - one like:
** - name: user
** - fieldName: userId
** - sourceTableName: User.TABLE_NAME
** - sourceTableKeyFieldName: email (e.g., can be a UK, not just the PKey)
**
** and another similar, w/ the group-type details.
*******************************************************************************/
public class ShareableAudienceType implements Serializable
{
private String name;
private String fieldName;
private String sourceTableName;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// maybe normally the primary key in the source table, but could be a unique-key instead sometimes //
/////////////////////////////////////////////////////////////////////////////////////////////////////
private String sourceTableKeyFieldName;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ShareableAudienceType()
{
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public ShareableAudienceType withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for fieldName
*******************************************************************************/
public String getFieldName()
{
return (this.fieldName);
}
/*******************************************************************************
** Setter for fieldName
*******************************************************************************/
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
/*******************************************************************************
** Fluent setter for fieldName
*******************************************************************************/
public ShareableAudienceType withFieldName(String fieldName)
{
this.fieldName = fieldName;
return (this);
}
/*******************************************************************************
** Getter for sourceTableName
*******************************************************************************/
public String getSourceTableName()
{
return (this.sourceTableName);
}
/*******************************************************************************
** Setter for sourceTableName
*******************************************************************************/
public void setSourceTableName(String sourceTableName)
{
this.sourceTableName = sourceTableName;
}
/*******************************************************************************
** Fluent setter for sourceTableName
*******************************************************************************/
public ShareableAudienceType withSourceTableName(String sourceTableName)
{
this.sourceTableName = sourceTableName;
return (this);
}
/*******************************************************************************
** Getter for sourceTableKeyFieldName
*******************************************************************************/
public String getSourceTableKeyFieldName()
{
return (this.sourceTableKeyFieldName);
}
/*******************************************************************************
** Setter for sourceTableKeyFieldName
*******************************************************************************/
public void setSourceTableKeyFieldName(String sourceTableKeyFieldName)
{
this.sourceTableKeyFieldName = sourceTableKeyFieldName;
}
/*******************************************************************************
** Fluent setter for sourceTableKeyFieldName
*******************************************************************************/
public ShareableAudienceType withSourceTableKeyFieldName(String sourceTableKeyFieldName)
{
this.sourceTableKeyFieldName = sourceTableKeyFieldName;
return (this);
}
}

View File

@ -0,0 +1,398 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.sharing;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** meta data to attach to a table, to describe that its records are shareable.
*******************************************************************************/
public class ShareableTableMetaData implements Serializable
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is the name of the table that is a many-to-one join to the table whose records are being shared. //
// not the table whose records are shared (the asset table) //
// for example: given that we want to share "savedReports", the value here could be "sharedSavedReports" //
// and this object will be attached to the savedReports table. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String sharedRecordTableName;
///////////////////////////////////////////////////////////////////////////////////////////////////////
// name of the field in the sharedRecordTable that has a foreign key pointing at the asset table //
///////////////////////////////////////////////////////////////////////////////////////////////////////
private String assetIdFieldName;
//////////////////////////////////////////////////////
// name of the scope field in the sharedRecordTable //
//////////////////////////////////////////////////////
private String scopeFieldName;
///////////////////////////////////////////////////////////
// map of audienceTypes names to type definition objects //
///////////////////////////////////////////////////////////
private Map<String, ShareableAudienceType> audienceTypes;
/////////////////////////////////////////////////
// PVS that lists the available audience types //
/////////////////////////////////////////////////
private String audienceTypesPossibleValueSourceName;
///////////////////////////////////////////////////
// PVS that lists the available audience records //
///////////////////////////////////////////////////
private String audiencePossibleValueSourceName;
//////////////////////////////////////////////////////////////
// name of a field in "this" table, that has the owner's id //
//////////////////////////////////////////////////////////////
private String thisTableOwnerIdFieldName;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ShareableTableMetaData()
{
}
/*******************************************************************************
** Getter for sharedRecordTableName
*******************************************************************************/
public String getSharedRecordTableName()
{
return (this.sharedRecordTableName);
}
/*******************************************************************************
** Setter for sharedRecordTableName
*******************************************************************************/
public void setSharedRecordTableName(String sharedRecordTableName)
{
this.sharedRecordTableName = sharedRecordTableName;
}
/*******************************************************************************
** Fluent setter for sharedRecordTableName
*******************************************************************************/
public ShareableTableMetaData withSharedRecordTableName(String sharedRecordTableName)
{
this.sharedRecordTableName = sharedRecordTableName;
return (this);
}
/*******************************************************************************
** Getter for assetIdFieldName
*******************************************************************************/
public String getAssetIdFieldName()
{
return (this.assetIdFieldName);
}
/*******************************************************************************
** Setter for assetIdFieldName
*******************************************************************************/
public void setAssetIdFieldName(String assetIdFieldName)
{
this.assetIdFieldName = assetIdFieldName;
}
/*******************************************************************************
** Fluent setter for assetIdFieldName
*******************************************************************************/
public ShareableTableMetaData withAssetIdFieldName(String assetIdFieldName)
{
this.assetIdFieldName = assetIdFieldName;
return (this);
}
/*******************************************************************************
** Getter for scopeFieldName
*******************************************************************************/
public String getScopeFieldName()
{
return (this.scopeFieldName);
}
/*******************************************************************************
** Setter for scopeFieldName
*******************************************************************************/
public void setScopeFieldName(String scopeFieldName)
{
this.scopeFieldName = scopeFieldName;
}
/*******************************************************************************
** Fluent setter for scopeFieldName
*******************************************************************************/
public ShareableTableMetaData withScopeFieldName(String scopeFieldName)
{
this.scopeFieldName = scopeFieldName;
return (this);
}
/*******************************************************************************
** Getter for audienceTypes
*******************************************************************************/
public Map<String, ShareableAudienceType> getAudienceTypes()
{
return (this.audienceTypes);
}
/*******************************************************************************
** Setter for audienceTypes
*******************************************************************************/
public void setAudienceTypes(Map<String, ShareableAudienceType> audienceTypes)
{
this.audienceTypes = audienceTypes;
}
/*******************************************************************************
** Fluent setter for audienceTypes
*******************************************************************************/
public ShareableTableMetaData withAudienceTypes(Map<String, ShareableAudienceType> audienceTypes)
{
this.audienceTypes = audienceTypes;
return (this);
}
/*******************************************************************************
** Fluent setter for audienceTypes
*******************************************************************************/
public ShareableTableMetaData withAudienceType(ShareableAudienceType audienceType)
{
if(this.audienceTypes == null)
{
this.audienceTypes = new LinkedHashMap<>();
}
if(audienceType.getName() == null)
{
throw (new IllegalArgumentException("Attempt to add an audience type without a name"));
}
if(this.audienceTypes.containsKey(audienceType.getName()))
{
throw (new IllegalArgumentException("Attempt to add more than 1 audience type with the same name [" + audienceType.getName() + "]"));
}
this.audienceTypes.put(audienceType.getName(), audienceType);
return (this);
}
/*******************************************************************************
** Getter for audienceTypesPossibleValueSourceName
*******************************************************************************/
public String getAudienceTypesPossibleValueSourceName()
{
return (this.audienceTypesPossibleValueSourceName);
}
/*******************************************************************************
** Setter for audienceTypesPossibleValueSourceName
*******************************************************************************/
public void setAudienceTypesPossibleValueSourceName(String audienceTypesPossibleValueSourceName)
{
this.audienceTypesPossibleValueSourceName = audienceTypesPossibleValueSourceName;
}
/*******************************************************************************
** Fluent setter for audienceTypesPossibleValueSourceName
*******************************************************************************/
public ShareableTableMetaData withAudienceTypesPossibleValueSourceName(String audienceTypesPossibleValueSourceName)
{
this.audienceTypesPossibleValueSourceName = audienceTypesPossibleValueSourceName;
return (this);
}
/*******************************************************************************
** Getter for thisTableOwnerIdFieldName
*******************************************************************************/
public String getThisTableOwnerIdFieldName()
{
return (this.thisTableOwnerIdFieldName);
}
/*******************************************************************************
** Setter for thisTableOwnerIdFieldName
*******************************************************************************/
public void setThisTableOwnerIdFieldName(String thisTableOwnerIdFieldName)
{
this.thisTableOwnerIdFieldName = thisTableOwnerIdFieldName;
}
/*******************************************************************************
** Fluent setter for thisTableOwnerIdFieldName
*******************************************************************************/
public ShareableTableMetaData withThisTableOwnerIdFieldName(String thisTableOwnerIdFieldName)
{
this.thisTableOwnerIdFieldName = thisTableOwnerIdFieldName;
return (this);
}
/*******************************************************************************
** Getter for audiencePossibleValueSourceName
*******************************************************************************/
public String getAudiencePossibleValueSourceName()
{
return (this.audiencePossibleValueSourceName);
}
/*******************************************************************************
** Setter for audiencePossibleValueSourceName
*******************************************************************************/
public void setAudiencePossibleValueSourceName(String audiencePossibleValueSourceName)
{
this.audiencePossibleValueSourceName = audiencePossibleValueSourceName;
}
/*******************************************************************************
** Fluent setter for audiencePossibleValueSourceName
*******************************************************************************/
public ShareableTableMetaData withAudiencePossibleValueSourceName(String audiencePossibleValueSourceName)
{
this.audiencePossibleValueSourceName = audiencePossibleValueSourceName;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
{
String prefix = "ShareableTableMetaData for table [" + tableMetaData.getName() + "]: ";
if(qInstanceValidator.assertCondition(StringUtils.hasContent(sharedRecordTableName), prefix + "missing sharedRecordTableName."))
{
boolean hasAssetIdFieldName = qInstanceValidator.assertCondition(StringUtils.hasContent(assetIdFieldName), prefix + "missing assetIdFieldName");
boolean hasScopeFieldName = qInstanceValidator.assertCondition(StringUtils.hasContent(scopeFieldName), prefix + "missing scopeFieldName");
QTableMetaData sharedRecordTable = qInstance.getTable(sharedRecordTableName);
boolean hasValidSharedRecordTable = qInstanceValidator.assertCondition(sharedRecordTable != null, prefix + "unrecognized sharedRecordTableName [" + sharedRecordTableName + "]");
if(hasValidSharedRecordTable && hasAssetIdFieldName)
{
qInstanceValidator.assertCondition(sharedRecordTable.getFields().containsKey(assetIdFieldName), prefix + "unrecognized assertIdFieldName [" + assetIdFieldName + "] in sharedRecordTable [" + sharedRecordTableName + "]");
}
if(hasValidSharedRecordTable && hasScopeFieldName)
{
qInstanceValidator.assertCondition(sharedRecordTable.getFields().containsKey(scopeFieldName), prefix + "unrecognized scopeFieldName [" + scopeFieldName + "] in sharedRecordTable [" + sharedRecordTableName + "]");
}
if(qInstanceValidator.assertCondition(CollectionUtils.nullSafeHasContents(audienceTypes), prefix + "missing audienceTypes"))
{
for(Map.Entry<String, ShareableAudienceType> entry : audienceTypes.entrySet())
{
ShareableAudienceType audienceType = entry.getValue();
qInstanceValidator.assertCondition(Objects.equals(entry.getKey(), audienceType.getName()), prefix + "inconsistent naming for shareableAudienceType [" + entry.getKey() + "] != [" + audienceType.getName() + "]");
if(qInstanceValidator.assertCondition(StringUtils.hasContent(audienceType.getFieldName()), prefix + "missing fieldName for shareableAudienceType [" + entry.getKey() + "]") && hasValidSharedRecordTable)
{
qInstanceValidator.assertCondition(sharedRecordTable.getFields().containsKey(audienceType.getFieldName()), prefix + "unrecognized fieldName [" + audienceType.getFieldName() + "] for shareableAudienceType [" + entry.getKey() + "] in sharedRecordTable [" + sharedRecordTableName + "]");
}
// todo - validate this audienceType.getSourceTableKeyFieldName() is a field, and it is a UKey
/* todo - make these optional i guess, because i didn't put user table in qqq
boolean hasSourceTableKeyFieldName = qInstanceValidator.assertCondition(StringUtils.hasContent(audienceType.getSourceTableKeyFieldName()), prefix + "missing sourceTableKeyFieldName for shareableAudienceType [" + entry.getKey() + "]");
if(qInstanceValidator.assertCondition(qInstance.getTable(audienceType.getSourceTableName()) != null, prefix + "unrecognized sourceTableName [" + audienceType.getSourceTableName() + "] for shareableAudienceType [" + entry.getKey() + "] in sharedRecordTable [" + sharedRecordTableName + "]") && hasSourceTableKeyFieldName)
{
qInstanceValidator.assertCondition(qInstance.getTable(audienceType.getSourceTableName()).getFields().containsKey(audienceType.getSourceTableKeyFieldName()), prefix + "unrecognized sourceTableKeyFieldName [" + audienceType.getSourceTableKeyFieldName() + "] for shareableAudienceType [" + entry.getKey() + "] in sharedRecordTable [" + sharedRecordTableName + "]");
}
*/
}
}
}
if(StringUtils.hasContent(thisTableOwnerIdFieldName))
{
qInstanceValidator.assertCondition(tableMetaData.getFields().containsKey(thisTableOwnerIdFieldName), prefix + "unrecognized thisTableOwnerIdFieldName [" + thisTableOwnerIdFieldName + "]");
}
if(StringUtils.hasContent(audienceTypesPossibleValueSourceName))
{
qInstanceValidator.assertCondition(qInstance.getPossibleValueSource(audienceTypesPossibleValueSourceName) != null, prefix + "unrecognized audienceTypesPossibleValueSourceName [" + audienceTypesPossibleValueSourceName + "]");
}
if(StringUtils.hasContent(audiencePossibleValueSourceName))
{
qInstanceValidator.assertCondition(qInstance.getPossibleValueSource(audiencePossibleValueSourceName) != null, prefix + "unrecognized audiencePossibleValueSourceName [" + audiencePossibleValueSourceName + "]");
}
}
}

View File

@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -107,6 +108,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private List<ExposedJoin> exposedJoins;
private ShareableTableMetaData shareableTableMetaData;
/*******************************************************************************
@ -1385,4 +1387,35 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
return (this);
}
/*******************************************************************************
** Getter for shareableTableMetaData
*******************************************************************************/
public ShareableTableMetaData getShareableTableMetaData()
{
return (this.shareableTableMetaData);
}
/*******************************************************************************
** Setter for shareableTableMetaData
*******************************************************************************/
public void setShareableTableMetaData(ShareableTableMetaData shareableTableMetaData)
{
this.shareableTableMetaData = shareableTableMetaData;
}
/*******************************************************************************
** Fluent setter for shareableTableMetaData
*******************************************************************************/
public QTableMetaData withShareableTableMetaData(ShareableTableMetaData shareableTableMetaData)
{
this.shareableTableMetaData = shareableTableMetaData;
return (this);
}
}

View File

@ -54,7 +54,7 @@ public class SavedReport extends QRecordEntity
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "Table", isRequired = true)
private String tableName;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID)
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID, label = "Owner")
private String userId;
@QField(label = "Query Filter")

View File

@ -47,10 +47,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableAudienceType;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RenderSavedReportMetaDataProducer;
import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RunScheduledReportMetaDataProducer;
@ -62,7 +66,8 @@ public class SavedReportsMetaDataProvider
{
public static final String REPORT_STORAGE_TABLE_NAME = "reportStorage";
public static final String SAVED_REPORT_JOIN_SCHEDULED_REPORT = "scheduledReportJoinSavedReport";
public static final String SAVED_REPORT_JOIN_SCHEDULED_REPORT = "scheduledReportJoinSavedReport";
public static final String SHARED_SAVED_REPORT_JOIN_SAVED_REPORT = "sharedSavedReportJoinSavedReport";
public static final String SCHEDULED_REPORT_VALUES_WIDGET = "scheduledReportValuesWidget";
public static final String RENDER_REPORT_PROCESS_VALUES_WIDGET = "renderReportProcessValuesWidget";
@ -111,6 +116,16 @@ public class SavedReportsMetaDataProvider
{
instance.addPossibleValueSource(new TimeZonePossibleValueSourceMetaDataProvider().produce());
}
/////////////////////////////////////
// todo - param to enable sharing? //
/////////////////////////////////////
instance.addTable(defineSharedSavedReportTable(recordTablesBackendName, backendDetailEnricher));
instance.addJoin(defineSharedSavedReportJoinSavedReport());
if(instance.getPossibleValueSource(ShareScopePossibleValueMetaDataProducer.NAME) == null)
{
instance.addPossibleValueSource(new ShareScopePossibleValueMetaDataProducer().produce(new QInstance()));
}
}
@ -173,6 +188,21 @@ public class SavedReportsMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
private QJoinMetaData defineSharedSavedReportJoinSavedReport()
{
return (new QJoinMetaData()
.withName(SHARED_SAVED_REPORT_JOIN_SAVED_REPORT)
.withLeftTable(SharedSavedReport.TABLE_NAME)
.withRightTable(SavedReport.TABLE_NAME)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("savedReportId", "id")));
}
/*******************************************************************************
**
*******************************************************************************/
@ -255,6 +285,44 @@ public class SavedReportsMetaDataProvider
table.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
table.withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(SharedSavedReport.TABLE_NAME)
.withAssetIdFieldName("savedReportId")
.withScopeFieldName("scope")
.withThisTableOwnerIdFieldName("userId")
.withAudienceType(new ShareableAudienceType().withName("user").withFieldName("userId")));
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineSharedSavedReportTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(SharedSavedReport.TABLE_NAME)
.withLabel("Shared Report")
.withIcon(new QIcon().withName("share"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("savedReportId")
.withBackendName(backendName)
.withUniqueKey(new UniqueKey("savedReportId", "userId"))
.withPrimaryKeyField("id")
.withFieldsFromEntity(SharedSavedReport.class)
// todo - security key
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "savedReportId", "userId")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("scope")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);

View File

@ -0,0 +1,267 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.savedreports;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer;
/*******************************************************************************
** Entity bean for the shared saved report table
*******************************************************************************/
public class SharedSavedReport extends QRecordEntity
{
public static final String TABLE_NAME = "sharedSavedReport";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = SavedReport.TABLE_NAME, label = "Report")
private Integer savedReportId;
@QField(label = "User")
private String userId;
@QField(possibleValueSourceName = ShareScopePossibleValueMetaDataProducer.NAME)
private String scope;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SharedSavedReport()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SharedSavedReport(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public SharedSavedReport withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public SharedSavedReport withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public SharedSavedReport withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for savedReportId
*******************************************************************************/
public Integer getSavedReportId()
{
return (this.savedReportId);
}
/*******************************************************************************
** Setter for savedReportId
*******************************************************************************/
public void setSavedReportId(Integer savedReportId)
{
this.savedReportId = savedReportId;
}
/*******************************************************************************
** Fluent setter for savedReportId
*******************************************************************************/
public SharedSavedReport withSavedReportId(Integer savedReportId)
{
this.savedReportId = savedReportId;
return (this);
}
/*******************************************************************************
** Getter for userId
*******************************************************************************/
public String getUserId()
{
return (this.userId);
}
/*******************************************************************************
** Setter for userId
*******************************************************************************/
public void setUserId(String userId)
{
this.userId = userId;
}
/*******************************************************************************
** Fluent setter for userId
*******************************************************************************/
public SharedSavedReport withUserId(String userId)
{
this.userId = userId;
return (this);
}
/*******************************************************************************
** Getter for scope
*******************************************************************************/
public String getScope()
{
return (this.scope);
}
/*******************************************************************************
** Setter for scope
*******************************************************************************/
public void setScope(String scope)
{
this.scope = scope;
}
/*******************************************************************************
** Fluent setter for scope
*******************************************************************************/
public SharedSavedReport withScope(String scope)
{
this.scope = scope;
return (this);
}
}

View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.statusmessages;
/*******************************************************************************
** specialization of bad-input status message, specifically for the case of
** a duplicated key (e.g., unique-key validation error)
*******************************************************************************/
public class DuplicateKeyBadInputStatusMessage extends BadInputStatusMessage
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DuplicateKeyBadInputStatusMessage(String message)
{
super(message);
}
}

View File

@ -52,4 +52,14 @@ public interface QAuthenticationModuleCustomizerInterface
//////////
}
/*******************************************************************************
**
*******************************************************************************/
default void finalCustomizeSession(QInstance qInstance, QSession qSession)
{
//////////
// noop //
//////////
}
}

View File

@ -230,6 +230,14 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
}
}
//////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired //
//////////////////////////////////////////////////////////////
if(getCustomizer() != null)
{
getCustomizer().finalCustomizeSession(qInstance, qSession);
}
return (qSession);
}
else if(CollectionUtils.containsKeyWithNonNullValue(context, BASIC_AUTH_KEY))
@ -284,7 +292,17 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// try to build session to see if still valid //
// then call method to check more session validity //
/////////////////////////////////////////////////////
return buildAndValidateSession(qInstance, accessToken);
QSession qSession = buildAndValidateSession(qInstance, accessToken);
//////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired //
//////////////////////////////////////////////////////////////
if(getCustomizer() != null)
{
getCustomizer().finalCustomizeSession(qInstance, qSession);
}
return (qSession);
}
catch(QAuthenticationException qae)
{

View File

@ -177,6 +177,14 @@ public class MemoryRecordStore
for(QRecord qRecord : tableData)
{
if(qRecord.getTableName() == null)
{
///////////////////////////////////////////////////////////////////////////////////////////
// internally, doesRecordMatch likes to know table names on records, so, set if missing. //
///////////////////////////////////////////////////////////////////////////////////////////
qRecord.setTableName(input.getTableName());
}
boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord);
if(recordMatches)
@ -232,16 +240,7 @@ public class MemoryRecordStore
{
QTableMetaData nextTable = qInstance.getTable(queryJoin.getJoinTable());
Collection<QRecord> nextTableRecords = getTableData(nextTable).values();
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
{
QJoinMetaData found = joinsContext.findJoinMetaData(qInstance, input.getTableName(), queryJoin.getJoinTable());
if(found == null)
{
throw (new RuntimeException("Could not find a join between tables [" + input.getTableName() + "][" + queryJoin.getJoinTable() + "]"));
}
return (found);
});
QJoinMetaData joinMetaData = Objects.requireNonNull(queryJoin.getJoinMetaData(), () -> "Could not find a join between tables [" + leftTable + "][" + queryJoin.getJoinTable() + "]");
List<QRecord> nextLevelProduct = new ArrayList<>();
for(QRecord productRecord : crossProduct)

View File

@ -78,14 +78,16 @@ public class BackendQueryFilterUtils
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// 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... //
// field names in the record are fully qualified - OR - the table name portion of the field name //
// matches the record's field name, then just use the field-name portion... //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
Map<String, Serializable> values = qRecord.getValues();
if(values.keySet().stream().noneMatch(n -> n.contains(".")))
if(values.keySet().stream().noneMatch(n -> n.contains(".")) || parts[0].equals(qRecord.getTableName()))
{
value = qRecord.getValue(fieldName.substring(fieldName.indexOf(".") + 1));
value = qRecord.getValue(parts[1]);
}
}
}
@ -177,6 +179,8 @@ public class BackendQueryFilterUtils
boolean between = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
yield !between;
}
case TRUE -> true;
case FALSE -> false;
};
return criterionMatches;
}
@ -203,12 +207,13 @@ public class BackendQueryFilterUtils
** operator, update the accumulator, and if we can then short-circuit remaining
** operations, return a true or false. Returning null means to keep going.
*******************************************************************************/
private static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator)
static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator)
{
boolean accumulatorValue = accumulator.getPlain();
if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND))
{
accumulatorValue &= newValue;
accumulator.set(accumulatorValue);
if(!accumulatorValue)
{
return (false);
@ -217,6 +222,7 @@ public class BackendQueryFilterUtils
else
{
accumulatorValue |= newValue;
accumulator.set(accumulatorValue);
if(accumulatorValue)
{
return (true);

View File

@ -112,4 +112,17 @@ public abstract class AbstractExtractStep implements BackendStep
this.limit = limit;
}
/*******************************************************************************
** Create the record pipe to be used for this process step.
**
** Here in case a subclass needs a different type of pipe - for example, a
** DistinctFilteringRecordPipe.
*******************************************************************************/
public RecordPipe createRecordPipe(RunBackendStepInput runBackendStepInput, Integer overrideCapacity)
{
return (new RecordPipe(overrideCapacity));
}
}

View File

@ -24,8 +24,14 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.reporting.DistinctFilteringRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -36,9 +42,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -105,6 +115,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
QueryInput queryInput = new QueryInput();
queryInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
queryInput.setFilter(filterClone);
getQueryJoinsForOrderByIfNeeded(queryFilter).forEach(queryJoin -> queryInput.withQueryJoin(queryJoin));
queryInput.setSelectDistinct(true);
queryInput.setRecordPipe(getRecordPipe());
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
@ -139,6 +150,45 @@ public class ExtractViaQueryStep extends AbstractExtractStep
/*******************************************************************************
** If the queryFilter has order-by fields from a joinTable, then create QueryJoins
** for each such table - marked as LEFT, and select=true.
**
** This is under the rationale that, the filter would have come from the frontend,
** which would be doing outer-join semantics for a column being shown (but not filtered by).
** If the table IS filtered by, it's still OK to do a LEFT, as we'll only get rows
** that match.
**
** Also, they are being select=true'ed so that the DISTINCT clause works (since
** process queries always try to be DISTINCT).
*******************************************************************************/
private List<QueryJoin> getQueryJoinsForOrderByIfNeeded(QQueryFilter queryFilter)
{
if(queryFilter == null)
{
return (Collections.emptyList());
}
List<QueryJoin> rs = new ArrayList<>();
Set<String> addedTables = new HashSet<>();
for(QFilterOrderBy filterOrderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
{
if(filterOrderBy.getFieldName().contains("."))
{
String tableName = filterOrderBy.getFieldName().split("\\.")[0];
if(!addedTables.contains(tableName))
{
rs.add(new QueryJoin(tableName).withType(QueryJoin.Type.LEFT).withSelect(true));
}
addedTables.add(tableName);
}
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
@ -148,6 +198,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
CountInput countInput = new CountInput();
countInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
countInput.setFilter(queryFilter);
getQueryJoinsForOrderByIfNeeded(queryFilter).forEach(queryJoin -> countInput.withQueryJoin(queryJoin));
countInput.setIncludeDistinctCount(true);
CountOutput countOutput = new CountAction().execute(countInput);
Integer count = countOutput.getDistinctCount();
@ -247,4 +298,33 @@ public class ExtractViaQueryStep extends AbstractExtractStep
}
}
/*******************************************************************************
** Create the record pipe to be used for this process step.
**
*******************************************************************************/
@Override
public RecordPipe createRecordPipe(RunBackendStepInput runBackendStepInput, Integer overrideCapacity)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the filter has order-bys from a join-table, then we have to include that join-table in the SELECT clause, //
// which means we need to do distinct "manually", e.g., via a DistinctFilteringRecordPipe //
// todo - really, wouldn't this only be if it's a many-join? but that's not completely trivial to detect, given join-chains... //
// as it is, we may end up using DistinctPipe in some cases that we need it - which isn't an error, just slightly sub-optimal. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> queryJoinsForOrderByIfNeeded = getQueryJoinsForOrderByIfNeeded(queryFilter);
boolean needDistinctPipe = CollectionUtils.nullSafeHasContents(queryJoinsForOrderByIfNeeded);
if(needDistinctPipe)
{
String sourceTableName = runBackendStepInput.getValueString(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE);
QTableMetaData sourceTable = runBackendStepInput.getInstance().getTable(sourceTableName);
return (new DistinctFilteringRecordPipe(new UniqueKey(sourceTable.getPrimaryKeyField()), overrideCapacity));
}
else
{
return (super.createRecordPipe(runBackendStepInput, overrideCapacity));
}
}
}

View File

@ -72,15 +72,6 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
//////////////////////////////
// set up the extract steps //
//////////////////////////////
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
RecordPipe recordPipe = new RecordPipe();
extractStep.setLimit(limit);
extractStep.setRecordPipe(recordPipe);
extractStep.preRun(runBackendStepInput, runBackendStepOutput);
/////////////////////////////////////////////////////////////////
// if we're running inside an automation, then skip this step. //
/////////////////////////////////////////////////////////////////
@ -90,6 +81,19 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
/////////////////////////////
// set up the extract step //
/////////////////////////////
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
extractStep.setLimit(limit);
extractStep.preRun(runBackendStepInput, runBackendStepOutput);
//////////////////////////////////////////
// set up a record pipe for the process //
//////////////////////////////////////////
RecordPipe recordPipe = extractStep.createRecordPipe(runBackendStepInput, null);
extractStep.setRecordPipe(recordPipe);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if skipping frontend steps, skip this action - //
// but, if inside an (ideally, only async) API call, at least do the count, so status calls can get x of y status //

View File

@ -77,14 +77,16 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
moveReviewStepAfterValidateStep(runBackendStepOutput);
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
runBackendStepInput.getAsyncJobCallback().updateStatus("Validating Records");
//////////////////////////////////////////////////////////
// basically repeat the preview step, but with no limit //
//////////////////////////////////////////////////////////
runBackendStepInput.getAsyncJobCallback().updateStatus("Validating Records");
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
//////////////////////////////////////////////////////////////////////
// let the transform step override the capacity for the record pipe //
//////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,130 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** DeleteSharedRecord: {tableName; recordId; shareId;}
*******************************************************************************/
public class DeleteSharedRecordProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
{
public static final String NAME = "deleteSharedRecord";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("share"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect
.withStepList(List.of(
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(getClass()))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this...
.withField(new QFieldMetaData("recordId", QFieldType.STRING))
.withField(new QFieldMetaData("shareId", QFieldType.INTEGER))
)
));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
String tableName = runBackendStepInput.getValueString("tableName");
String recordIdString = runBackendStepInput.getValueString("recordId");
Integer shareId = runBackendStepInput.getValueInteger("shareId");
Objects.requireNonNull(tableName, "Missing required input: tableName");
Objects.requireNonNull(recordIdString, "Missing required input: recordId");
Objects.requireNonNull(shareId, "Missing required input: shareId");
try
{
SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString);
ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData();
QRecord assetRecord = assetTableAndRecord.record();
SharedRecordProcessUtils.assertRecordOwnership(shareableTableMetaData, assetRecord, "delete shares of");
///////////////////
// do the delete //
///////////////////
DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(shareableTableMetaData.getSharedRecordTableName()).withPrimaryKeys(List.of(shareId)));
//////////////////////
// check for errors //
//////////////////////
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
{
throw (new QException("Error deleting shared record: " + deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0).getMessage()));
}
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error deleting shared record", e));
}
}
}

View File

@ -0,0 +1,140 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** EditSharedRecord: {tableName; recordId; shareId; scopeId;}
*******************************************************************************/
public class EditSharedRecordProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
{
public static final String NAME = "editSharedRecord";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("share"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect
.withStepList(List.of(
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(getClass()))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this...
.withField(new QFieldMetaData("recordId", QFieldType.STRING))
.withField(new QFieldMetaData("scopeId", QFieldType.STRING).withPossibleValueSourceName(ShareScopePossibleValueMetaDataProducer.NAME))
.withField(new QFieldMetaData("shareId", QFieldType.INTEGER))
)
));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
String tableName = runBackendStepInput.getValueString("tableName");
String recordIdString = runBackendStepInput.getValueString("recordId");
String scopeId = runBackendStepInput.getValueString("scopeId");
Integer shareId = runBackendStepInput.getValueInteger("shareId");
Objects.requireNonNull(tableName, "Missing required input: tableName");
Objects.requireNonNull(recordIdString, "Missing required input: recordId");
Objects.requireNonNull(scopeId, "Missing required input: scopeId");
Objects.requireNonNull(shareId, "Missing required input: shareId");
try
{
SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString);
ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData();
QRecord assetRecord = assetTableAndRecord.record();
QTableMetaData shareTable = QContext.getQInstance().getTable(shareableTableMetaData.getSharedRecordTableName());
SharedRecordProcessUtils.assertRecordOwnership(shareableTableMetaData, assetRecord, "edit shares of");
ShareScope shareScope = SharedRecordProcessUtils.validateScopeId(scopeId);
///////////////////
// do the insert //
///////////////////
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(shareableTableMetaData.getSharedRecordTableName()).withRecord(new QRecord()
.withValue(shareTable.getPrimaryKeyField(), shareId)
.withValue(shareableTableMetaData.getScopeFieldName(), shareScope.getPossibleValueId())));
//////////////////////
// check for errors //
//////////////////////
if(CollectionUtils.nullSafeHasContents(updateOutput.getRecords().get(0).getErrors()))
{
throw (new QException("Error editing shared record: " + updateOutput.getRecords().get(0).getErrors().get(0).getMessage()));
}
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error editing shared record", e));
}
}
}

View File

@ -0,0 +1,249 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableAudienceType;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** GetSharedRecords: {tableName; recordId;} => [{id; audienceType; audienceId; audienceLabel; scopeId}]
*******************************************************************************/
public class GetSharedRecordsProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
{
public static final String NAME = "getSharedRecords";
private static final QLogger LOG = QLogger.getLogger(GetSharedRecordsProcess.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("share"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect
.withStepList(List.of(
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(getClass()))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this...
.withField(new QFieldMetaData("recordId", QFieldType.STRING))
)
));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
String tableName = runBackendStepInput.getValueString("tableName");
String recordIdString = runBackendStepInput.getValueString("recordId");
Objects.requireNonNull(tableName, "Missing required input: tableName");
Objects.requireNonNull(recordIdString, "Missing required input: recordId");
try
{
SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString);
ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData();
QTableMetaData shareTable = QContext.getQInstance().getTable(shareableTableMetaData.getSharedRecordTableName());
Serializable recordId = assetTableAndRecord.recordId();
/////////////////////////////////////
// query for shares on this record //
/////////////////////////////////////
QueryInput queryInput = new QueryInput(shareTable.getName());
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria(shareableTableMetaData.getAssetIdFieldName(), QCriteriaOperator.EQUALS, recordId))
.withOrderBy(new QFilterOrderBy(shareTable.getPrimaryKeyField()))
);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// iterate results, building QRecords to output - note - we'll need to collect ids, then look them up in audience-source tables //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<QRecord> resultList = new ArrayList<>();
ListingHash<String, Serializable> audienceIds = new ListingHash<>();
for(QRecord record : queryOutput.getRecords())
{
QRecord outputRecord = new QRecord();
outputRecord.setValue("shareId", record.getValue(shareTable.getPrimaryKeyField()));
outputRecord.setValue("scopeId", record.getValue(shareableTableMetaData.getScopeFieldName()));
boolean foundAudienceType = false;
for(ShareableAudienceType audienceType : shareableTableMetaData.getAudienceTypes().values())
{
Serializable audienceId = record.getValue(audienceType.getFieldName());
if(audienceId != null)
{
outputRecord.setValue("audienceType", audienceType.getName());
outputRecord.setValue("audienceId", audienceId);
audienceIds.add(audienceType.getName(), audienceId);
foundAudienceType = true;
break;
}
}
if(!foundAudienceType)
{
LOG.warn("Failed to find what audience type to use for a shared record",
logPair("sharedTableName", shareTable.getName()),
logPair("id", record.getValue(shareTable.getPrimaryKeyField())),
logPair("recordId", record.getValue(shareableTableMetaData.getAssetIdFieldName())));
continue;
}
resultList.add(outputRecord);
}
/////////////////////////////////
// look up the audience labels //
/////////////////////////////////
Map<String, Map<Serializable, String>> audienceLabels = new HashMap<>();
Set<String> audienceTypesWithLabels = new HashSet<>();
for(Map.Entry<String, List<Serializable>> entry : audienceIds.entrySet())
{
String audienceType = entry.getKey();
List<Serializable> ids = entry.getValue();
if(CollectionUtils.nullSafeHasContents(ids))
{
ShareableAudienceType shareableAudienceType = shareableTableMetaData.getAudienceTypes().get(audienceType);
if(StringUtils.hasContent(shareableAudienceType.getSourceTableName()))
{
audienceTypesWithLabels.add(audienceType);
String keyField = shareableAudienceType.getSourceTableKeyFieldName();
if(!StringUtils.hasContent(keyField))
{
keyField = QContext.getQInstance().getTable(shareableAudienceType.getSourceTableName()).getPrimaryKeyField();
}
QueryInput audienceQueryInput = new QueryInput(shareableAudienceType.getSourceTableName());
audienceQueryInput.setFilter(new QQueryFilter(new QFilterCriteria(keyField, QCriteriaOperator.IN, ids)));
audienceQueryInput.setShouldGenerateDisplayValues(true); // to get record labels
QueryOutput audienceQueryOutput = new QueryAction().execute(audienceQueryInput);
for(QRecord audienceRecord : audienceQueryOutput.getRecords())
{
audienceLabels.computeIfAbsent(audienceType, k -> new HashMap<>());
audienceLabels.get(audienceType).put(audienceRecord.getValue(keyField), audienceRecord.getRecordLabel());
}
}
}
}
////////////////////////////////////////////
// put those labels on the output records //
////////////////////////////////////////////
for(QRecord outputRecord : resultList)
{
String audienceType = outputRecord.getValueString("audienceType");
Map<Serializable, String> typeLabels = audienceLabels.getOrDefault(audienceType, Collections.emptyMap());
Serializable audienceId = outputRecord.getValue("audienceId");
String label = typeLabels.get(audienceId);
if(StringUtils.hasContent(label))
{
outputRecord.setValue("audienceLabel", label);
}
else
{
if(audienceTypesWithLabels.contains(audienceType))
{
outputRecord.setValue("audienceLabel", "Unknown " + audienceType + " (id=" + audienceId + ")");
}
else
{
outputRecord.setValue("audienceLabel", audienceType + " " + audienceId);
}
}
}
////////////////////////////
// sort results by labels //
////////////////////////////
resultList.sort(Comparator.comparing(r -> r.getValueString("audienceLabel")));
runBackendStepOutput.addValue("resultList", resultList);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting shared records.", e));
}
}
}

View File

@ -0,0 +1,208 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
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.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableAudienceType;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.DuplicateKeyBadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** InsertSharedRecord: {tableName; recordId; audienceType; audienceId; scopeId;}
*******************************************************************************/
public class InsertSharedRecordProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
{
public static final String NAME = "insertSharedRecord";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("share"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect
.withStepList(List.of(
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(getClass()))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this...
.withField(new QFieldMetaData("recordId", QFieldType.STRING))
.withField(new QFieldMetaData("audienceType", QFieldType.STRING)) // todo take a PVS name as param?
.withField(new QFieldMetaData("audienceId", QFieldType.STRING))
.withField(new QFieldMetaData("scopeId", QFieldType.STRING).withPossibleValueSourceName(ShareScopePossibleValueMetaDataProducer.NAME))
)
));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
String tableName = runBackendStepInput.getValueString("tableName");
String recordIdString = runBackendStepInput.getValueString("recordId");
String audienceType = runBackendStepInput.getValueString("audienceType");
String audienceIdString = runBackendStepInput.getValueString("audienceId");
String scopeId = runBackendStepInput.getValueString("scopeId");
Objects.requireNonNull(tableName, "Missing required input: tableName");
Objects.requireNonNull(recordIdString, "Missing required input: recordId");
Objects.requireNonNull(audienceType, "Missing required input: audienceType");
Objects.requireNonNull(audienceIdString, "Missing required input: audienceId");
Objects.requireNonNull(scopeId, "Missing required input: scopeId");
String assetTableLabel = tableName;
try
{
SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString);
ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData();
QRecord assetRecord = assetTableAndRecord.record();
Serializable recordId = assetTableAndRecord.recordId();
assetTableLabel = assetTableAndRecord.table().getLabel();
SharedRecordProcessUtils.assertRecordOwnership(shareableTableMetaData, assetRecord, "share");
////////////////////////////////
// validate the audience type //
////////////////////////////////
ShareableAudienceType shareableAudienceType = shareableTableMetaData.getAudienceTypes().get(audienceType);
if(shareableAudienceType == null)
{
throw (new QException("[" + audienceType + "] is not a recognized audience type for sharing records from the " + tableName + " table. Allowed values are: " + shareableTableMetaData.getAudienceTypes().keySet()));
}
///////////////////////////////////////////////////////////////////////////////////////////////
// if we know the audience source-table, then fetch & validate security-wise the audience id //
///////////////////////////////////////////////////////////////////////////////////////////////
Serializable audienceId = audienceIdString;
String audienceTableLabel = "audience";
if(StringUtils.hasContent(shareableAudienceType.getSourceTableName()))
{
QTableMetaData audienceTable = QContext.getQInstance().getTable(shareableAudienceType.getSourceTableName());
audienceTableLabel = audienceTable.getLabel();
GetInput getInput = new GetInput(audienceTable.getName());
if(StringUtils.hasContent(shareableAudienceType.getSourceTableKeyFieldName()))
{
audienceId = ValueUtils.getValueAsFieldType(audienceTable.getField(shareableAudienceType.getSourceTableKeyFieldName()).getType(), audienceIdString);
getInput.withUniqueKey(Map.of(shareableAudienceType.getSourceTableKeyFieldName(), audienceId));
}
else
{
audienceId = ValueUtils.getValueAsFieldType(audienceTable.getField(audienceTable.getPrimaryKeyField()).getType(), audienceIdString);
getInput.withPrimaryKey(audienceId);
}
QRecord audienceRecord = new GetAction().executeForRecord(getInput);
if(audienceRecord == null)
{
throw (new QException("A record could not be found for audience type " + audienceType + ", audience id: " + audienceIdString));
}
}
////////////////////////////////
// validate input share scope //
////////////////////////////////
ShareScope shareScope = SharedRecordProcessUtils.validateScopeId(scopeId);
///////////////////
// do the insert //
///////////////////
InsertOutput insertOutput = new InsertAction().execute(new InsertInput(shareableTableMetaData.getSharedRecordTableName()).withRecord(new QRecord()
.withValue(shareableTableMetaData.getAssetIdFieldName(), recordId)
.withValue(shareableTableMetaData.getScopeFieldName(), shareScope.getPossibleValueId())
.withValue(shareableAudienceType.getFieldName(), audienceId)));
//////////////////////
// check for errors //
//////////////////////
if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors()))
{
QErrorMessage errorMessage = insertOutput.getRecords().get(0).getErrors().get(0);
if(errorMessage instanceof DuplicateKeyBadInputStatusMessage)
{
throw (new QUserFacingException("This " + assetTableLabel + " has already been shared with this " + audienceTableLabel));
}
else if(errorMessage instanceof BadInputStatusMessage)
{
throw (new QUserFacingException(errorMessage.getMessage()));
}
throw (new QException("Error sharing " + assetTableLabel + ": " + errorMessage.getMessage()));
}
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error sharing " + assetTableLabel, e));
}
}
}

View File

@ -0,0 +1,81 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
** for a shared record, what scope of access is given.
*******************************************************************************/
public enum ShareScope implements PossibleValueEnum<String>
{
READ_ONLY("Read Only"),
READ_WRITE("Read and Edit");
private final String label;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
ShareScope(String label)
{
this.label = label;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueId()
{
return name();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return label;
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return label;
}
}

View File

@ -0,0 +1,126 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class SharedRecordProcessUtils
{
/*******************************************************************************
**
*******************************************************************************/
record AssetTableAndRecord(QTableMetaData table, ShareableTableMetaData shareableTableMetaData, QRecord record, Serializable recordId) {}
/*******************************************************************************
**
*******************************************************************************/
static AssetTableAndRecord getAssetTableAndRecord(String tableName, String recordIdString) throws QException
{
//////////////////////////////
// validate the asset table //
//////////////////////////////
QTableMetaData assetTable = QContext.getQInstance().getTable(tableName);
if(assetTable == null)
{
throw (new QException("The specified tableName, " + tableName + ", was not found."));
}
ShareableTableMetaData shareableTableMetaData = assetTable.getShareableTableMetaData();
if(shareableTableMetaData == null)
{
throw (new QException("The specified tableName, " + tableName + ", is not shareable."));
}
//////////////////////////////
// look up the asset record //
//////////////////////////////
Serializable recordId = ValueUtils.getValueAsFieldType(assetTable.getField(assetTable.getPrimaryKeyField()).getType(), recordIdString);
QRecord assetRecord = new GetAction().executeForRecord(new GetInput(tableName).withPrimaryKey(recordId));
if(assetRecord == null)
{
throw (new QException("A record could not be found in table, " + tableName + ", with primary key: " + recordIdString));
}
return new AssetTableAndRecord(assetTable, shareableTableMetaData, assetRecord, recordId);
}
/*******************************************************************************
**
*******************************************************************************/
static void assertRecordOwnership(ShareableTableMetaData shareableTableMetaData, QRecord assetRecord, String verbClause) throws QException
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the shareable meta-data says this-table's owner id, then validate that the current user own the record //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(shareableTableMetaData.getThisTableOwnerIdFieldName()))
{
Serializable ownerId = assetRecord.getValue(shareableTableMetaData.getThisTableOwnerIdFieldName());
if(!Objects.equals(ownerId, QContext.getQSession().getUser().getIdReference()))
{
throw (new QException("You are not the owner of this record, so you may not " + verbClause + " it."));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
static ShareScope validateScopeId(String scopeId) throws QException
{
////////////////////////////////
// validate input share scope //
////////////////////////////////
ShareScope shareScope = null;
try
{
shareScope = ShareScope.valueOf(scopeId);
return (shareScope);
}
catch(IllegalArgumentException e)
{
throw (new QException("[" + shareScope + "] is not a recognized value for shareScope. Allowed values are: " + Arrays.toString(ShareScope.values())));
}
}
}

View File

@ -0,0 +1,61 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.processes.implementations.sharing;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class SharingMetaDataProvider
{
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, Consumer<QProcessMetaData> processEnricher) throws QException
{
List<QProcessMetaData> processes = new ArrayList<>();
processes.add(new GetSharedRecordsProcess().produce(instance));
processes.add(new InsertSharedRecordProcess().produce(instance));
processes.add(new EditSharedRecordProcess().produce(instance));
processes.add(new DeleteSharedRecordProcess().produce(instance));
for(QProcessMetaData process : processes)
{
if(processEnricher != null)
{
processEnricher.accept(process);
}
instance.addProcess(process);
}
}
}