Compare commits

..

17 Commits

Author SHA1 Message Date
d44ab8582f Merge branch 'feature/CE-1107-add-day-by-day-views-for-the' into integration/sprint-40 2024-04-17 22:35:31 -05:00
b729d57661 Merged feature/CE-881-create-basic-saved-reports into integration/sprint-40 2024-04-16 20:40:24 -05:00
88e697e698 Merged feature/more-state-provider-cleanup into integration/sprint-40 2024-04-16 12:21:05 -05:00
1f3702d5fa Turn off logging (sorry for filling dev...) 2024-04-16 12:20:47 -05:00
eea67de539 do not also run tests on integration branches 2024-04-16 07:56:09 -05:00
c76ad7a5fe try to make deploy always run on integration branches 2024-04-16 07:55:36 -05:00
bb0315eef5 Merged feature/CE-1123-expose-access-lo-data-in into integration/sprint-40 2024-04-15 19:28:04 -05:00
1106f6fe63 Merge branch 'feature/CE-1107-add-day-by-day-views-for-the' into integration/sprint-40 2024-04-15 12:01:09 -05:00
997c5572e2 Merged feature/CE-881-create-basic-saved-reports into integration/sprint-40 2024-04-15 08:50:46 -05:00
b9677343a3 Merged feature/CE-978-crashing-nodes into integration/sprint-40 2024-04-08 14:31:50 -05:00
34c67e9d53 Merged feature/CE-881-create-basic-saved-reports into integration/sprint-40 2024-04-07 17:25:12 -05:00
0f867c2001 Merged feature/more-state-provider-cleanup into integration/sprint-40 2024-04-07 10:42:52 -05:00
266163be6b Merged feature/CE-881-create-basic-saved-reports into integration/sprint-40 2024-04-05 16:17:29 -05:00
c40e5d813a Merged feature/CE-881-create-basic-saved-reports into integration/sprint-40 2024-04-04 12:14:49 -05:00
9298ae2bbf Merged feature/CE-978-crashing-nodes into integration/sprint-40 2024-04-03 16:56:12 -05:00
3b3bbcb91b Merged feature/CE-881-create-basic-saved-reports into integration/sprint-40 2024-04-03 16:51:02 -05:00
e77701ce15 Merged hotfix/large-result-query-hint-exports into integration/sprint-40 2024-04-03 16:50:43 -05:00
121 changed files with 1699 additions and 9775 deletions

View File

@ -245,7 +245,8 @@ echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
if which xpath > /dev/null 2>&1; then
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
@ -254,7 +255,8 @@ else
echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
fi
if which html2text > /dev/null 2>&1; then
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Untested classes, per Jacoco:"
echo "-----------------------------"
for i in target/site/jacoco/*/index.html; do

View File

@ -54,9 +54,9 @@ public class NonPersistedAsyncJobCallback extends AsyncJobCallback
@Override
protected void storeUpdatedStatus()
{
///////////////////////////////////////////////////////////////////////////////////////
// noop - cf. base class, which writes to persistence here (our point is, we do not) //
///////////////////////////////////////////////////////////////////////////////////////
//////////
// noop //
//////////
}
}

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 -> Objects.requireNonNullElse(table.getFields().get(fieldName).getLabel(), fieldName)))
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
.toList();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());

View File

@ -1,146 +0,0 @@
/*
* 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,9 +190,6 @@ 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("."))
@ -201,7 +198,27 @@ public class ExportAction
String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName))
{
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
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)));
}
addedJoinNames.add(joinTableName);
}
}

View File

@ -136,8 +136,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
*******************************************************************************/
public ReportOutput execute(ReportInput reportInput) throws QException
{
ReportOutput reportOutput = new ReportOutput();
QReportMetaData report = getReportMetaData(reportInput);
ReportOutput reportOutput = new ReportOutput();
QReportMetaData report = getReportMetaData(reportInput);
this.views = report.getViews();
this.dataSources = report.getDataSources();
@ -398,7 +398,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
RunBackendStepInput finalTransformStepInput = transformStepInput;
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
AtomicInteger consumedCount = new AtomicInteger(0);
/////////////////////////////////////////////////////////////////
@ -704,24 +704,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : record.getValues().keySet())
{
QFieldMetaData field = null;
try
{
//////////////////////////////////////////////////////
// todo - memoize this, if we ever need to optimize //
//////////////////////////////////////////////////////
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
field = fieldAndJoinTable.field();
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////
// for non-real-fields... let's skip for now - but maybe treat as string in future? //
//////////////////////////////////////////////////////////////////////////////////////
LOG.debug("Couldn't find field in table qInstance - won't compute aggregates for it", logPair("fieldName", fieldName));
continue;
}
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
QFieldMetaData field = fieldAndJoinTable.field();
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
@SuppressWarnings("unchecked")

View File

@ -24,7 +24,6 @@ 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;
@ -44,9 +43,8 @@ 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 = DEFAULT_CAPACITY;
private int capacity = 1_000;
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(capacity);
private boolean isTerminated = false;
@ -72,13 +70,11 @@ 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 = Objects.requireNonNullElse(overrideCapacity, DEFAULT_CAPACITY);
queue = new ArrayBlockingQueue<>(this.capacity);
this.capacity = overrideCapacity;
queue = new ArrayBlockingQueue<>(overrideCapacity);
}

View File

@ -211,7 +211,6 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
/////////////////////////////////////////////////////////////////////////////////////////////
// open up a zipOutputStream around the output stream that the report is to be written to. //
// note: this stream is closed in the finish method - see more comments there. //
/////////////////////////////////////////////////////////////////////////////////////////////
this.zipOutputStream = new ZipOutputStream(this.outputStream);
@ -330,6 +329,10 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{
int columnLabelColumnIndex = getColumnIndex(dataView.getColumns(), value.getFieldName());
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - some bug where, if use a group-by field here, then ... it doesn't get used for the grouping. //
// g-sheets does let me do this, so, maybe, download their file and see how it's different? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
String labelPrefix = value.getFunction().name() + " of ";
String label = labelPrefix + QInstanceEnricher.nameToLabel(value.getFieldName());
String valueFormat = null;

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 " + Objects.requireNonNullElse(primaryKeyField.getLabel(), primaryKeyField.getName()) + " = " + primaryKeyValue));
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + primaryKeyField.getLabel() + " = " + primaryKeyValue));
}
}
}

View File

@ -62,7 +62,6 @@ 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;
@ -415,7 +414,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 DuplicateKeyBadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
record.addError(new BadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
foundDupe = true;
break;
}

View File

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

View File

@ -24,7 +24,6 @@ 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;
@ -43,16 +42,13 @@ 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;
@ -85,273 +81,160 @@ public class ValidateRecordSecurityLockHelper
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
{
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())
for(RecordSecurityLock recordSecurityLock : locksToCheck)
{
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()))
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition);
treePosition.remove(treePosition.size() - 1);
i++;
}
//////////////////////////////////////////////////////////////////////////////////
// handle the value being in the table we're inserting/updating (e.g., no join) //
//////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
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;
}
////////////////////////////////
// proceed w/ non-multi locks //
////////////////////////////////
String primaryKeyField = table.getPrimaryKeyField();
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)
{
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
for(QRecord record : records)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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(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;
}
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);
Serializable recordSecurityValue = record.getValue(field.getName());
validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action);
}
}
}
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
{
////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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());
for(String joinName : recordSecurityLock.getJoinNameChain())
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
{
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
////////////////////////////////////////////////////////////////////////////////////////////////
// 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())
{
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()))
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
{
updatingAnyLockJoinFields = true;
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();
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));
}
subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
}
//////////////////////////////////
// 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))
{
////////////////////////////////////////////////////////////////////////////////
// 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);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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(".")))
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
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))
{
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(".")))
{
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
////////////////////////////////////////////////////////////////////////////////
// only add this sub-filter if it's for a list of keys we haven't seen before //
////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(subFilter);
}
for(QRecord inputRecord : inputRecords)
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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())
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
if(CollectionUtils.nullSafeHasContents(recordErrors))
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
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))
{
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(".")))
{
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
}
for(QRecord inputRecord : inputRecords)
{
validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action);
}
}
}
else
{
for(QRecord inputRecord : inputRecords)
else
{
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
for(QRecord inputRecord : inputRecords)
{
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);
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."));
}
}
}
}
@ -363,81 +246,45 @@ 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 Map<Serializable, QRecord> makeUpPrimaryKeysIfNeeded(List<QRecord> records, QTableMetaData table)
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
{
String primaryKeyField = table.getPrimaryKeyField();
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
Integer madeUpPrimaryKey = -1;
for(QRecord record : records)
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
recordSecurityLocks = switch(action)
{
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);
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
};
if(action.equals(Action.UPDATE))
{
////////////////////////////////////////////////////////////////////////////
// todo at some point this seemed right, but now it doesn't - needs work. //
////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////
// // when doing an update, convert all OR's to AND's... //
// ////////////////////////////////////////////////////////
// updateOperators(locksOfType, MultiRecordSecurityLock.BooleanOperator.AND);
}
////////////////////////////////////////
// if there are no locks, just return //
////////////////////////////////////////
if(locksOfType == null || CollectionUtils.nullSafeIsEmpty(locksOfType.getLocks()))
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks))
{
return (null);
}
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())
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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)
{
if(childLock instanceof MultiRecordSecurityLock childMultiLock)
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
updateOperators(childMultiLock, operator);
LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
}
else
{
locksToCheck.add(recordSecurityLock);
}
}
return (locksToCheck);
}
@ -445,7 +292,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{
if(recordSecurityValue == null)
{
@ -455,7 +302,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();
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)));
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
}
}
else
@ -467,305 +314,15 @@ public class ValidateRecordSecurityLockHelper
///////////////////////////////////////////////////////////////////////////////////////////////
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
///////////////////////////////////////////////////////////////////////////////////////////////
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record.")));
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record."));
}
else
{
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
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())));
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 (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,47 +560,20 @@ 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(idField, QCriteriaOperator.IN, page)));
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
queryInput.setTransaction(getTransaction(tableName));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -645,7 +618,7 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////////////////////////////////////////
for(QRecord record : queryOutput.getRecords())
{
Serializable pkeyValue = record.getValue(idField);
Serializable pkeyValue = record.getValue(primaryKeyField);
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
QPossibleValue<?> possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel());

View File

@ -275,19 +275,8 @@ public class SearchPossibleValueSourceAction
queryInput.setFilter(queryFilter);
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();
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
output.setResults(qPossibleValues);

View File

@ -86,7 +86,6 @@ 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;
@ -625,11 +624,6 @@ public class QInstanceValidator
supplementalTableMetaData.validate(qInstance, table, this);
}
if(table.getShareableTableMetaData() != null)
{
table.getShareableTableMetaData().validate(qInstance, table, this);
}
runPlugins(QTableMetaData.class, table, qInstance);
});
}
@ -717,6 +711,7 @@ 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?)"))
@ -724,129 +719,91 @@ public class QInstanceValidator
continue;
}
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
{
validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix);
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
}
else
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()))
{
validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
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))
{
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(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
String[] split = fieldName.split("\\.");
String joinTableName = split[0];
String joinFieldName = split[1];
hasAnyBadJoins = true;
}
}
List<QueryJoin> joins = new ArrayList<>();
String fieldName = recordSecurityLock.getFieldName();
///////////////////////////////////////////////////////////////////////////////////////////////////
// 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)
////////////////////////////////////////////////////////////////////////////////
// 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."))
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
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)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
return;
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;
}
}
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(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
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."))
else
{
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
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,7 +23,6 @@ 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;
@ -140,24 +139,6 @@ 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,7 +22,6 @@
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;
@ -38,17 +37,12 @@ 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;
@ -66,23 +60,11 @@ 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<>();
/////////////////////////////////////////////////////////////////////////////
// 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;
private Level logLevel = Level.OFF;
@ -92,225 +74,54 @@ 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 //
///////////////////////////////////////////////////////////////
ensureAllJoinRecordSecurityLocksAreRepresented(instance);
logFilter("After ensureAllJoinRecordSecurityLocksAreRepresented:", securityFilter);
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
{
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
}
////////////////////////////////////////////////////////////////////////////////////
// if there were any security filters built, then put those into the input filter //
////////////////////////////////////////////////////////////////////////////////////
addSecurityFiltersToInputFilter(filter);
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());
}
}
*/
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
log("", logPair("securityFilter", securityFilter));
log("", logPair("fullFilter", filter));
dumpDebug(false, true);
// log("--- END ------------------------------------------------------------------------");
log("--- END ------------------------------------------------------------------------");
}
/*******************************************************************************
** Update the input filter with any security filters that were built.
*******************************************************************************/
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
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) 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;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// 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): //
// 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, //
@ -318,30 +129,30 @@ public class JoinsContext
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
log("Evaluating recordSecurityLock. Join name chain is of length: " + joinNameChain.size(), logPair("tableNameOrAlias", tableNameOrAlias), logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
QTableMetaData tmpTable = instance.getTable(tableName);
String securityFieldTableAlias = tableNameOrAlias;
String baseTableOrAlias = tableNameOrAlias;
boolean chainIsInner = true;
if(sourceQueryJoin != null && QueryJoin.Type.isOuter(sourceQueryJoin.getType()))
{
chainIsInner = false;
}
QTableMetaData tmpTable = instance.getTable(mainTableName);
for(String joinName : joinNameChain)
{
//////////////////////////////////////////////////////////////////////////////////////////////////
// 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 ->
///////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 ->
{
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
QJoinMetaData joinMetaData = null;
if(queryJoin.getJoinMetaData() != null)
{
joinMetaData = queryJoin.getJoinMetaData();
}
else
{
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
}
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}).toList();
if(CollectionUtils.nullSafeHasContents(matchingQueryJoins))
if(CollectionUtils.nullSafeHasContents(matchingJoins))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
@ -349,40 +160,11 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
log("- skipping join already in the query", logPair("joinName", joinName));
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))
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
{
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
matchedQueryJoin.setType(QueryJoin.Type.INNER);
matchingJoins.get(0).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;
}
@ -390,233 +172,20 @@ public class JoinsContext
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
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);
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
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);
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
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);
}
}
@ -628,9 +197,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, String logPrefix) throws QException
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
{
log(Objects.requireNonNullElse(logPrefix, "") + "Adding query join to context",
log("Adding query join to context",
logPair("reason", reason),
logPair("joinTable", queryJoin.getJoinTable()),
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
@ -639,46 +208,34 @@ 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 all joins in the
** instance, or 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 the main table's
** exposed joins, and using their join paths.
*******************************************************************************/
private void fillInMissingJoinMetaData() throws QException
private void addJoinsFromExposedJoinPaths() 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. //
////////////////////////////////////////////////////////////////////////////////
Set<QueryJoin> processedQueryJoins = new HashSet<>();
boolean addedJoin;
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))
@ -708,7 +265,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());
}
}
@ -718,13 +275,11 @@ 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(baseTableName, queryJoin.getJoinTable(), true);
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
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
@ -738,7 +293,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) //
@ -749,7 +304,6 @@ 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) //
@ -778,22 +332,15 @@ 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;
log("- - - - this is not the last element in the join path, so adding a new query join:");
addQueryJoin(queryJoinToAdd, "forExposedJoin", "- - - - - - ");
dumpDebug(false, false);
this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
}
}
else
{
log("- - - - join doesn't need added to the query");
}
tmpTable = nextTable;
}
@ -814,7 +361,6 @@ public class JoinsContext
}
while(addedJoin);
log("Done adding missing join meta data");
}
@ -824,12 +370,12 @@ public class JoinsContext
*******************************************************************************/
private boolean doesJoinNeedAddedToQuery(String joinName)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all queryJoins already in context - if any have this join's name, then we don't need this join... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin queryJoin : queryJoins)
{
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName) && !(queryJoin instanceof ImplicitQueryJoinForSecurityLock))
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
{
return (false);
}
@ -849,7 +395,6 @@ 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());
@ -894,7 +439,6 @@ public class JoinsContext
String[] parts = fieldName.split("\\.");
if(parts.length != 2)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Mal-formatted field name in query: " + fieldName);
}
@ -905,7 +449,6 @@ 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);
@ -960,17 +503,17 @@ public class JoinsContext
for(String filterTable : filterTables)
{
log("Evaluating filter", logPair("filterTable", filterTable));
log("Evaluating filterTable", logPair("filterTable", filterTable));
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{
log("- table not in query - adding a join for it", logPair("filterTable", filterTable));
log("- table not in query - adding 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)
{
addQueryJoin(queryJoin, "forFilter (join found in instance)", "- - ");
this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
found = true;
break;
}
@ -979,13 +522,9 @@ public class JoinsContext
if(!found)
{
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
addQueryJoin(queryJoin, "forFilter (join not found in instance)", "- - ");
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
}
}
else
{
log("- table is already in query - not adding any joins", logPair("filterTable", filterTable));
}
}
}
@ -1027,11 +566,6 @@ 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);
@ -1058,7 +592,7 @@ public class JoinsContext
/*******************************************************************************
**
*******************************************************************************/
public QJoinMetaData findJoinMetaData(String baseTableName, String joinTableName, boolean useExposedJoins)
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
{
List<QJoinMetaData> matches = new ArrayList<>();
if(baseTableName != null)
@ -1110,29 +644,7 @@ public class JoinsContext
}
else if(matches.size() > 1)
{
////////////////////////////////////////////////////////////////////////////////
// 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."));
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
}
return (null);
@ -1157,79 +669,4 @@ 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,7 +49,5 @@ public enum QCriteriaOperator
IS_BLANK,
IS_NOT_BLANK,
BETWEEN,
NOT_BETWEEN,
TRUE,
FALSE
NOT_BETWEEN
}

View File

@ -306,11 +306,6 @@ 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 != null && subFilter.hasAnyCriteria())
if(subFilter.hasAnyCriteria())
{
return (true);
}
@ -361,44 +361,23 @@ public class QQueryFilter implements Serializable, Cloneable
StringBuilder rs = new StringBuilder("(");
try
{
int criteriaIndex = 0;
for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
{
if(criteriaIndex > 0)
{
rs.append(" ").append(getBooleanOperator()).append(" ");
}
rs.append(criterion);
criteriaIndex++;
rs.append(criterion).append(" ").append(getBooleanOperator()).append(" ");
}
if(CollectionUtils.nullSafeHasContents(subFilters))
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
{
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(subFilter);
}
rs.append(")");
if(CollectionUtils.nullSafeHasContents(orderBys))
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
{
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
{
rs.append(orderBy).append(",");
}
rs.append("]");
rs.append(orderBy).append(",");
}
rs.append("]");
}
catch(Exception e)
{

View File

@ -22,8 +22,6 @@
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;
@ -51,10 +49,6 @@ 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
{
@ -65,30 +59,13 @@ 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;
/*******************************************************************************
** check if a join is an OUTER type (LEFT or RIGHT).
*******************************************************************************/
public static boolean isOuter(Type type)
{
return (LEFT == type || RIGHT == type);
}
}
{INNER, LEFT, RIGHT, FULL}
@ -371,66 +348,4 @@ 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);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "QueryJoin{base="
+ baseTableOrAlias + ", joinTable='"
+ joinTable + ", joinMetaData="
+ (joinMetaData == null ? null : joinMetaData.getName()) + ", alias='"
+ alias + ", select="
+ select + ", type="
+ type + '}';
}
}

View File

@ -27,15 +27,11 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.reflect.ClassPath;
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.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -47,29 +43,6 @@ public class MetaDataProducerHelper
{
private static final QLogger LOG = QLogger.getLogger(MetaDataProducerHelper.class);
private static Map<Class<?>, Integer> comparatorValuesByType = new HashMap<>();
private static Integer defaultComparatorValue;
static
{
////////////////////////////////////////////////////////////////////////////////////////
// define how we break ties in sort-order based on the meta-dta type. e.g., do apps //
// after all other types (as apps often try to get other types from the instance) //
// also - do backends earlier than others (e.g., tables may expect backends to exist) //
// any types not in the map get the default value. //
////////////////////////////////////////////////////////////////////////////////////////
comparatorValuesByType.put(QBackendMetaData.class, 1);
/////////////////////////////////////
// unspecified ones will come here //
/////////////////////////////////////
defaultComparatorValue = 10;
comparatorValuesByType.put(QJoinMetaData.class, 21);
comparatorValuesByType.put(QWidgetMetaData.class, 22);
comparatorValuesByType.put(QAppMetaData.class, 23);
}
/*******************************************************************************
@ -129,9 +102,11 @@ public class MetaDataProducerHelper
}
}
/////////////////////////////////////////////////////////////////////////////////////////////
// sort them by sort order, then by the type that they return, as set up in the static map //
/////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
// sort them by sort order, then by the type that they return - specifically - doing apps //
// after all other types (as apps often try to get other types from the instance) //
// also - do backends earlier than others (e.g., tables may expect backends to exist) //
////////////////////////////////////////////////////////////////////////////////////////////
producers.sort(Comparator
.comparing((MetaDataProducerInterface<?> p) -> p.getSortOrder())
.thenComparing((MetaDataProducerInterface<?> p) ->
@ -139,7 +114,18 @@ public class MetaDataProducerHelper
try
{
Class<?> outputType = p.getClass().getMethod("produce", QInstance.class).getReturnType();
return comparatorValuesByType.getOrDefault(outputType, defaultComparatorValue);
if(outputType.equals(QAppMetaData.class))
{
return (2);
}
else if(outputType.equals(QBackendMetaData.class))
{
return (0);
}
else
{
return (1);
}
}
catch(Exception e)
{

View File

@ -155,6 +155,18 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter, returning generically, to help sub-class fluent flows
*******************************************************************************/
@SuppressWarnings("unchecked")
public <T extends QBackendMetaData> T withBackendType(String backendType)
{
this.backendType = backendType;
return (T) this;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -93,13 +93,7 @@ public class DateTimeDisplayValueBehavior implements FieldDisplayBehavior<DateTi
{
try
{
Instant instant = record.getValueInstant(field.getName());
if(instant == null)
{
continue;
}
Instant instant = record.getValueInstant(field.getName());
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of(defaultZoneId));
record.setDisplayValue(field.getName(), QValueFormatter.formatDateTimeWithZone(zonedDateTime));
}
@ -121,57 +115,35 @@ public class DateTimeDisplayValueBehavior implements FieldDisplayBehavior<DateTi
{
try
{
Instant instant = record.getValueInstant(field.getName());
if(instant == null)
Instant instant = record.getValueInstant(field.getName());
String zoneString = record.getValueString(zoneIdFromFieldName);
ZoneId zoneId;
try
{
continue;
zoneId = ZoneId.of(zoneString);
}
String zoneString = record.getValueString(zoneIdFromFieldName);
ZoneId zoneId = null;
if(StringUtils.hasContent(zoneString))
{
try
{
zoneId = ZoneId.of(zoneString);
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we probably(?) don't need a stack trace here (and it could get noisy?), so just info w/ the exception message... //
// and we expect this might be somewhat frequent, if you might have invalid values in your zoneId field... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.info("Exception applying zoneIdFromFieldName behavior", logPair("message", e.getMessage()), logPair("table", table.getName()), logPair("field", field.getName()), logPair("id", record.getValue(table.getPrimaryKeyField())));
}
}
if(zoneId == null)
catch(Exception e)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if the zone string from the other field isn't valid, and we have a fallback, try to use it //
////////////////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(fallbackZoneId))
{
////////////////////////////////////////////////////////////////////////////////////////////
// assume that validation has confirmed this is a valid zone - so no try-catch right here //
////////////////////////////////////////////////////////////////////////////////////////////
zoneId = ZoneId.of(fallbackZoneId);
}
else
{
throw (e);
}
}
if(zoneId != null)
{
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
record.setDisplayValue(field.getName(), QValueFormatter.formatDateTimeWithZone(zonedDateTime));
}
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
record.setDisplayValue(field.getName(), QValueFormatter.formatDateTimeWithZone(zonedDateTime));
}
catch(Exception e)
{
///////////////////////////////////////////////////////////////////////
// we don't expect this to ever hit - so warn it w/ stack if it does //
///////////////////////////////////////////////////////////////////////
LOG.warn("Unexpected error applying zoneIdFromFieldName behavior", e, logPair("table", table.getName()), logPair("field", field.getName()), logPair("id", record.getValue(table.getPrimaryKeyField())));
LOG.info("Error applying zoneIdFromFieldName DateTimeDisplayValueBehavior", e, logPair("table", table.getName()), logPair("field", field.getName()), logPair("id", record.getValue(table.getPrimaryKeyField())));
}
}
}

View File

@ -39,7 +39,6 @@ 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;
@ -76,8 +75,6 @@ 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!! //
//////////////////////////////////////////////////////////////////////////////////
@ -107,8 +104,6 @@ public class QFrontendTableMetaData
}
this.sections = tableMetaData.getSections();
this.shareableTableMetaData = tableMetaData.getShareableTableMetaData();
}
if(includeJoins)
@ -372,14 +367,4 @@ public class QFrontendTableMetaData
return (this.variantTableLabel);
}
/*******************************************************************************
** Getter for shareableTableMetaData
**
*******************************************************************************/
public ShareableTableMetaData getShareableTableMetaData()
{
return shareableTableMetaData;
}
}

View File

@ -53,7 +53,6 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
// for type = TABLE //
//////////////////////
private String tableName;
private String overrideIdField;
private List<String> searchFields;
private List<QFilterOrderBy> orderByFields;
@ -631,35 +630,4 @@ 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

@ -1,198 +0,0 @@
/*
* 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,7 +22,6 @@
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;
@ -42,7 +41,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 implements Cloneable
public class RecordSecurityLock
{
private String securityKeyType;
private String fieldName;
@ -53,28 +52,6 @@ public class RecordSecurityLock implements Cloneable
/*******************************************************************************
**
*******************************************************************************/
@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
**
@ -129,9 +106,8 @@ public class RecordSecurityLock implements Cloneable
*******************************************************************************/
public enum LockScope
{
READ_AND_WRITE, // lock both reads and writes
WRITE, // only lock writes
READ // only lock reads
READ_AND_WRITE,
WRITE
}
@ -289,22 +265,4 @@ public class RecordSecurityLock implements Cloneable
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "RecordSecurityLock{"
+ "securityKeyType='" + securityKeyType + '\''
+ ", fieldName='" + fieldName + '\''
+ ", joinNameChain=" + joinNameChain
+ ", nullValueBehavior=" + nullValueBehavior
+ ", lockScope=" + lockScope
+ '}';
}
}

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.List;
import java.util.Set;
/*******************************************************************************
@ -47,65 +46,6 @@ 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

@ -1,48 +0,0 @@
/*
* 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

@ -1,186 +0,0 @@
/*
* 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

@ -1,398 +0,0 @@
/*
* 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,7 +48,6 @@ 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;
@ -108,7 +107,6 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private List<ExposedJoin> exposedJoins;
private ShareableTableMetaData shareableTableMetaData;
/*******************************************************************************
@ -1387,35 +1385,4 @@ 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, label = "Owner")
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID)
private String userId;
@QField(label = "Query Filter")

View File

@ -39,20 +39,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.JoinType;
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;
@ -63,7 +56,6 @@ public class SavedReportsMetaDataProvider
{
public static final String REPORT_STORAGE_TABLE_NAME = "reportStorage";
public static final String SHARED_SAVED_REPORT_JOIN_SAVED_REPORT = "sharedSavedReportJoinSavedReport";
/*******************************************************************************
@ -88,31 +80,6 @@ public class SavedReportsMetaDataProvider
instance.addWidget(defineReportSetupWidget());
instance.addWidget(definePivotTableSetupWidget());
/////////////////////////////////////
// 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()));
}
}
/*******************************************************************************
**
*******************************************************************************/
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")));
}
@ -198,44 +165,6 @@ 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

@ -1,267 +0,0 @@
/*
* 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

@ -26,22 +26,15 @@ 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.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer;
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.savedviews.DeleteSavedViewProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.QuerySavedViewProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.StoreSavedViewProcess;
@ -52,7 +45,6 @@ import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.Store
*******************************************************************************/
public class SavedViewsMetaDataProvider
{
public static final String SHARED_SAVED_VIEW_JOIN_SAVED_VIEW = "sharedSavedViewJoinSavedView";
/*******************************************************************************
@ -65,16 +57,6 @@ public class SavedViewsMetaDataProvider
instance.addProcess(QuerySavedViewProcess.getProcessMetaData());
instance.addProcess(StoreSavedViewProcess.getProcessMetaData());
instance.addProcess(DeleteSavedViewProcess.getProcessMetaData());
/////////////////////////////////////
// todo - param to enable sharing? //
/////////////////////////////////////
instance.addTable(defineSharedSavedViewTable(backendName, backendDetailEnricher));
instance.addJoin(defineSharedSavedViewJoinSavedView());
if(instance.getPossibleValueSource(ShareScopePossibleValueMetaDataProducer.NAME) == null)
{
instance.addPossibleValueSource(new ShareScopePossibleValueMetaDataProducer().produce(new QInstance()));
}
}
@ -122,50 +104,4 @@ public class SavedViewsMetaDataProvider
.withOrderByField("label");
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineSharedSavedViewTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(SharedSavedView.TABLE_NAME)
.withLabel("Shared View")
.withIcon(new QIcon().withName("share"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("savedViewId")
.withBackendName(backendName)
.withUniqueKey(new UniqueKey("savedViewId", "userId"))
.withPrimaryKeyField("id")
.withFieldsFromEntity(SharedSavedView.class)
// todo - security key
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "savedViewId", "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);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
private QJoinMetaData defineSharedSavedViewJoinSavedView()
{
return (new QJoinMetaData()
.withName(SHARED_SAVED_VIEW_JOIN_SAVED_VIEW)
.withLeftTable(SharedSavedView.TABLE_NAME)
.withRightTable(SavedView.TABLE_NAME)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("savedViewId", "id")));
}
}

View File

@ -1,265 +0,0 @@
/*
* 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.savedviews;
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 view table
*******************************************************************************/
public class SharedSavedView extends QRecordEntity
{
public static final String TABLE_NAME = "sharedSavedView";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = SavedView.TABLE_NAME, label = "View")
private Integer savedViewId;
@QField(label = "User")
private String userId;
@QField(possibleValueSourceName = ShareScopePossibleValueMetaDataProducer.NAME)
private String scope;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SharedSavedView()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SharedSavedView(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 com.kingsrook.qqq.backend.core.model.savedviews.SharedSavedView 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 com.kingsrook.qqq.backend.core.model.savedviews.SharedSavedView 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 com.kingsrook.qqq.backend.core.model.savedviews.SharedSavedView withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for savedViewId
*******************************************************************************/
public Integer getSavedViewId()
{
return (this.savedViewId);
}
/*******************************************************************************
** Setter for savedViewId
*******************************************************************************/
public void setSavedViewId(Integer savedViewId)
{
this.savedViewId = savedViewId;
}
/*******************************************************************************
** Fluent setter for savedViewId
*******************************************************************************/
public com.kingsrook.qqq.backend.core.model.savedviews.SharedSavedView withSavedViewId(Integer savedViewId)
{
this.savedViewId = savedViewId;
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 com.kingsrook.qqq.backend.core.model.savedviews.SharedSavedView 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 com.kingsrook.qqq.backend.core.model.savedviews.SharedSavedView withScope(String scope)
{
this.scope = scope;
return (this);
}
}

View File

@ -1,39 +0,0 @@
/*
* 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,14 +52,4 @@ public interface QAuthenticationModuleCustomizerInterface
//////////
}
/*******************************************************************************
**
*******************************************************************************/
default void finalCustomizeSession(QInstance qInstance, QSession qSession)
{
//////////
// noop //
//////////
}
}

View File

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

View File

@ -27,18 +27,21 @@ import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** This class is responsible for loading a backend module, by its name, and
** returning an instance.
**
** TODO - make this mapping runtime-bound, not pre-compiled in.
**
*******************************************************************************/
public class QBackendModuleDispatcher
{
private static final QLogger LOG = QLogger.getLogger(QBackendModuleDispatcher.class);
private static Map<String, String> backendTypeToModuleClassNameMap = new HashMap<>();
private static Map<String, String> backendTypeToModuleClassNameMap;
@ -47,6 +50,51 @@ public class QBackendModuleDispatcher
*******************************************************************************/
public QBackendModuleDispatcher()
{
initBackendTypeToModuleClassNameMap();
}
/*******************************************************************************
**
*******************************************************************************/
private static void initBackendTypeToModuleClassNameMap()
{
if(backendTypeToModuleClassNameMap != null)
{
return;
}
Map<String, String> newMap = new HashMap<>();
String[] moduleClassNames = new String[]
{
// todo - let modules somehow "export" their types here?
// e.g., backend-core shouldn't need to "know" about the modules.
"com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule",
"com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule",
"com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration.EnumerationBackendModule",
"com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule",
"com.kingsrook.qqq.backend.module.api.APIBackendModule"
};
for(String moduleClassName : moduleClassNames)
{
try
{
Class<?> moduleClass = Class.forName(moduleClassName);
QBackendModuleInterface module = (QBackendModuleInterface) moduleClass.getConstructor().newInstance();
newMap.put(module.getBackendType(), moduleClassName);
}
catch(Exception e)
{
LOG.debug("Backend module could not be loaded", e, logPair("moduleClassName", moduleClassName));
}
}
backendTypeToModuleClassNameMap = newMap;
}
@ -56,6 +104,7 @@ public class QBackendModuleDispatcher
*******************************************************************************/
public static void registerBackendModule(QBackendModuleInterface moduleInstance)
{
initBackendTypeToModuleClassNameMap();
String backendType = moduleInstance.getBackendType();
if(backendTypeToModuleClassNameMap.containsKey(backendType))
{

View File

@ -40,7 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails
/*******************************************************************************
** Interface that a QBackendModule must implement.
**
** Note, all methods have a default version, which throws a 'not implemented'
** Note, some methods all have a default version, which throws a 'not implemented'
** exception.
**
*******************************************************************************/

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.enumerati
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -38,10 +37,6 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class EnumerationBackendModule implements QBackendModuleInterface
{
static
{
QBackendModuleDispatcher.registerBackendModule(new EnumerationBackendModule());
}
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).

View File

@ -29,7 +29,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -44,12 +43,6 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class MemoryBackendModule implements QBackendModuleInterface
{
static
{
QBackendModuleDispatcher.registerBackendModule(new MemoryBackendModule());
}
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/

View File

@ -177,14 +177,6 @@ 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)
@ -240,7 +232,16 @@ public class MemoryRecordStore
{
QTableMetaData nextTable = qInstance.getTable(queryJoin.getJoinTable());
Collection<QRecord> nextTableRecords = getTableData(nextTable).values();
QJoinMetaData joinMetaData = Objects.requireNonNull(queryJoin.getJoinMetaData(), () -> "Could not find a join between tables [" + leftTable + "][" + queryJoin.getJoinTable() + "]");
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
{
QJoinMetaData found = joinsContext.findJoinMetaData(qInstance, input.getTableName(), queryJoin.getJoinTable());
if(found == null)
{
throw (new RuntimeException("Could not find a join between tables [" + input.getTableName() + "][" + queryJoin.getJoinTable() + "]"));
}
return (found);
});
List<QRecord> nextLevelProduct = new ArrayList<>();
for(QRecord productRecord : crossProduct)

View File

@ -28,7 +28,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -41,11 +40,6 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class MockBackendModule implements QBackendModuleInterface
{
static
{
QBackendModuleDispatcher.registerBackendModule(new MockBackendModule());
}
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/

View File

@ -78,16 +78,14 @@ 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 - OR - the table name portion of the field name //
// matches the record's field name, then just use the field-name portion... //
// field names in the record are fully qualified, 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(".")) || parts[0].equals(qRecord.getTableName()))
if(values.keySet().stream().noneMatch(n -> n.contains(".")))
{
value = qRecord.getValue(parts[1]);
value = qRecord.getValue(fieldName.substring(fieldName.indexOf(".") + 1));
}
}
}
@ -179,8 +177,6 @@ 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;
}
@ -207,13 +203,12 @@ 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.
*******************************************************************************/
static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator)
private 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);
@ -222,7 +217,6 @@ public class BackendQueryFilterUtils
else
{
accumulatorValue |= newValue;
accumulator.set(accumulatorValue);
if(accumulatorValue)
{
return (true);

View File

@ -112,17 +112,4 @@ 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,14 +24,8 @@ 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;
@ -42,13 +36,9 @@ 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;
@ -115,7 +105,6 @@ 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());
@ -150,45 +139,6 @@ 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);
}
/*******************************************************************************
**
*******************************************************************************/
@ -198,7 +148,6 @@ 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();
@ -298,33 +247,4 @@ 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,6 +72,15 @@ 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. //
/////////////////////////////////////////////////////////////////
@ -81,19 +90,6 @@ 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,16 +77,14 @@ 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

@ -1,130 +0,0 @@
/*
* 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

@ -1,140 +0,0 @@
/*
* 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

@ -1,249 +0,0 @@
/*
* 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

@ -1,208 +0,0 @@
/*
* 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

@ -1,81 +0,0 @@
/*
* 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

@ -1,126 +0,0 @@
/*
* 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

@ -1,61 +0,0 @@
/*
* 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);
}
}
}

View File

@ -191,41 +191,6 @@ public class QScheduleManager
/*******************************************************************************
**
*******************************************************************************/
public void setupAllNewSchedules() throws QException
{
if(QContext.getQInstance().getTables().containsKey(ScheduledJob.TABLE_NAME))
{
List<ScheduledJob> scheduledJobList = new QueryAction()
.execute(new QueryInput(ScheduledJob.TABLE_NAME)
.withIncludeAssociations(true))
.getRecordEntities(ScheduledJob.class);
for(ScheduledJob scheduledJob : scheduledJobList)
{
try
{
QSchedulerInterface scheduler = getScheduler(scheduledJob.getSchedulerName());
BasicSchedulableIdentity schedulableIdentity = SchedulableIdentityFactory.of(scheduledJob);
SchedulableType schedulableType = qInstance.getSchedulableType(scheduledJob.getType());
if(!scheduler.isScheduled(schedulableIdentity, schedulableType))
{
setupScheduledJob(scheduledJob);
}
}
catch(Exception e)
{
LOG.warn("Error evaluating scheduled job", logPair("id", scheduledJob.getId()));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -27,7 +27,6 @@ import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.identity.BasicSchedulableIdentity;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.identity.SchedulableIdentity;
@ -66,11 +65,6 @@ public interface QSchedulerInterface
*******************************************************************************/
void unscheduleSchedulable(SchedulableIdentity schedulableIdentity, SchedulableType schedulableType);
/*******************************************************************************
**
*******************************************************************************/
boolean isScheduled(BasicSchedulableIdentity schedulableIdentity, SchedulableType schedulableType);
/*******************************************************************************
**
*******************************************************************************/

View File

@ -1,90 +0,0 @@
/*
* 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.scheduler.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
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.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
/*******************************************************************************
** Management process to schedule all new scheduled jobs (in all schedulers).
*******************************************************************************/
public class ScheduleAllNewJobsProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData()
.withName(getClass().getSimpleName())
.withLabel("Schedule all New Scheduled Jobs")
.withIcon(new QIcon("more_time"))
.withStepList(List.of(
new QFrontendStepMetaData()
.withName("confirm")
.withComponent(new NoCodeWidgetFrontendComponentMetaData()
.withOutput(new WidgetHtmlLine().withVelocityTemplate("Please confirm you wish to schedule all new jobs."))),
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(getClass())),
new QFrontendStepMetaData()
.withName("results")
.withComponent(new NoCodeWidgetFrontendComponentMetaData()
.withOutput(new WidgetHtmlLine().withVelocityTemplate("All new jobs have been scheduled.")))));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
try
{
QScheduleManager.getInstance().setupAllNewSchedules();
}
catch(Exception e)
{
throw (new QException("Error scheduling new jobs.", e));
}
}
}

View File

@ -44,7 +44,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QSchedulerInterface;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.identity.BasicSchedulableIdentity;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.identity.SchedulableIdentity;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.AnyKey;
@ -487,26 +486,6 @@ public class QuartzScheduler implements QSchedulerInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean isScheduled(BasicSchedulableIdentity schedulableIdentity, SchedulableType schedulableType)
{
try
{
JobKey jobKey = new JobKey(schedulableIdentity.getIdentity(), schedulableType.getName());
return (isJobAlreadyScheduled(jobKey));
}
catch(Exception e)
{
LOG.warn("Error checking if job is scheduled", logPair("identity", schedulableIdentity));
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -37,7 +37,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QSchedulerInterface;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.identity.BasicSchedulableIdentity;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.identity.SchedulableIdentity;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -204,17 +203,6 @@ public class SimpleScheduler implements QSchedulerInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean isScheduled(BasicSchedulableIdentity schedulableIdentity, SchedulableType schedulableType)
{
return (executors.containsKey(schedulableIdentity));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -244,7 +244,6 @@ public class JsonUtils
.registerModule(new JavaTimeModule())
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
/* todo - some future version we may need to do inclusion/exclusion lists like this:

View File

@ -40,8 +40,6 @@ public class BaseTest
{
private static final QLogger LOG = QLogger.getLogger(BaseTest.class);
public static final String DEFAULT_USER_ID = "001";
/*******************************************************************************
@ -52,34 +50,15 @@ public class BaseTest
{
System.setProperty("qqq.logger.logSessionId.disabled", "true");
QContext.init(TestUtils.defineInstance(), newSession());
QContext.init(TestUtils.defineInstance(), new QSession()
.withUser(new QUser()
.withIdReference("001")
.withFullName("Anonymous")));
resetMemoryRecordStore();
}
/*******************************************************************************
**
*******************************************************************************/
protected QSession newSession()
{
return newSession(DEFAULT_USER_ID);
}
/*******************************************************************************
**
*******************************************************************************/
protected QSession newSession(String userId)
{
return new QSession().withUser(new QUser()
.withIdReference(userId)
.withFullName("Anonymous"));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -1,109 +0,0 @@
/*
* 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.actions.tables.helpers;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper.RecordWithErrors;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
/*******************************************************************************
** Unit test for ValidateRecordSecurityLockHelper
*******************************************************************************/
class ValidateRecordSecurityLockHelperTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordWithErrors()
{
{
RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
recordWithErrors.add(new BadInputStatusMessage("0"), List.of(0));
System.out.println(recordWithErrors);
recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(new RecordSecurityLock())));
System.out.println("----------------------------------------------------------------------------");
}
{
RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
recordWithErrors.add(new BadInputStatusMessage("1"), List.of(1));
System.out.println(recordWithErrors);
recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())));
System.out.println("----------------------------------------------------------------------------");
}
{
RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
recordWithErrors.add(new BadInputStatusMessage("0"), List.of(0));
recordWithErrors.add(new BadInputStatusMessage("1"), List.of(1));
System.out.println(recordWithErrors);
recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())));
System.out.println("----------------------------------------------------------------------------");
}
{
RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
recordWithErrors.add(new BadInputStatusMessage("1,1"), List.of(1, 1));
System.out.println(recordWithErrors);
recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(
new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())),
new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock()))
)));
System.out.println("----------------------------------------------------------------------------");
}
{
RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
recordWithErrors.add(new BadInputStatusMessage("0,0"), List.of(0, 0));
recordWithErrors.add(new BadInputStatusMessage("1,1"), List.of(1, 1));
System.out.println(recordWithErrors);
recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(
new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())),
new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock()))
)));
System.out.println("----------------------------------------------------------------------------");
}
{
RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
recordWithErrors.add(new BadInputStatusMessage("0"), List.of(0));
recordWithErrors.add(new BadInputStatusMessage("1,1"), List.of(1, 1));
System.out.println(recordWithErrors);
recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(
new RecordSecurityLock(),
new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock()))
)));
System.out.println("----------------------------------------------------------------------------");
}
}
}

View File

@ -55,10 +55,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DateTimeDisplayValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DateTimeDisplayValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
@ -78,7 +78,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
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.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
@ -1903,7 +1902,6 @@ public class QInstanceValidatorTest extends BaseTest
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(new ArrayList<>())), "looks like a join (has a dot), but no joinNameChain was given");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("storeId")), "does not look like a join (does not have a dot), but a joinNameChain was given");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("order.wrongId")), "unrecognized fieldName: order.wrongId");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("lineItem.id")), "joinNameChain doesn't end in the expected table [lineItem]");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(List.of("notAJoin"))), "an unrecognized join");
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(List.of("orderLineItem"))), "joinNameChain could not be followed through join");
}
@ -2005,21 +2003,7 @@ public class QInstanceValidatorTest extends BaseTest
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
});
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testShareableTableMetaData()
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// just make sure we call this class's validator - the rest of its conditions are covered in its own test //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable("A", "id").withShareableTableMetaData(new ShareableTableMetaData())),
"missing sharedRecordTableName");
}
@ -2128,7 +2112,7 @@ public class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
protected QTableMetaData newTable(String tableName, String... fieldNames)
private QTableMetaData newTable(String tableName, String... fieldNames)
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName(tableName)
@ -2222,7 +2206,7 @@ public class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
** Assert that an instance is valid!
*******************************************************************************/
public static void assertValidationSuccess(Consumer<QInstance> setup)
private void assertValidationSuccess(Consumer<QInstance> setup)
{
try
{

View File

@ -36,7 +36,6 @@ import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
@ -102,48 +101,6 @@ class DateTimeDisplayValueBehaviorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBadZoneIdFromOtherField()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
table.getField("createDate").withBehavior(new DateTimeDisplayValueBehavior().withZoneIdFromFieldName("timeZone"));
QRecord record = new QRecord().withValue("createDate", Instant.parse("2024-04-04T19:12:00Z")).withValue("timeZone", "fail");
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
assertNull(record.getDisplayValue("createDate"));
record = new QRecord().withValue("createDate", Instant.parse("2024-04-04T19:12:00Z")).withValue("timeZone", null);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
assertNull(record.getDisplayValue("createDate"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNullValue()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
table.getField("createDate").withBehavior(new DateTimeDisplayValueBehavior().withZoneIdFromFieldName("timeZone"));
QRecord record = new QRecord().withValue("createDate", null).withValue("timeZone", "UTC");
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
assertNull(record.getDisplayValue("createDate"));
}
/*******************************************************************************
**
*******************************************************************************/
@ -151,8 +108,8 @@ class DateTimeDisplayValueBehaviorTest extends BaseTest
void testValidation()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
QFieldMetaData field = table.getField("createDate");
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
QFieldMetaData field = table.getField("createDate");
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
Function<Consumer<DateTimeDisplayValueBehavior>, List<String>> testOne = setup ->

View File

@ -1,160 +0,0 @@
/*
* 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.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.OR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for RecordSecurityLockFilters
*******************************************************************************/
class RecordSecurityLockFiltersTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
MultiRecordSecurityLock nullBecauseNull = RecordSecurityLockFilters.filterForReadLockTree(null);
assertNull(nullBecauseNull);
MultiRecordSecurityLock emptyBecauseEmptyList = RecordSecurityLockFilters.filterForReadLockTree(List.of());
assertEquals(0, emptyBecauseEmptyList.getLocks().size());
MultiRecordSecurityLock emptyBecauseAllWrite = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
));
assertEquals(0, emptyBecauseAllWrite.getLocks().size());
MultiRecordSecurityLock onlyA = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
));
assertMultiRecordSecurityLock(onlyA, AND, "A");
MultiRecordSecurityLock twoOutOfThreeTopLevel = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
));
assertMultiRecordSecurityLock(twoOutOfThreeTopLevel, AND, "A", "C");
MultiRecordSecurityLock treeOfAllReads = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
)),
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
))
));
assertEquals(2, treeOfAllReads.getLocks().size());
assertEquals(AND, treeOfAllReads.getOperator());
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeOfAllReads.getLocks().get(0), OR, "A", "B");
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeOfAllReads.getLocks().get(1), OR, "C", "D");
MultiRecordSecurityLock treeWithOneBranchReadsOneBranchWrites = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
)),
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.WRITE)
))
));
assertEquals(2, treeWithOneBranchReadsOneBranchWrites.getLocks().size());
assertEquals(AND, treeWithOneBranchReadsOneBranchWrites.getOperator());
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeWithOneBranchReadsOneBranchWrites.getLocks().get(0), OR, "A", "B");
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeWithOneBranchReadsOneBranchWrites.getLocks().get(1), OR);
MultiRecordSecurityLock deepSparseTree = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
)),
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.WRITE)
)),
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
new RecordSecurityLock().withFieldName("E").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("F").withLockScope(RecordSecurityLock.LockScope.WRITE)
)),
new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
new RecordSecurityLock().withFieldName("G").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("H").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
))
)),
new RecordSecurityLock().withFieldName("I").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("J").withLockScope(RecordSecurityLock.LockScope.WRITE)
));
assertEquals(3, deepSparseTree.getLocks().size());
assertEquals(AND, deepSparseTree.getOperator());
MultiRecordSecurityLock deepChild0 = (MultiRecordSecurityLock) deepSparseTree.getLocks().get(0);
assertEquals(2, deepChild0.getLocks().size());
assertEquals(OR, deepChild0.getOperator());
MultiRecordSecurityLock deepGrandChild0 = (MultiRecordSecurityLock) deepChild0.getLocks().get(0);
assertMultiRecordSecurityLock(deepGrandChild0, AND, "A");
assertEquals("C", deepChild0.getLocks().get(1).getFieldName());
MultiRecordSecurityLock deepChild1 = (MultiRecordSecurityLock) deepSparseTree.getLocks().get(1);
assertEquals(2, deepChild1.getLocks().size());
assertEquals(OR, deepChild1.getOperator());
MultiRecordSecurityLock deepGrandChild1 = (MultiRecordSecurityLock) deepChild1.getLocks().get(0);
assertMultiRecordSecurityLock(deepGrandChild1, AND);
MultiRecordSecurityLock deepGrandChild2 = (MultiRecordSecurityLock) deepChild1.getLocks().get(1);
assertMultiRecordSecurityLock(deepGrandChild2, AND, "G", "H");
assertEquals("I", deepSparseTree.getLocks().get(2).getFieldName());
}
/*******************************************************************************
**
*******************************************************************************/
private void assertMultiRecordSecurityLock(MultiRecordSecurityLock lock, MultiRecordSecurityLock.BooleanOperator operator, String... lockFieldNames)
{
assertEquals(lockFieldNames.length, lock.getLocks().size());
assertEquals(operator, lock.getOperator());
for(int i = 0; i < lockFieldNames.length; i++)
{
assertEquals(lockFieldNames[i], lock.getLocks().get(i).getFieldName());
}
}
}

View File

@ -1,129 +0,0 @@
/*
* 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.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.instances.QInstanceValidatorTest.assertValidationFailureReasons;
import static com.kingsrook.qqq.backend.core.instances.QInstanceValidatorTest.assertValidationFailureReasonsAllowingExtraReasons;
import static com.kingsrook.qqq.backend.core.instances.QInstanceValidatorTest.assertValidationSuccess;
/*******************************************************************************
** Unit test for ShareableTableMetaData
*******************************************************************************/
class ShareableTableMetaDataTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testValidation()
{
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData())),
"missing sharedRecordTableName");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName("notATable")
)), "unrecognized sharedRecordTableName");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withAudienceTypesPossibleValueSourceName("notAPVS")
)), "unrecognized audienceTypesPossibleValueSourceName");
assertValidationFailureReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
)), "missing assetIdFieldName",
"missing scopeFieldName",
"missing audienceTypes");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withAssetIdFieldName("notAField")
)), "unrecognized assertIdFieldName");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withScopeFieldName("notAField")
)), "unrecognized scopeFieldName");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withAudienceType(new ShareableAudienceType().withName("myType"))
)), "missing fieldName for shareableAudienceType");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withAudienceType(new ShareableAudienceType().withName("myType").withFieldName("notAField"))
)), "unrecognized fieldName");
/* todo - corresponding todo in main class
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withAudienceType(new ShareableAudienceType().withName("myType").withFieldName("firstName").withSourceTableName("notATable"))
)), "unrecognized sourceTableName");
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withAudienceType(new ShareableAudienceType().withName("myType").withFieldName("firstName").withSourceTableName(TestUtils.TABLE_NAME_SHAPE).withSourceTableKeyFieldName("notAField"))
)), "unrecognized sourceTableKeyFieldName");
*/
assertValidationFailureReasonsAllowingExtraReasons(qInstance -> qInstance.addTable(newTable().withShareableTableMetaData(new ShareableTableMetaData()
.withThisTableOwnerIdFieldName("notAField")
)), "unrecognized thisTableOwnerIdFieldName");
assertValidationSuccess(qInstance -> qInstance.addTable(newTable()
.withField(new QFieldMetaData("userId", QFieldType.INTEGER))
.withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withAssetIdFieldName("firstName")
.withScopeFieldName("firstName")
.withThisTableOwnerIdFieldName("userId")
.withAudienceTypesPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)
.withAudienceType(new ShareableAudienceType().withName("myType").withFieldName("lastName").withSourceTableName(TestUtils.TABLE_NAME_SHAPE).withSourceTableKeyFieldName("id"))
)));
}
/*******************************************************************************
**
*******************************************************************************/
protected QTableMetaData newTable()
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName("A")
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id");
tableMetaData.addField(new QFieldMetaData("id", QFieldType.INTEGER));
return (tableMetaData);
}
}

View File

@ -45,7 +45,6 @@ import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.scheduler.SchedulerTestUtils;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzJobAndTriggerWrapper;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -98,7 +97,7 @@ class ScheduledJobTableCustomizerTest extends BaseTest
@AfterEach
void afterEach()
{
SchedulerTestUtils.afterEach();
QuartzTestUtils.afterEach();
}

View File

@ -89,7 +89,7 @@ class EnumerationCountActionTest extends BaseTest
QInstance instance = QContext.getQInstance();
instance.addBackend(new QBackendMetaData()
.withName("enum")
.withBackendType(EnumerationBackendModule.class)
.withBackendType("enum")
);
instance.addTable(new QTableMetaData()

View File

@ -167,7 +167,7 @@ class EnumerationQueryActionTest extends BaseTest
QInstance instance = QContext.getQInstance();
instance.addBackend(new QBackendMetaData()
.withName("enum")
.withBackendType(EnumerationBackendModule.class)
.withBackendType("enum")
);
instance.addTable(new QTableMetaData()

View File

@ -224,9 +224,6 @@ class MemoryBackendModuleTest extends BaseTest
));
new InsertAction().execute(insertInput);
assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.TRUE)).size());
assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.FALSE)).size());
assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 2))).size());
assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(3, 4))).size());

View File

@ -23,16 +23,11 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
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.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -42,182 +37,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class BackendQueryFilterUtilsTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatch_emptyFilters()
{
assertTrue(BackendQueryFilterUtils.doesRecordMatch(null, new QRecord().withValue("a", 1)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(new QQueryFilter(), new QRecord().withValue("a", 1)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(new QQueryFilter().withSubFilters(ListBuilder.of(null)), new QRecord().withValue("a", 1)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(new QQueryFilter().withSubFilters(List.of(new QQueryFilter())), new QRecord().withValue("a", 1)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatch_singleAnd()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatch_singleOr()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2)));
}
/***************************************************************************
**
***************************************************************************/
@Test
void testDoesRecordMatch_multipleAnd()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1))
.withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord()));
}
/***************************************************************************
**
***************************************************************************/
@Test
void testDoesRecordMatch_multipleOr()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1))
.withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 3).withValue("b", 4)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord()));
}
/***************************************************************************
**
***************************************************************************/
@Test
void testDoesRecordMatch_subFilterAnd()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withSubFilters(List.of(
new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)),
new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2))
));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord()));
}
/***************************************************************************
**
***************************************************************************/
@Test
void testDoesRecordMatch_subFilterOr()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withSubFilters(List.of(
new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)),
new QQueryFilter()
.withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2))
));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2)));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 3).withValue("b", 4)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatch_criteriaHasTableNameNoFieldsDo()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("t.a", QCriteriaOperator.EQUALS, 1));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatch_criteriaHasTableNameSomeFieldsDo()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("t.a", QCriteriaOperator.EQUALS, 1));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// shouldn't find the "a", because "some" fields in here have a prefix (e.g., 's' was a join table, selected with 't' as the main table, which didn't prefix) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("s.b", 2)));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// but this case (contrasted with above) set the record's tableName to "t", so criteria on "t.a" should find field "a" //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withTableName("t").withValue("a", 1).withValue("s.b", 2)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatch_criteriaHasTableNameMatchingField()
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withCriteria(new QFilterCriteria("t.a", QCriteriaOperator.EQUALS, 1));
assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("t.a", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("t.b", 1)));
assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("s.a", 1)));
}
/*******************************************************************************
**
*******************************************************************************/
@ -365,94 +184,4 @@ class BackendQueryFilterUtilsTest
assertFalse("Not Darin".matches(pattern));
assertFalse("David".matches(pattern));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testApplyBooleanOperator()
{
/////////////////////////////
// tests for operator: AND //
/////////////////////////////
{
/////////////////////////////////////////////////////////////////////////////////////
// old value was true; new value is true. //
// result should be true, and we should not be short-circuited (return value null) //
/////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(true);
assertNull(BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.AND));
assertTrue(accumulator.getPlain());
}
{
//////////////////////////////////////////////////////////////////////////////////////
// old value was true; new value is false. //
// result should be false, and we should be short-circuited (return value not-null) //
//////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(true);
assertEquals(Boolean.FALSE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.AND));
assertFalse(accumulator.getPlain());
}
{
//////////////////////////////////////////////////////////////////////////////////////
// old value was false; new value is true. //
// result should be false, and we should be short-circuited (return value not-null) //
//////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(false);
assertEquals(Boolean.FALSE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.AND));
assertFalse(accumulator.getPlain());
}
{
//////////////////////////////////////////////////////////////////////////////////////
// old value was false; new value is false. //
// result should be false, and we should be short-circuited (return value not-null) //
//////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(false);
assertEquals(Boolean.FALSE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.AND));
assertFalse(accumulator.getPlain());
}
////////////////////////////
// tests for operator: OR //
////////////////////////////
{
/////////////////////////////////////////////////////////////////////////////////////
// old value was true; new value is true. //
// result should be true, and we should be short-circuited (return value not-null) //
/////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(true);
assertEquals(Boolean.TRUE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.OR));
assertTrue(accumulator.getPlain());
}
{
//////////////////////////////////////////////////////////////////////////////////////
// old value was true; new value is false. //
// result should be true, and we should be short-circuited (return value not-null) //
//////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(true);
assertEquals(Boolean.TRUE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.OR));
assertTrue(accumulator.getPlain());
}
{
//////////////////////////////////////////////////////////////////////////////////////
// old value was false; new value is true. //
// result should be false, and we should be short-circuited (return value not-null) //
//////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(false);
assertEquals(Boolean.TRUE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.OR));
assertTrue(accumulator.getPlain());
}
{
//////////////////////////////////////////////////////////////////////////////////////
// old value was false; new value is false. //
// result should be false, and we should not be short-circuited (return value null) //
//////////////////////////////////////////////////////////////////////////////////////
AtomicBoolean accumulator = new AtomicBoolean(false);
assertNull(BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.OR));
assertFalse(accumulator.getPlain());
}
}
}

View File

@ -1,132 +0,0 @@
/*
* 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.BaseTest;
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.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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for DeleteSharedRecordProcess
*******************************************************************************/
class DeleteSharedRecordProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws Exception
{
new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withLabel("Test")
.withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id")))
));
new InsertAction().execute(new InsertInput(SharedSavedReport.TABLE_NAME).withRecordEntity(new SharedSavedReport()
.withSavedReportId(1)
.withUserId(BaseTest.DEFAULT_USER_ID)
.withScope(ShareScope.READ_WRITE.getPossibleValueId())
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFailCases() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
DeleteSharedRecordProcess processStep = new DeleteSharedRecordProcess();
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: tableName");
input.addValue("tableName", SavedReport.TABLE_NAME);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: recordId");
input.addValue("recordId", 1);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: shareId");
input.addValue("shareId", 3);
///////////////////////////////////////////////////
// fail because the requested record isn't found //
///////////////////////////////////////////////////
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Error deleting shared record: No record was found to delete");
///////////////////////////////////////////////////////////////////////////////////////////////////////
// now fail because a different user (than the owner, who did the initial delete) is trying to share //
///////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.setQSession(newSession("not-" + DEFAULT_USER_ID));
input.addValue("shareId", 1);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("not the owner of this record");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSuccess() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
DeleteSharedRecordProcess processStep = new DeleteSharedRecordProcess();
input.addValue("tableName", SavedReport.TABLE_NAME);
input.addValue("recordId", 1);
input.addValue("shareId", 1);
//////////////////////////////////////////
// assert the shared record got deleted //
//////////////////////////////////////////
processStep.run(input, output);
QRecord sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1));
assertNull(sharedSavedReportRecord);
}
}

View File

@ -1,144 +0,0 @@
/*
* 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.BaseTest;
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.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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for EditSharedRecordProcess
*******************************************************************************/
class EditSharedRecordProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws Exception
{
new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withLabel("Test")
.withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id")))
));
new InsertAction().execute(new InsertInput(SharedSavedReport.TABLE_NAME).withRecordEntity(new SharedSavedReport()
.withSavedReportId(1)
.withUserId(BaseTest.DEFAULT_USER_ID)
.withScope(ShareScope.READ_WRITE.getPossibleValueId())
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFailCases() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
EditSharedRecordProcess processStep = new EditSharedRecordProcess();
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: tableName");
input.addValue("tableName", SavedReport.TABLE_NAME);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: recordId");
input.addValue("recordId", 1);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: scopeId");
input.addValue("scopeId", ShareScope.READ_WRITE.getPossibleValueId());
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: shareId");
input.addValue("shareId", 3);
///////////////////////////////////////////////////
// fail because the requested record isn't found //
///////////////////////////////////////////////////
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Error editing shared record: No record was found to update");
///////////////////////////////////////////////////////////////////////////////////////////////////////
// now fail because a different user (than the owner, who did the initial edit) is trying to share //
///////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.setQSession(newSession("not-" + DEFAULT_USER_ID));
input.addValue("shareId", 1);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("not the owner of this record");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSuccess() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
EditSharedRecordProcess processStep = new EditSharedRecordProcess();
///////////////////////////
// assert original value //
///////////////////////////
QRecord sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1));
assertEquals(ShareScope.READ_WRITE.getPossibleValueId(), sharedSavedReportRecord.getValueString("scope"));
input.addValue("tableName", SavedReport.TABLE_NAME);
input.addValue("recordId", 1);
input.addValue("shareId", 1);
input.addValue("scopeId", ShareScope.READ_ONLY.getPossibleValueId());
/////////////////////////////////////////
// assert the shared record got edited //
/////////////////////////////////////////
processStep.run(input, output);
//////////////////////////
// assert updated value //
//////////////////////////
sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1));
assertEquals(ShareScope.READ_ONLY.getPossibleValueId(), sharedSavedReportRecord.getValueString("scope"));
}
}

View File

@ -1,98 +0,0 @@
/*
* 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 com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for GetSharedRecordsProcess
*******************************************************************************/
class GetSharedRecordsProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws Exception
{
new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withLabel("Test")
.withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id")))));
new InsertAction().execute(new InsertInput(SharedSavedReport.TABLE_NAME).withRecordEntity(new SharedSavedReport()
.withScope(ShareScope.READ_WRITE.getPossibleValueId())
.withUserId("007")
.withSavedReportId(1)
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
GetSharedRecordsProcess processStep = new GetSharedRecordsProcess();
input.addValue("tableName", SavedReport.TABLE_NAME);
input.addValue("recordId", 1);
processStep.run(input, output);
List<QRecord> resultList = (List<QRecord>) output.getValue("resultList");
assertEquals(1, resultList.size());
QRecord outputRecord = resultList.get(0);
assertEquals(1, outputRecord.getValueInteger("shareId"));
assertEquals(ShareScope.READ_WRITE.getPossibleValueId(), outputRecord.getValueString("scopeId"));
assertEquals("user", outputRecord.getValueString("audienceType"));
assertEquals("007", outputRecord.getValueString("audienceId"));
assertEquals("user 007", outputRecord.getValueString("audienceLabel"));
}
}

View File

@ -1,146 +0,0 @@
/*
* 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.BaseTest;
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.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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for InsertSharedRecordProcess
*******************************************************************************/
class InsertSharedRecordProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws Exception
{
new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFailCases() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
InsertSharedRecordProcess processStep = new InsertSharedRecordProcess();
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: tableName");
input.addValue("tableName", SavedReport.TABLE_NAME);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: recordId");
input.addValue("recordId", 1);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: audienceType");
input.addValue("audienceType", "user");
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: audienceId");
input.addValue("audienceId", "darin@kingsrook.com");
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: scopeId");
input.addValue("scopeId", ShareScope.READ_WRITE);
//////////////////////////////
// try a non-sharable table //
//////////////////////////////
input.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("is not shareable");
input.addValue("tableName", SavedReport.TABLE_NAME);
///////////////////////////////////////////////////
// fail because the requested record isn't found //
///////////////////////////////////////////////////
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("record could not be found in table, savedReport, with primary key: 1");
new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withLabel("Test")
.withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id")))
));
///////////////////////////////////////////////////////////////////////////////////////////////////////
// now fail because a different user (than the owner, who did the initial insert) is trying to share //
///////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.setQSession(newSession("not-" + DEFAULT_USER_ID));
assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("not the owner of this record");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSuccess() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
InsertSharedRecordProcess processStep = new InsertSharedRecordProcess();
input.addValue("tableName", SavedReport.TABLE_NAME);
input.addValue("recordId", 1);
input.addValue("audienceType", "user");
input.addValue("audienceId", "darin@kingsrook.com");
input.addValue("scopeId", ShareScope.READ_WRITE);
new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withLabel("Test")
.withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id")))
));
////////////////////////////////////////
// assert the shared record got built //
////////////////////////////////////////
processStep.run(input, output);
QRecord sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1));
assertNotNull(sharedSavedReportRecord);
assertEquals(1, sharedSavedReportRecord.getValueInteger("savedReportId"));
assertEquals("darin@kingsrook.com", sharedSavedReportRecord.getValueString("userId"));
assertEquals(ShareScope.READ_WRITE.getPossibleValueId(), sharedSavedReportRecord.getValueString("scope"));
}
}

View File

@ -1,46 +0,0 @@
/*
* 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.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** Unit test for SharingMetaDataProvider
*******************************************************************************/
class SharingMetaDataProviderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
new SharingMetaDataProvider().defineAll(QContext.getQInstance(), null);
}
}

View File

@ -54,7 +54,28 @@ class QScheduleManagerTest extends BaseTest
void afterEach()
{
QLogger.deactivateCollectingLoggerForClass(QuartzScheduler.class);
SchedulerTestUtils.afterEach();
try
{
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}
@ -157,4 +178,4 @@ class QScheduleManagerTest extends BaseTest
.anyMatch(l -> l.getMessage().matches(".*Scheduled new job.*TABLE_AUTOMATIONS.scheduledJob:4.*"));
}
}
}

View File

@ -36,7 +36,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
@ -87,38 +86,6 @@ public class SchedulerTestUtils
/*******************************************************************************
**
*******************************************************************************/
public static void afterEach()
{
try
{
QScheduleManager.getInstance().stop();
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().stop();
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -58,7 +58,28 @@ class RescheduleAllJobsProcessTest extends BaseTest
void afterEach()
{
QLogger.deactivateCollectingLoggerForClass(QuartzScheduler.class);
SchedulerTestUtils.afterEach();
try
{
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}

View File

@ -1,157 +0,0 @@
/*
* 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.scheduler.processes;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
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.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.scheduler.SchedulerTestUtils;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzJobAndTriggerWrapper;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.runner.SchedulableSQSQueueRunner;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.quartz.SchedulerException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for ScheduleAllNewJobsProcess
*******************************************************************************/
class ScheduleAllNewJobsProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
SchedulerTestUtils.afterEach();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException, SchedulerException
{
try
{
QCollectingLogger quartzSchedulerLog = QLogger.activateCollectingLoggerForClass(QuartzScheduler.class);
QInstance qInstance = QContext.getQInstance();
new ScheduledJobsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, ScheduleAllNewJobsProcess.class.getPackageName());
QuartzTestUtils.setupInstanceForQuartzTests();
///////////////////////////////////////////////////////////////////////////////////
// clear out the customizers that would normally schedule jobs as we insert them //
///////////////////////////////////////////////////////////////////////////////////
qInstance.getTable(ScheduledJob.TABLE_NAME).withCustomizers(Collections.emptyMap());
QScheduleManager qScheduleManager = QScheduleManager.initInstance(qInstance, () -> QContext.getQSession());
qScheduleManager.start();
QuartzScheduler quartzScheduler = QuartzScheduler.getInstance();
List<QuartzJobAndTriggerWrapper> wrappers = quartzScheduler.queryQuartz();
//////////////////////////////////////////////
// make sure nothing is scheduled initially //
//////////////////////////////////////////////
assertTrue(wrappers.isEmpty());
////////////////////////////////////////////////////////////////////////////
// insert a scheduled job - run schedule-new, make sure it gets scheduled //
////////////////////////////////////////////////////////////////////////////
new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(SchedulerTestUtils
.newScheduledJob(ScheduledJobType.PROCESS, Map.of("processName", TestUtils.PROCESS_NAME_GREET_PEOPLE))
.withLabel("Test job 1")
.withId(null)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME)));
RunProcessInput input = new RunProcessInput();
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.setProcessName(ScheduleAllNewJobsProcess.class.getSimpleName());
new RunProcessAction().execute(input);
///////////////////////////////////////////////////////////////
// make sure our scheduledJob here got scheduled with quartz //
///////////////////////////////////////////////////////////////
wrappers = quartzScheduler.queryQuartz();
assertEquals(1, wrappers.size());
assertTrue(wrappers.stream().anyMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:1")));
///////////////
// repeat it //
///////////////
new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(SchedulerTestUtils
.newScheduledJob(ScheduledJobType.PROCESS, Map.of("processName", TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE))
.withLabel("Test job 2")
.withId(null)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME)));
input = new RunProcessInput();
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.setProcessName(ScheduleAllNewJobsProcess.class.getSimpleName());
new RunProcessAction().execute(input);
wrappers = quartzScheduler.queryQuartz();
assertEquals(2, wrappers.size());
assertTrue(wrappers.stream().anyMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
/////////////////////////////////////////////////////////////////////////////////////
// make sure quartzScheduler never logged about deleting or re-scheduling anything //
/////////////////////////////////////////////////////////////////////////////////////
assertThat(quartzSchedulerLog.getCollectedMessages())
.noneMatch(m -> m.getMessage().toLowerCase().contains("delete"))
.noneMatch(m -> m.getMessage().toLowerCase().contains("re-schedule"));
}
finally
{
QLogger.deactivateCollectingLoggerForClass(SchedulableSQSQueueRunner.class);
}
}
}

View File

@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
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.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -39,10 +40,8 @@ import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.quartz.SchedulerException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -52,23 +51,35 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class UnscheduleAllJobsProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
SchedulerTestUtils.afterEach();
}
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
SchedulerTestUtils.afterEach();
QLogger.deactivateCollectingLoggerForClass(QuartzScheduler.class);
try
{
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}
@ -92,7 +103,7 @@ class UnscheduleAllJobsProcessTest extends BaseTest
QuartzScheduler quartzScheduler = QuartzScheduler.getInstance();
List<QuartzJobAndTriggerWrapper> wrappers = quartzScheduler.queryQuartz();
assertEquals(1, wrappers.size());
assertTrue(wrappers.stream().anyMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
RunProcessInput input = new RunProcessInput();
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
@ -100,7 +111,7 @@ class UnscheduleAllJobsProcessTest extends BaseTest
new RunProcessAction().execute(input);
wrappers = quartzScheduler.queryQuartz();
assertTrue(wrappers.isEmpty());
assertTrue(wrappers.stream().noneMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
}
}

View File

@ -62,7 +62,7 @@ class QuartzSchedulerTest extends BaseTest
@AfterEach
void afterEach()
{
SchedulerTestUtils.afterEach();
QuartzTestUtils.afterEach();
}

View File

@ -27,6 +27,7 @@ import java.util.Properties;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.quartz.QuartzSchedulerMetaData;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import org.quartz.SchedulerException;
@ -103,4 +104,34 @@ public class QuartzTestUtils
return QuartzScheduler.getInstance().queryQuartz();
}
/*******************************************************************************
**
*******************************************************************************/
public static void afterEach()
{
try
{
QScheduleManager.getInstance().stop();
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}
}

View File

@ -41,7 +41,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
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.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.scheduler.SchedulerTestUtils;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzJobAndTriggerWrapper;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
@ -93,7 +92,7 @@ class QuartzJobsProcessTest extends BaseTest
@AfterEach
void afterEach()
{
SchedulerTestUtils.afterEach();
QuartzTestUtils.afterEach();
}

View File

@ -53,7 +53,7 @@ class SimpleSchedulerTest extends BaseTest
@AfterEach
void afterEach()
{
SchedulerTestUtils.afterEach();
QScheduleManager.getInstance().unInit();
}

View File

@ -30,7 +30,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.api.actions.APICountAction;
import com.kingsrook.qqq.backend.module.api.actions.APIDeleteAction;
@ -45,11 +44,6 @@ import com.kingsrook.qqq.backend.module.api.actions.APIUpdateAction;
*******************************************************************************/
public class APIBackendModule implements QBackendModuleInterface
{
static
{
QBackendModuleDispatcher.registerBackendModule(new APIBackendModule());
}
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/

View File

@ -32,7 +32,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
@ -56,10 +55,6 @@ public class FilesystemBackendModule implements QBackendModuleInterface, Filesys
public static final String BACKEND_TYPE = "filesystem";
static
{
QBackendModuleDispatcher.registerBackendModule(new FilesystemBackendModule());
}
/*******************************************************************************
** For filesystem backends, get the module-specific action base-class, that helps

View File

@ -30,7 +30,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
@ -51,10 +50,6 @@ public class S3BackendModule implements QBackendModuleInterface, FilesystemBacke
{
public static final String BACKEND_TYPE = "s3";
static
{
QBackendModuleDispatcher.registerBackendModule(new S3BackendModule());
}
/*******************************************************************************
** For filesystem backends, get the module-specific action base-class, that helps

View File

@ -50,6 +50,19 @@ public class S3BackendMetaData extends AbstractFilesystemBackendMetaData
/*******************************************************************************
** Fluent setter for backendType
**
*******************************************************************************/
@Override
public S3BackendMetaData withBackendType(String backendType)
{
setBackendType(backendType);
return this;
}
/*******************************************************************************
** Getter for bucketName
**

View File

@ -40,7 +40,6 @@ 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.modules.authentication.implementations.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.RecordFormat;
@ -404,7 +403,7 @@ public class TestUtils
public static QBackendMetaData defineMockBackend()
{
return (new QBackendMetaData()
.withBackendType(MockBackendModule.class)
.withBackendType("mock")
.withName(BACKEND_NAME_MOCK));
}

View File

@ -626,8 +626,6 @@ public class AbstractMongoDBAction
case IS_NOT_BLANK -> Filters.nor(filterIsBlank(fieldBackendName));
case BETWEEN -> filterBetween(fieldBackendName, values);
case NOT_BETWEEN -> Filters.nor(filterBetween(fieldBackendName, values));
case TRUE -> Filters.or(Filters.eq(fieldBackendName, "true"), Filters.ne(fieldBackendName, "true"), Filters.eq(fieldBackendName, null)); // todo test!!
case FALSE -> Filters.and(Filters.eq(fieldBackendName, "true"), Filters.ne(fieldBackendName, "true"), Filters.eq(fieldBackendName, null));
});
}

View File

@ -213,33 +213,6 @@ class MongoDBQueryActionTest extends BaseTest
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testTrueQuery() throws QException
{
QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.TRUE)));
QueryOutput queryOutput = new MongoDBQueryAction().execute(queryInput);
assertEquals(5, queryOutput.getRecords().size(), "'TRUE' query should find all rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testFalseQuery() throws QException
{
QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.FALSE)));
QueryOutput queryOutput = new MongoDBQueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size(), "'FALSE' query should find no rows");
}
/*******************************************************************************
**

View File

@ -35,7 +35,6 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.rdbms.actions.AbstractRDBMSAction;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSAggregateAction;
@ -56,10 +55,7 @@ public class RDBMSBackendModule implements QBackendModuleInterface
{
private static final QLogger LOG = QLogger.getLogger(RDBMSBackendModule.class);
static
{
QBackendModuleDispatcher.registerBackendModule(new RDBMSBackendModule());
}
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).

View File

@ -27,7 +27,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
@ -43,6 +42,7 @@ import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -50,7 +50,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.ImplicitQueryJoinForSecurityLock;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -64,15 +66,16 @@ 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.JoinType;
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.querystats.QueryStat;
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.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
@ -92,9 +95,6 @@ public abstract class AbstractRDBMSAction
protected PreparedStatement statement;
protected boolean isCancelled = false;
private static Memoization<String, Boolean> doesSelectClauseRequireDistinctMemoization = new Memoization<String, Boolean>()
.withTimeout(Duration.ofDays(365));
/*******************************************************************************
@ -210,7 +210,7 @@ public abstract class AbstractRDBMSAction
/*******************************************************************************
**
*******************************************************************************/
protected String makeFromClause(QInstance instance, String tableName, JoinsContext joinsContext, List<Serializable> params)
protected String makeFromClause(QInstance instance, String tableName, JoinsContext joinsContext) throws QException
{
StringBuilder rs = new StringBuilder(escapeIdentifier(getTableName(instance.getTable(tableName))) + " AS " + escapeIdentifier(tableName));
@ -227,9 +227,17 @@ public abstract class AbstractRDBMSAction
////////////////////////////////////////////////////////////
// find the join in the instance, to set the 'on' clause //
////////////////////////////////////////////////////////////
List<String> joinClauseList = new ArrayList<>();
String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName);
QJoinMetaData joinMetaData = Objects.requireNonNull(queryJoin.getJoinMetaData(), () -> "Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]");
List<String> joinClauseList = new ArrayList<>();
String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName);
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
{
QJoinMetaData found = joinsContext.findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
if(found == null)
{
throw (new RuntimeException("Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]"));
}
return (found);
});
for(JoinOn joinOn : joinMetaData.getJoinOns())
{
@ -260,17 +268,6 @@ public abstract class AbstractRDBMSAction
+ " = " + escapeIdentifier(joinTableOrAlias)
+ "." + escapeIdentifier(getColumnName((rightTable.getField(joinOn.getRightField())))));
}
if(CollectionUtils.nullSafeHasContents(queryJoin.getSecurityCriteria()))
{
Optional<String> securityOnClause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(joinsContext, queryJoin.getSecurityCriteria(), QQueryFilter.BooleanOperator.AND, params);
if(securityOnClause.isPresent())
{
LOG.debug("Wrote securityOnClause", logPair("clause", securityOnClause));
joinClauseList.add(securityOnClause.get());
}
}
rs.append(" ON ").append(StringUtils.join(" AND ", joinClauseList));
}
@ -286,66 +283,34 @@ public abstract class AbstractRDBMSAction
*******************************************************************************/
private List<QueryJoin> sortQueryJoinsForFromClause(String mainTableName, List<QueryJoin> queryJoins)
{
List<QueryJoin> rs = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////////
// make a copy of the input list that we can feel safe removing elements from //
////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> inputListCopy = new ArrayList<>(queryJoins);
///////////////////////////////////////////////////////////////////////////////////////////////////
// keep track of the tables (or aliases) that we've seen - that's what we'll "grow" outward from //
///////////////////////////////////////////////////////////////////////////////////////////////////
Set<String> seenTablesOrAliases = new HashSet<>();
seenTablesOrAliases.add(mainTableName);
List<QueryJoin> rs = new ArrayList<>();
Set<String> seenTables = new HashSet<>();
seenTables.add(mainTableName);
////////////////////////////////////////////////////////////////////////////////////
// loop as long as there are more tables in the inputList, and the keepGoing flag //
// is set (e.g., indicating that we added something in the last iteration) //
////////////////////////////////////////////////////////////////////////////////////
boolean keepGoing = true;
while(!inputListCopy.isEmpty() && keepGoing)
{
keepGoing = false;
Iterator<QueryJoin> iterator = inputListCopy.iterator();
while(iterator.hasNext())
{
QueryJoin nextQueryJoin = iterator.next();
//////////////////////////////////////////////////////////////////////////
// get the baseTableOrAlias from this join - and if it isn't set in the //
// QueryJoin, then get it from the left-side of the join's metaData //
//////////////////////////////////////////////////////////////////////////
String baseTableOrAlias = nextQueryJoin.getBaseTableOrAlias();
if(baseTableOrAlias == null && nextQueryJoin.getJoinMetaData() != null)
QueryJoin next = iterator.next();
if((StringUtils.hasContent(next.getBaseTableOrAlias()) && seenTables.contains(next.getBaseTableOrAlias())) || seenTables.contains(next.getJoinTable()))
{
baseTableOrAlias = nextQueryJoin.getJoinMetaData().getLeftTable();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have a baseTableOrAlias (would we ever not?), and we've seen it before - OR - we've seen this query join's joinTableOrAlias, //
// then we can add this pair of namesOrAliases to our seen-set, remove this queryJoin from the inputListCopy (iterator), and keep going //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if((StringUtils.hasContent(baseTableOrAlias) && seenTablesOrAliases.contains(baseTableOrAlias)) || seenTablesOrAliases.contains(nextQueryJoin.getJoinTableOrItsAlias()))
{
rs.add(nextQueryJoin);
if(StringUtils.hasContent(baseTableOrAlias))
rs.add(next);
if(StringUtils.hasContent(next.getBaseTableOrAlias()))
{
seenTablesOrAliases.add(baseTableOrAlias);
seenTables.add(next.getBaseTableOrAlias());
}
seenTablesOrAliases.add(nextQueryJoin.getJoinTableOrItsAlias());
seenTables.add(next.getJoinTable());
iterator.remove();
keepGoing = true;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case any are left, add them all here - does this ever happen? //
// the only time a conditional breakpoint here fires in the RDBMS test suite, is in query designed to throw. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
rs.addAll(inputListCopy);
return (rs);
@ -354,72 +319,208 @@ public abstract class AbstractRDBMSAction
/*******************************************************************************
** Method to make a full WHERE clause.
**
** Note that criteria for security are assumed to have been added to the filter
** during the construction of the JoinsContext.
** method that sub-classes should call to make a full WHERE clause, including
** security clauses.
*******************************************************************************/
protected String makeWhereClause(JoinsContext joinsContext, QQueryFilter filter, List<Serializable> params) throws IllegalArgumentException
protected String makeWhereClause(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List<Serializable> params) throws IllegalArgumentException, QException
{
String whereClauseWithoutSecurity = makeWhereClauseWithoutSecurity(instance, table, joinsContext, filter, params);
QQueryFilter securityFilter = getSecurityFilter(instance, session, table, joinsContext);
if(!securityFilter.hasAnyCriteria())
{
return (whereClauseWithoutSecurity);
}
String securityWhereClause = makeWhereClauseWithoutSecurity(instance, table, joinsContext, securityFilter, params);
return ("(" + whereClauseWithoutSecurity + ") AND (" + securityWhereClause + ")");
}
/*******************************************************************************
** private method for making the part of a where clause that gets AND'ed to the
** security clause. Recursively handles sub-clauses.
*******************************************************************************/
private String makeWhereClauseWithoutSecurity(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List<Serializable> params) throws IllegalArgumentException, QException
{
if(filter == null || !filter.hasAnyCriteria())
{
return ("1 = 1");
}
Optional<String> clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
String clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
{
///////////////////////////////////////////////////////////////
// if there are no sub-clauses, then just return this clause //
// and if there's no clause, use the default 1 = 1 //
///////////////////////////////////////////////////////////////
return (clause.orElse("1 = 1"));
return (clause);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, build a list of clauses - recursively expanding the sub-filters into clauses, then return them joined with our operator //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<String> clauses = new ArrayList<>();
if(clause.isPresent() && StringUtils.hasContent(clause.get()))
if(StringUtils.hasContent(clause))
{
clauses.add("(" + clause.get() + ")");
clauses.add("(" + clause + ")");
}
for(QQueryFilter subFilter : filter.getSubFilters())
{
String subClause = makeWhereClause(joinsContext, subFilter, params);
String subClause = makeWhereClauseWithoutSecurity(instance, table, joinsContext, subFilter, params);
if(StringUtils.hasContent(subClause))
{
clauses.add("(" + subClause + ")");
}
}
return (String.join(" " + filter.getBooleanOperator().toString() + " ", clauses));
}
/*******************************************************************************
**
** @return optional sql where sub-clause, as in "x AND y"
** Build a QQueryFilter to apply record-level security to the query.
** Note, it may be empty, if there are no lock fields, or all are all-access.
*******************************************************************************/
private Optional<String> getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(JoinsContext joinsContext, List<QFilterCriteria> criteria, QQueryFilter.BooleanOperator booleanOperator, List<Serializable> params) throws IllegalArgumentException
private QQueryFilter getSecurityFilter(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext)
{
QQueryFilter securityFilter = new QQueryFilter();
securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
// todo - uh, if it's a RIGHT (or FULL) join, then, this should be isOuter = true, right?
boolean isOuter = false;
addSubFilterForRecordSecurityLock(instance, session, table, securityFilter, recordSecurityLock, joinsContext, table.getName(), isOuter);
}
for(QueryJoin queryJoin : CollectionUtils.nonNullList(joinsContext.getQueryJoins()))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for user-added joins, we want to add their security-locks to the query //
// but if a join was implicitly added because it's needed to find a security lock on table being queried, //
// don't add additional layers of locks for each join table. that's the idea here at least. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(queryJoin instanceof ImplicitQueryJoinForSecurityLock)
{
continue;
}
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())))
{
boolean isOuter = queryJoin.getType().equals(QueryJoin.Type.LEFT); // todo full?
addSubFilterForRecordSecurityLock(instance, session, joinTable, securityFilter, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias(), isOuter);
}
}
return (securityFilter);
}
/*******************************************************************************
**
*******************************************************************************/
private static void addSubFilterForRecordSecurityLock(QInstance instance, QSession session, QTableMetaData table, QQueryFilter securityFilter, RecordSecurityLock recordSecurityLock, JoinsContext joinsContext, String tableNameOrAlias, boolean isOuter)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
///////////////////////////////////////////////////////////////////////////////
// if we have all-access on this key, then we don't need a criterion for it. //
///////////////////////////////////////////////////////////////////////////////
return;
}
}
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{
fieldName = recordSecurityLock.getFieldName();
}
///////////////////////////////////////////////////////////////////////////////////////////
// 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 = joinsContext.getFieldAndTableNameOrAlias(fieldName);
type = fieldAndTableNameOrAlias.field().getType();
}
catch(Exception e)
{
LOG.debug("Error getting field type... Trying Integer", e);
}
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
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 - make some explicit contradiction here - maybe even avoid running the whole query - as you're not allowed ANY records //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, Collections.emptyList()));
}
}
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(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
}
else
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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)
{
lockFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
lockFilter.addCriteria(new QFilterCriteria(tableNameOrAlias + "." + table.getPrimaryKeyField(), QCriteriaOperator.IS_BLANK));
}
securityFilter.addSubFilter(lockFilter);
}
/*******************************************************************************
**
*******************************************************************************/
private String getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List<QFilterCriteria> criteria, QQueryFilter.BooleanOperator booleanOperator, List<Serializable> params) throws IllegalArgumentException
{
List<String> clauses = new ArrayList<>();
for(QFilterCriteria criterion : criteria)
{
if(criterion.getFieldName() == null)
{
LOG.info("QFilter criteria is missing a fieldName - will not be included in query.");
continue;
}
if(criterion.getOperator() == null)
{
LOG.info("QFilter criteria is missing a operator - will not be included in query.", logPair("fieldName", criterion.getFieldName()));
continue;
}
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(criterion.getFieldName());
List<Serializable> values = criterion.getValues() == null ? new ArrayList<>() : new ArrayList<>(criterion.getValues());
@ -429,22 +530,25 @@ public abstract class AbstractRDBMSAction
Integer expectedNoOfParams = null;
switch(criterion.getOperator())
{
case EQUALS ->
case EQUALS:
{
clause += " = ?";
expectedNoOfParams = 1;
break;
}
case NOT_EQUALS ->
case NOT_EQUALS:
{
clause += " != ?";
expectedNoOfParams = 1;
break;
}
case NOT_EQUALS_OR_IS_NULL ->
case NOT_EQUALS_OR_IS_NULL:
{
clause += " != ? OR " + column + " IS NULL ";
expectedNoOfParams = 1;
break;
}
case IN ->
case IN:
{
if(values.isEmpty())
{
@ -457,8 +561,9 @@ public abstract class AbstractRDBMSAction
{
clause += " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
break;
}
case IS_NULL_OR_IN ->
case IS_NULL_OR_IN:
{
clause += " IS NULL ";
@ -466,8 +571,9 @@ public abstract class AbstractRDBMSAction
{
clause += " OR " + column + " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
break;
}
case NOT_IN ->
case NOT_IN:
{
if(values.isEmpty())
{
@ -480,74 +586,87 @@ public abstract class AbstractRDBMSAction
{
clause += " NOT IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
break;
}
case LIKE ->
case LIKE:
{
clause += " LIKE ?";
expectedNoOfParams = 1;
break;
}
case NOT_LIKE ->
case NOT_LIKE:
{
clause += " NOT LIKE ?";
expectedNoOfParams = 1;
break;
}
case STARTS_WITH ->
case STARTS_WITH:
{
clause += " LIKE ?";
ActionHelper.editFirstValue(values, (s -> s + "%"));
expectedNoOfParams = 1;
break;
}
case ENDS_WITH ->
case ENDS_WITH:
{
clause += " LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s));
expectedNoOfParams = 1;
break;
}
case CONTAINS ->
case CONTAINS:
{
clause += " LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s + "%"));
expectedNoOfParams = 1;
break;
}
case NOT_STARTS_WITH ->
case NOT_STARTS_WITH:
{
clause += " NOT LIKE ?";
ActionHelper.editFirstValue(values, (s -> s + "%"));
expectedNoOfParams = 1;
break;
}
case NOT_ENDS_WITH ->
case NOT_ENDS_WITH:
{
clause += " NOT LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s));
expectedNoOfParams = 1;
break;
}
case NOT_CONTAINS ->
case NOT_CONTAINS:
{
clause += " NOT LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s + "%"));
expectedNoOfParams = 1;
break;
}
case LESS_THAN ->
case LESS_THAN:
{
clause += " < ?";
expectedNoOfParams = 1;
break;
}
case LESS_THAN_OR_EQUALS ->
case LESS_THAN_OR_EQUALS:
{
clause += " <= ?";
expectedNoOfParams = 1;
break;
}
case GREATER_THAN ->
case GREATER_THAN:
{
clause += " > ?";
expectedNoOfParams = 1;
break;
}
case GREATER_THAN_OR_EQUALS ->
case GREATER_THAN_OR_EQUALS:
{
clause += " >= ?";
expectedNoOfParams = 1;
break;
}
case IS_BLANK ->
case IS_BLANK:
{
clause += " IS NULL";
if(field.getType().isStringLike())
@ -555,8 +674,9 @@ public abstract class AbstractRDBMSAction
clause += " OR " + column + " = ''";
}
expectedNoOfParams = 0;
break;
}
case IS_NOT_BLANK ->
case IS_NOT_BLANK:
{
clause += " IS NOT NULL";
if(field.getType().isStringLike())
@ -564,28 +684,24 @@ public abstract class AbstractRDBMSAction
clause += " AND " + column + " != ''";
}
expectedNoOfParams = 0;
break;
}
case BETWEEN ->
case BETWEEN:
{
clause += " BETWEEN ? AND ?";
expectedNoOfParams = 2;
break;
}
case NOT_BETWEEN ->
case NOT_BETWEEN:
{
clause += " NOT BETWEEN ? AND ?";
expectedNoOfParams = 2;
break;
}
case TRUE ->
default:
{
clause = " 1 = 1 ";
expectedNoOfParams = 0;
throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator());
}
case FALSE ->
{
clause = " 0 = 1 ";
expectedNoOfParams = 0;
}
default -> throw new IllegalStateException("Unexpected operator: " + criterion.getOperator());
}
if(expectedNoOfParams != null)
@ -640,16 +756,7 @@ public abstract class AbstractRDBMSAction
params.addAll(values);
}
//////////////////////////////////////////////////////////////////////////////
// since we're skipping criteria w/o a field or operator in the loop - //
// we can get to the end here without any clauses... so, return a null here //
//////////////////////////////////////////////////////////////////////////////
if(clauses.isEmpty())
{
return (Optional.empty());
}
return (Optional.of(String.join(" " + booleanOperator.toString() + " ", clauses)));
return (String.join(" " + booleanOperator.toString() + " ", clauses));
}
@ -858,7 +965,6 @@ public abstract class AbstractRDBMSAction
}
/*******************************************************************************
** Make it easy (e.g., for tests) to turn on logging of SQL
*******************************************************************************/
@ -945,52 +1051,25 @@ public abstract class AbstractRDBMSAction
/*******************************************************************************
** method that looks at security lock joins, and if a one-to-many is found where
** the specified field name is on the 'right side' of the join, then a distinct
** needs added to select clause.
**
** Memoized because it's a lot of gyrations, and it never ever changes for a
** running server.
** needs added to select clause
*******************************************************************************/
protected boolean doesSelectClauseRequireDistinct(QTableMetaData table)
{
if(table == null)
if(table != null)
{
return (false);
}
return doesSelectClauseRequireDistinctMemoization.getResult(table.getName(), (name) ->
{
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
return doesMultiLockRequireDistinct(multiRecordSecurityLock, table);
}).orElse(false);
}
/*******************************************************************************
**
*******************************************************************************/
private boolean doesMultiLockRequireDistinct(MultiRecordSecurityLock multiRecordSecurityLock, QTableMetaData table)
{
for(RecordSecurityLock recordSecurityLock : multiRecordSecurityLock.getLocks())
{
if(recordSecurityLock instanceof MultiRecordSecurityLock childMultiLock)
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
if(doesMultiLockRequireDistinct(childMultiLock, table))
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{
return (true);
}
}
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{
QJoinMetaData joinMetaData = QContext.getQInstance().getJoin(joinName);
if(JoinType.ONE_TO_MANY.equals(joinMetaData.getType()) && !joinMetaData.getRightTable().equals(table.getName()))
{
return (true);
}
else if(JoinType.MANY_TO_ONE.equals(joinMetaData.getType()) && !joinMetaData.getLeftTable().equals(table.getName()))
{
return (true);
QJoinMetaData joinMetaData = QContext.getQInstance().getJoin(joinName);
if(JoinType.ONE_TO_MANY.equals(joinMetaData.getType()) && !joinMetaData.getRightTable().equals(table.getName()))
{
return (true);
}
else if(JoinType.MANY_TO_ONE.equals(joinMetaData.getType()) && !joinMetaData.getLeftTable().equals(table.getName()))
{
return (true);
}
}
}
}
@ -1065,20 +1144,4 @@ public abstract class AbstractRDBMSAction
}
}
/*******************************************************************************
** Either clone the input filter (so we can change it safely), or return a new blank filter.
*******************************************************************************/
protected QQueryFilter clonedOrNewFilter(QQueryFilter filter)
{
if(filter == null)
{
return (new QQueryFilter());
}
else
{
return (filter.clone());
}
}
}

View File

@ -59,7 +59,6 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
private ActionTimeoutHelper actionTimeoutHelper;
/*******************************************************************************
**
*******************************************************************************/
@ -69,17 +68,16 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
{
QTableMetaData table = aggregateInput.getTable();
QQueryFilter filter = clonedOrNewFilter(aggregateInput.getFilter());
JoinsContext joinsContext = new JoinsContext(aggregateInput.getInstance(), table.getName(), aggregateInput.getQueryJoins(), filter);
List<Serializable> params = new ArrayList<>();
String fromClause = makeFromClause(aggregateInput.getInstance(), table.getName(), joinsContext, params);
JoinsContext joinsContext = new JoinsContext(aggregateInput.getInstance(), table.getName(), aggregateInput.getQueryJoins(), aggregateInput.getFilter());
String fromClause = makeFromClause(aggregateInput.getInstance(), table.getName(), joinsContext);
List<String> selectClauses = buildSelectClauses(aggregateInput, joinsContext);
String sql = "SELECT " + StringUtils.join(", ", selectClauses)
+ " FROM " + fromClause
+ " WHERE " + makeWhereClause(joinsContext, filter, params);
+ " FROM " + fromClause;
QQueryFilter filter = aggregateInput.getFilter();
List<Serializable> params = new ArrayList<>();
sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), aggregateInput.getSession(), table, joinsContext, filter, params);
if(CollectionUtils.nullSafeHasContents(aggregateInput.getGroupBys()))
{

Some files were not shown because too many files have changed in this diff Show More