mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-25 08:48:44 +00:00
Compare commits
17 Commits
snapshot-f
...
snapshot-i
Author | SHA1 | Date | |
---|---|---|---|
d44ab8582f | |||
b729d57661 | |||
88e697e698 | |||
1f3702d5fa | |||
eea67de539 | |||
c76ad7a5fe | |||
bb0315eef5 | |||
1106f6fe63 | |||
997c5572e2 | |||
b9677343a3 | |||
34c67e9d53 | |||
0f867c2001 | |||
266163be6b | |||
c40e5d813a | |||
9298ae2bbf | |||
3b3bbcb91b | |||
e77701ce15 |
6
pom.xml
6
pom.xml
@ -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
|
||||
|
@ -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 //
|
||||
//////////
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
**
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,5 @@ public enum QCriteriaOperator
|
||||
IS_BLANK,
|
||||
IS_NOT_BLANK,
|
||||
BETWEEN,
|
||||
NOT_BETWEEN,
|
||||
TRUE,
|
||||
FALSE
|
||||
NOT_BETWEEN
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
+ '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*******************************************************************************/
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -52,14 +52,4 @@ public interface QAuthenticationModuleCustomizerInterface
|
||||
//////////
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void finalCustomizeSession(QInstance qInstance, QSession qSession)
|
||||
{
|
||||
//////////
|
||||
// noop //
|
||||
//////////
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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.
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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).
|
||||
|
@ -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).
|
||||
*******************************************************************************/
|
||||
|
@ -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)
|
||||
|
@ -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).
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 //
|
||||
|
@ -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 //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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:
|
||||
|
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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("----------------------------------------------------------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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 ->
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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.*"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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 //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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 //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
@ -62,7 +62,7 @@ class QuartzSchedulerTest extends BaseTest
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
SchedulerTestUtils.afterEach();
|
||||
QuartzTestUtils.afterEach();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,7 +53,7 @@ class SimpleSchedulerTest extends BaseTest
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
SchedulerTestUtils.afterEach();
|
||||
QScheduleManager.getInstance().unInit();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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).
|
||||
*******************************************************************************/
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
**
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -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).
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
Reference in New Issue
Block a user