Compare commits

..

4 Commits

89 changed files with 570 additions and 4487 deletions

View File

@ -48,7 +48,7 @@
</modules>
<properties>
<revision>0.24.0</revision>
<revision>0.24.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -27,7 +27,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
@ -161,18 +160,4 @@ public interface RecordCustomizerUtilityInterface
return (oldRecordMap);
}
/***************************************************************************
**
***************************************************************************/
static <T extends Serializable> T getValueFromRecordOrOldRecord(String fieldName, QRecord record, Serializable primaryKey, Optional<Map<Serializable, QRecord>> oldRecordMap)
{
T value = (T) record.getValue(fieldName);
if(value == null && primaryKey != null && oldRecordMap.isPresent() && oldRecordMap.get().containsKey(primaryKey))
{
value = (T) oldRecordMap.get().get(primaryKey).getValue(fieldName);
}
return value;
}
}

View File

@ -1,251 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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.dashboard.widgets;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.FilterUseCase;
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.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
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.AbstractWidgetMetaDataBuilder;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
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.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Generic widget to display a list of records.
**
** Note, closely related to (and copied from ChildRecordListRenderer.
** opportunity to share more code with that in the future??
*******************************************************************************/
public class RecordListWidgetRenderer extends AbstractWidgetRenderer
{
private static final QLogger LOG = QLogger.getLogger(RecordListWidgetRenderer.class);
/*******************************************************************************
**
*******************************************************************************/
public static Builder widgetMetaDataBuilder(String widgetName)
{
return (new Builder(new QWidgetMetaData()
.withName(widgetName)
.withIsCard(true)
.withCodeReference(new QCodeReference(RecordListWidgetRenderer.class))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withValidatorPlugin(new RecordListWidgetValidator())
));
}
/*******************************************************************************
**
*******************************************************************************/
public static class Builder extends AbstractWidgetMetaDataBuilder
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Builder(QWidgetMetaData widgetMetaData)
{
super(widgetMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withLabel(String label)
{
widgetMetaData.setLabel(label);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withMaxRows(Integer maxRows)
{
widgetMetaData.withDefaultValue("maxRows", maxRows);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withTableName(String tableName)
{
widgetMetaData.withDefaultValue("tableName", tableName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withFilter(QQueryFilter filter)
{
widgetMetaData.withDefaultValue("filter", filter);
return (this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
try
{
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().get("maxRows"));
}
QQueryFilter filter = ((QQueryFilter) input.getWidgetMetaData().getDefaultValues().get("filter")).clone();
filter.interpretValues(new HashMap<>(input.getQueryParams()), FilterUseCase.DEFAULT);
filter.setLimit(maxRows);
String tableName = ValueUtils.getValueAsString(input.getWidgetMetaData().getDefaultValues().get("tableName"));
QTableMetaData table = QContext.getQInstance().getTable(tableName);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QValueFormatter.setBlobValuesToDownloadUrls(table, queryOutput.getRecords());
int totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
/////////////////////////////////////////////////////////////////////////////////////
// if the input said to only do some max, and the # of results we got is that max, //
// then do a count query, for displaying 1-n of <count> //
/////////////////////////////////////////////////////////////////////////////////////
CountInput countInput = new CountInput();
countInput.setTableName(tableName);
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
}
String tablePath = QContext.getQInstance().getTablePath(tableName);
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(input.getQueryParams().get("widgetLabel"), queryOutput, table, tablePath, viewAllLink, totalRows);
return (new RenderWidgetOutput(widgetData));
}
catch(Exception e)
{
LOG.warn("Error rendering record list widget", e, logPair("widgetName", () -> input.getWidgetMetaData().getName()));
throw (e);
}
}
/***************************************************************************
**
***************************************************************************/
private static class RecordListWidgetValidator implements QInstanceValidatorPluginInterface<QWidgetMetaDataInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QWidgetMetaDataInterface widgetMetaData, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
String prefix = "Widget " + widgetMetaData.getName() + ": ";
//////////////////////////////////////////////
// make sure table name is given and exists //
//////////////////////////////////////////////
QTableMetaData table = null;
String tableName = ValueUtils.getValueAsString(CollectionUtils.nonNullMap(widgetMetaData.getDefaultValues()).get("tableName"));
if(qInstanceValidator.assertCondition(StringUtils.hasContent(tableName), prefix + "defaultValue for tableName must be given"))
{
////////////////////////////
// make sure table exists //
////////////////////////////
table = qInstance.getTable(tableName);
qInstanceValidator.assertCondition(table != null, prefix + "No table named " + tableName + " exists in the instance");
}
////////////////////////////////////////////////////////////////////////////////////
// make sure filter is given and is valid (only check that if table is given too) //
////////////////////////////////////////////////////////////////////////////////////
QQueryFilter filter = ((QQueryFilter) widgetMetaData.getDefaultValues().get("filter"));
if(qInstanceValidator.assertCondition(filter != null, prefix + "defaultValue for filter must be given") && table != null)
{
qInstanceValidator.validateQueryFilter(qInstance, prefix, table, filter, null);
}
}
}
}

View File

@ -108,7 +108,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
}
catch(Exception e)
{
throw (new QReportingException("Error starting CSV report", e));
throw (new QReportingException("Error starting CSV report"));
}
}

View File

@ -32,7 +32,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
@ -83,22 +82,6 @@ public class CountAction
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** count to be returned, and you just want to pass in a table name and filter.
*******************************************************************************/
public static Integer execute(String tableName, QQueryFilter filter) throws QException
{
CountAction countAction = new CountAction();
CountInput countInput = new CountInput();
countInput.setTableName(tableName);
countInput.setFilter(filter);
CountOutput countOutput = countAction.execute(countInput);
return (countOutput.getCount());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -238,11 +238,6 @@ public class GetAction
*******************************************************************************/
public static QRecord execute(String tableName, Serializable primaryKey) throws QException
{
if(primaryKey instanceof QQueryFilter)
{
LOG.warn("Unexpected use of QQueryFilter instead of primary key in GetAction call");
}
GetAction getAction = new GetAction();
GetInput getInput = new GetInput(tableName).withPrimaryKey(primaryKey);
return getAction.executeForRecord(getInput);

View File

@ -114,7 +114,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
if(!StringUtils.hasContent(insertInput.getTableName()))
{
throw (new QException("Table name was not specified in insert input"));
throw (new QException("Table name was not specified in update input"));
}
QTableMetaData table = insertInput.getTable();

View File

@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
@ -75,31 +74,6 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
}
/***************************************************************************
** meant to be protected (but interface...) - for a custom PVS implementation
** to complete its search (e.g., after it generates the list of PVS objects,
** let this method do the filtering).
***************************************************************************/
default List<QPossibleValue<T>> completeCustomPVSSearch(SearchPossibleValueSourceInput input, List<QPossibleValue<T>> possibleValues)
{
SearchPossibleValueSourceAction.PreparedSearchPossibleValueSourceInput preparedInput = SearchPossibleValueSourceAction.prepareSearchPossibleValueSourceInput(input);
List<QPossibleValue<T>> rs = new ArrayList<>();
for(QPossibleValue<T> possibleValue : possibleValues)
{
if(possibleValue != null && SearchPossibleValueSourceAction.doesPossibleValueMatchSearchInput(possibleValue, preparedInput))
{
rs.add(possibleValue);
}
}
rs.sort(Comparator.nullsLast(Comparator.comparing((QPossibleValue<T> pv) -> pv.getLabel())));
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@ -32,6 +34,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -45,7 +48,6 @@ 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;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang3.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -486,8 +488,6 @@ public class QValueFormatter
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));
String defaultExtension = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.DEFAULT_EXTENSION));
Boolean downloadUrlDynamic = ValueUtils.getValueAsBoolean(adornmentValues.get(AdornmentType.FileDownloadValues.DOWNLOAD_URL_DYNAMIC));
for(QRecord record : records)
{
if(!doesFieldHaveValue(field, record))
@ -495,11 +495,6 @@ public class QValueFormatter
continue;
}
if(BooleanUtils.isTrue(downloadUrlDynamic))
{
continue;
}
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
String fileName = null;
@ -549,7 +544,10 @@ public class QValueFormatter
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_CODE_REFERENCE)
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_PROCESS_NAME))
{
record.setValue(field.getName(), AdornmentType.FileDownloadValues.makeFieldDownloadUrl(table.getName(), primaryKey, field.getName(), fileName));
record.setValue(field.getName(), "/data/" + table.getName() + "/"
+ URLEncoder.encode(ValueUtils.getValueAsString(primaryKey), StandardCharsets.UTF_8) + "/"
+ field.getName() + "/"
+ URLEncoder.encode(Objects.requireNonNullElse(fileName, ""), StandardCharsets.UTF_8));
}
record.setDisplayValue(field.getName(), fileName);
}

View File

@ -26,7 +26,6 @@ import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -52,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
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.tables.QTableMetaData;
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -108,54 +108,60 @@ public class SearchPossibleValueSourceAction
/***************************************************************************
** record to store "computed" values as part of a possible-value search -
** e.g., ids type-convered, and lower-cased labels.
***************************************************************************/
public record PreparedSearchPossibleValueSourceInput(Collection<?> inputIdsAsCorrectType, Collection<String> lowerCaseLabels, String searchTerm) {}
/***************************************************************************
**
***************************************************************************/
public static PreparedSearchPossibleValueSourceInput prepareSearchPossibleValueSourceInput(SearchPossibleValueSourceInput input)
{
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(input.getPossibleValueSourceName());
List<?> inputIdsAsCorrectType = convertInputIdsToPossibleValueSourceIdType(possibleValueSource, input.getIdList());
Set<String> lowerCaseLabels = null;
if(input.getLabelList() != null)
{
lowerCaseLabels = input.getLabelList().stream()
.filter(Objects::nonNull)
.map(l -> l.toLowerCase())
.collect(Collectors.toSet());
}
return (new PreparedSearchPossibleValueSourceInput(inputIdsAsCorrectType, lowerCaseLabels, input.getSearchTerm()));
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
PreparedSearchPossibleValueSourceInput preparedSearchPossibleValueSourceInput = prepareSearchPossibleValueSourceInput(input);
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
List<Serializable> matchingIds = new ArrayList<>();
List<?> inputIdsAsCorrectType = convertInputIdsToEnumIdType(possibleValueSource, input.getIdList());
Set<String> labels = null;
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
boolean match = doesPossibleValueMatchSearchInput(possibleValue, preparedSearchPossibleValueSourceInput);
boolean match = false;
if(input.getIdList() != null)
{
if(inputIdsAsCorrectType.contains(possibleValue.getId()))
{
match = true;
}
}
else if(input.getLabelList() != null)
{
if(labels == null)
{
labels = input.getLabelList().stream().filter(Objects::nonNull).map(l -> l.toLowerCase()).collect(Collectors.toSet());
}
if(labels.contains(possibleValue.getLabel().toLowerCase()))
{
match = true;
}
}
else
{
if(StringUtils.hasContent(input.getSearchTerm()))
{
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.getSearchTerm().toLowerCase())
|| possibleValue.getLabel().toLowerCase().startsWith(input.getSearchTerm().toLowerCase()));
}
else
{
match = true;
}
}
if(match)
{
matchingIds.add(possibleValue.getId());
matchingIds.add((Serializable) possibleValue.getId());
}
// todo - skip & limit?
// todo - default filter
}
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds);
@ -166,84 +172,42 @@ public class SearchPossibleValueSourceAction
/***************************************************************************
**
***************************************************************************/
public static boolean doesPossibleValueMatchSearchInput(QPossibleValue<?> possibleValue, PreparedSearchPossibleValueSourceInput input)
{
boolean match = false;
if(input.inputIdsAsCorrectType() != null)
{
if(input.inputIdsAsCorrectType().contains(possibleValue.getId()))
{
match = true;
}
}
else if(input.lowerCaseLabels() != null)
{
if(input.lowerCaseLabels().contains(possibleValue.getLabel().toLowerCase()))
{
match = true;
}
}
else
{
if(StringUtils.hasContent(input.searchTerm()))
{
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.searchTerm().toLowerCase())
|| possibleValue.getLabel().toLowerCase().startsWith(input.searchTerm().toLowerCase()));
}
else
{
match = true;
}
}
return match;
}
/*******************************************************************************
** The input list of ids might come through as a type that isn't the same as
** the type of the ids in the enum (e.g., strings from a frontend, integers
** in an enum). So, this method type-converts them.
** in an enum). So, this method looks at the first id in the enum, and then
** maps all the inputIds to be of the same type.
*******************************************************************************/
private static List<Object> convertInputIdsToPossibleValueSourceIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
{
List<Object> rs = new ArrayList<>();
if(inputIdList == null)
{
return (null);
}
else if(inputIdList.isEmpty())
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
{
return (rs);
}
QFieldType type = possibleValueSource.getIdType();
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
for(Serializable inputId : inputIdList)
{
Object properlyTypedId = null;
try
{
if(type.equals(QFieldType.INTEGER))
if(anIdFromTheEnum instanceof Integer)
{
properlyTypedId = ValueUtils.getValueAsInteger(inputId);
}
else if(type.isStringLike())
else if(anIdFromTheEnum instanceof String)
{
properlyTypedId = ValueUtils.getValueAsString(inputId);
}
else if(type.equals(QFieldType.BOOLEAN))
else if(anIdFromTheEnum instanceof Boolean)
{
properlyTypedId = ValueUtils.getValueAsBoolean(inputId);
}
else
{
LOG.warn("Unexpected type [" + type + "] for ids in enum: " + possibleValueSource.getName());
LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName());
}
}
catch(Exception e)
@ -251,7 +215,7 @@ public class SearchPossibleValueSourceAction
LOG.debug("Error converting possible value id to expected id type", e, logPair("value", inputId));
}
if(properlyTypedId != null)
if (properlyTypedId != null)
{
rs.add(properlyTypedId);
}
@ -433,9 +397,9 @@ public class SearchPossibleValueSourceAction
}
catch(Exception e)
{
String message = "Error searching custom possible value source [" + input.getPossibleValueSourceName() + "]";
String message = "Error sending searching custom possible value source [" + input.getPossibleValueSourceName() + "]";
LOG.warn(message, e);
throw (new QException(message, e));
throw (new QException(message));
}
}

View File

@ -95,7 +95,6 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.Bulk
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -1483,31 +1482,6 @@ public class QInstanceEnricher
/***************************************************************************
** scan the classpath for classes in the specified package name which
** implement the QInstanceEnricherPluginInterface - any found get added
***************************************************************************/
public static void discoverAndAddPluginsInPackage(String packageName) throws QException
{
try
{
for(Class<?> aClass : ClassPathUtils.getClassesInPackage(packageName))
{
if(QInstanceEnricherPluginInterface.class.isAssignableFrom(aClass))
{
QInstanceEnricherPluginInterface<?> plugin = (QInstanceEnricherPluginInterface<?>) aClass.getConstructor().newInstance();
addEnricherPlugin(plugin);
}
}
}
catch(Exception e)
{
throw (new QException("Error discovering and adding enricher plugins in package [" + packageName + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -142,8 +142,6 @@ public class QInstanceValidator
private static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> validatorPlugins = new ListingHash<>();
private JoinGraph joinGraph = null;
private List<String> errors = new ArrayList<>();
@ -171,7 +169,8 @@ public class QInstanceValidator
// the enricher will build a join graph (if there are any joins). we'd like to only do that //
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
long start = System.currentTimeMillis();
JoinGraph joinGraph = null;
long start = System.currentTimeMillis();
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -180,7 +179,7 @@ public class QInstanceValidator
// TODO - possible point of customization (use a different enricher, or none, or pass it options).
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(qInstance);
qInstanceEnricher.enrich();
this.joinGraph = qInstanceEnricher.getJoinGraph();
joinGraph = qInstanceEnricher.getJoinGraph();
}
catch(Exception e)
{
@ -1906,7 +1905,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
public void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
private void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
{
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
{
@ -1950,8 +1949,7 @@ public class QInstanceValidator
{
if(fieldName.contains("."))
{
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
String tableNameBeforeDot = fieldName.substring(0, fieldName.lastIndexOf("."));
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
if(CollectionUtils.nullSafeHasContents(queryJoins))
{
@ -1975,32 +1973,11 @@ public class QInstanceValidator
}
else
{
if(this.joinGraph != null)
{
Set<JoinGraph.JoinConnectionList> joinConnections = joinGraph.getJoinConnections(table.getName());
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnections)
{
JoinGraph.JoinConnection joinConnection = joinConnectionList.list().get(joinConnectionList.list().size() - 1);
if(tableNameBeforeDot.equals(joinConnection.joinTable()))
{
QTableMetaData joinTable = qInstance.getTable(tableNameBeforeDot);
if(joinTable.getFields().containsKey(fieldNameAfterDot))
{
/////////////////////////
// mmm, looks valid... //
/////////////////////////
return (true);
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////
// todo - not sure how vulnerable we are to ongoing issues here... //
// idea: let a filter (or any object?) be opted out of validation, some version of //
// a static map of objects we can check at the top of various validate methods... //
//////////////////////////////////////////////////////////////////////////////////////
errors.add("Failed to find field named: " + fieldName);
errors.add("QInstanceValidator does not yet support finding a field that looks like a join field, but isn't associated with a query.");
return (true);
// todo! for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
// {
// }
}
}
}

View File

@ -40,7 +40,6 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
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.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface;
@ -618,14 +617,4 @@ public class RunBackendStepInput extends AbstractActionInput
}
}
}
/***************************************************************************
**
***************************************************************************/
public QProcessMetaData getProcess()
{
return (QContext.getQInstance().getProcess(getProcessName()));
}
}

View File

@ -29,40 +29,5 @@ package com.kingsrook.qqq.backend.core.model.metadata;
*******************************************************************************/
public abstract class MetaDataProducer<T extends MetaDataProducerOutput> implements MetaDataProducerInterface<T>
{
private Class<?> sourceClass;
/*******************************************************************************
** Getter for sourceClass
**
*******************************************************************************/
@Override
public Class<?> getSourceClass()
{
return sourceClass;
}
/*******************************************************************************
** Setter for sourceClass
**
*******************************************************************************/
@Override
public void setSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
}
/*******************************************************************************
** Fluent setter for sourceClass
**
*******************************************************************************/
public MetaDataProducer<T> withSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
return (this);
}
}

View File

@ -106,10 +106,14 @@ public class MetaDataProducerHelper
}
/***************************************************************************
/*******************************************************************************
** Recursively find all classes in the given package, that implement MetaDataProducerInterface
** run them, and add their output to the given qInstance.
**
***************************************************************************/
public static List<MetaDataProducerInterface<?>> findProducers(String packageName) throws QException
** Note - they'll be sorted by the sortOrder they provide.
*******************************************************************************/
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName) throws QException
{
List<Class<?>> classesInPackage;
try
@ -192,20 +196,6 @@ public class MetaDataProducerHelper
}
}));
return (producers);
}
/*******************************************************************************
** Recursively find all classes in the given package, that implement MetaDataProducerInterface
** run them, and add their output to the given qInstance.
**
** Note - they'll be sorted by the sortOrder they provide.
*******************************************************************************/
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName) throws QException
{
List<MetaDataProducerInterface<?>> producers = findProducers(packageName);
///////////////////////////////////////////////////////////////////////////
// execute each one (if enabled), adding their meta data to the instance //
///////////////////////////////////////////////////////////////////////////
@ -239,19 +229,17 @@ public class MetaDataProducerHelper
**
***************************************************************************/
@SuppressWarnings("unchecked")
private static <T extends Serializable & PossibleValueEnum<T>> MetaDataProducerInterface<?> processMetaDataProducingPossibleValueEnum(Class<?> sourceClass)
private static <T extends Serializable & PossibleValueEnum<T>> MetaDataProducerInterface<?> processMetaDataProducingPossibleValueEnum(Class<?> aClass)
{
String warningPrefix = "Found a class annotated as @" + QMetaDataProducingPossibleValueEnum.class.getSimpleName();
if(!PossibleValueEnum.class.isAssignableFrom(sourceClass))
if(!PossibleValueEnum.class.isAssignableFrom(aClass))
{
LOG.warn(warningPrefix + ", but which is not a " + PossibleValueEnum.class.getSimpleName() + ", so it will not be used.", logPair("class", sourceClass.getSimpleName()));
LOG.warn(warningPrefix + ", but which is not a " + PossibleValueEnum.class.getSimpleName() + ", so it will not be used.", logPair("class", aClass.getSimpleName()));
return null;
}
PossibleValueEnum<?>[] values = (PossibleValueEnum<?>[]) sourceClass.getEnumConstants();
PossibleValueSourceOfEnumGenericMetaDataProducer<T> producer = new PossibleValueSourceOfEnumGenericMetaDataProducer<>(sourceClass.getSimpleName(), (PossibleValueEnum<T>[]) values);
producer.setSourceClass(sourceClass);
return producer;
PossibleValueEnum<?>[] values = (PossibleValueEnum<?>[]) aClass.getEnumConstants();
return (new PossibleValueSourceOfEnumGenericMetaDataProducer<T>(aClass.getSimpleName(), (PossibleValueEnum<T>[]) values));
}
@ -259,32 +247,32 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static List<MetaDataProducerInterface<?>> processMetaDataProducingEntity(Class<?> sourceClass) throws Exception
private static List<MetaDataProducerInterface<?>> processMetaDataProducingEntity(Class<?> aClass) throws Exception
{
List<MetaDataProducerInterface<?>> rs = new ArrayList<>();
QMetaDataProducingEntity qMetaDataProducingEntity = sourceClass.getAnnotation(QMetaDataProducingEntity.class);
QMetaDataProducingEntity qMetaDataProducingEntity = aClass.getAnnotation(QMetaDataProducingEntity.class);
String warningPrefix = "Found a class annotated as @" + QMetaDataProducingEntity.class.getSimpleName();
///////////////////////////////////////////////////////////
// make sures class is QRecordEntity and cast it as such //
///////////////////////////////////////////////////////////
if(!QRecordEntity.class.isAssignableFrom(sourceClass))
if(!QRecordEntity.class.isAssignableFrom(aClass))
{
LOG.warn(warningPrefix + ", but which is not a " + QRecordEntity.class.getSimpleName() + ", so it will not be used.", logPair("class", sourceClass.getSimpleName()));
LOG.warn(warningPrefix + ", but which is not a " + QRecordEntity.class.getSimpleName() + ", so it will not be used.", logPair("class", aClass.getSimpleName()));
return (rs);
}
@SuppressWarnings("unchecked") // safe per the check above.
Class<? extends QRecordEntity> recordEntityClass = (Class<? extends QRecordEntity>) sourceClass;
Class<? extends QRecordEntity> recordEntityClass = (Class<? extends QRecordEntity>) aClass;
////////////////////////////////////////////////
// get TABLE_NAME static field from the class //
////////////////////////////////////////////////
Field tableNameField = recordEntityClass.getDeclaredField("TABLE_NAME");
Field tableNameField = aClass.getDeclaredField("TABLE_NAME");
if(!tableNameField.getType().equals(String.class))
{
LOG.warn(warningPrefix + ", but whose TABLE_NAME field is not a String, so it will not be used.", logPair("class", recordEntityClass.getSimpleName()));
LOG.warn(warningPrefix + ", but whose TABLE_NAME field is not a String, so it will not be used.", logPair("class", aClass.getSimpleName()));
return (rs);
}
@ -305,7 +293,6 @@ public class MetaDataProducerHelper
}
RecordEntityToTableGenericMetaDataProducer producer = new RecordEntityToTableGenericMetaDataProducer(tableNameValue, recordEntityClass, tableMetaDataProductionCustomizer);
producer.setSourceClass(recordEntityClass);
if(tableMetaDataCustomizer != null)
{
@ -325,9 +312,7 @@ public class MetaDataProducerHelper
////////////////////////////////////////
if(qMetaDataProducingEntity.producePossibleValueSource())
{
PossibleValueSourceOfTableGenericMetaDataProducer producer = new PossibleValueSourceOfTableGenericMetaDataProducer(tableNameValue);
producer.setSourceClass(recordEntityClass);
rs.add(producer);
rs.add(new PossibleValueSourceOfTableGenericMetaDataProducer(tableNameValue));
}
//////////////////////////
@ -338,11 +323,11 @@ public class MetaDataProducerHelper
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
if(childTable.childJoin().enabled())
{
CollectionUtils.addIfNotNull(rs, processChildJoin(recordEntityClass, childTable));
CollectionUtils.addIfNotNull(rs, processChildJoin(aClass, childTable));
if(childTable.childRecordListWidget().enabled())
{
CollectionUtils.addIfNotNull(rs, processChildRecordListWidget(recordEntityClass, childTable));
CollectionUtils.addIfNotNull(rs, processChildRecordListWidget(aClass, childTable));
}
}
else
@ -352,7 +337,7 @@ public class MetaDataProducerHelper
//////////////////////////////////////////////////////////////////////////
// if not doing the join, can't do the child-widget, so warn about that //
//////////////////////////////////////////////////////////////////////////
LOG.warn(warningPrefix + " requested to produce a ChildRecordListWidget, but not produce a Join - which is not allowed (must do join to do widget). ", logPair("class", recordEntityClass.getSimpleName()), logPair("childEntityClass", childEntityClass.getSimpleName()));
LOG.warn(warningPrefix + " requested to produce a ChildRecordListWidget, but not produce a Join - which is not allowed (must do join to do widget). ", logPair("class", aClass.getSimpleName()), logPair("childEntityClass", childEntityClass.getSimpleName()));
}
}
}
@ -365,16 +350,14 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processChildRecordListWidget(Class<? extends QRecordEntity> sourceClass, ChildTable childTable) throws Exception
private static MetaDataProducerInterface<?> processChildRecordListWidget(Class<?> aClass, ChildTable childTable) throws Exception
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
String parentTableName = getTableNameStaticFieldValue(sourceClass);
String parentTableName = getTableNameStaticFieldValue(aClass);
String childTableName = getTableNameStaticFieldValue(childEntityClass);
ChildRecordListWidget childRecordListWidget = childTable.childRecordListWidget();
ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer producer = new ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, childRecordListWidget);
producer.setSourceClass(sourceClass);
return producer;
return (new ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, childRecordListWidget));
}
@ -404,22 +387,20 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processChildJoin(Class<? extends QRecordEntity> entityClass, ChildTable childTable) throws Exception
private static MetaDataProducerInterface<?> processChildJoin(Class<?> aClass, ChildTable childTable) throws Exception
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
String parentTableName = getTableNameStaticFieldValue(entityClass);
String parentTableName = getTableNameStaticFieldValue(aClass);
String childTableName = getTableNameStaticFieldValue(childEntityClass);
String possibleValueFieldName = findPossibleValueField(childEntityClass, parentTableName);
if(!StringUtils.hasContent(possibleValueFieldName))
{
LOG.warn("Could not find field in [" + childEntityClass.getSimpleName() + "] with possibleValueSource referencing table [" + entityClass.getSimpleName() + "]");
LOG.warn("Could not find field in [" + childEntityClass.getSimpleName() + "] with possibleValueSource referencing table [" + aClass.getSimpleName() + "]");
return (null);
}
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName);
producer.setSourceClass(entityClass);
return producer;
return (new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName));
}
@ -427,20 +408,18 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processMetaDataProducer(Class<?> sourceCClass) throws Exception
private static MetaDataProducerInterface<?> processMetaDataProducer(Class<?> aClass) throws Exception
{
for(Constructor<?> constructor : sourceCClass.getConstructors())
for(Constructor<?> constructor : aClass.getConstructors())
{
if(constructor.getParameterCount() == 0)
{
Object o = constructor.newInstance();
MetaDataProducerInterface<?> producer = (MetaDataProducerInterface<?>) o;
producer.setSourceClass(sourceCClass);
return producer;
return (MetaDataProducerInterface<?>) o;
}
}
LOG.warn("Found a class which implements MetaDataProducerInterface, but it does not have a no-arg constructor, so it cannot be used.", logPair("class", sourceCClass.getSimpleName()));
LOG.warn("Found a class which implements MetaDataProducerInterface, but it does not have a no-arg constructor, so it cannot be used.", logPair("class", aClass.getSimpleName()));
return null;
}

View File

@ -73,23 +73,4 @@ public interface MetaDataProducerInterface<T extends MetaDataProducerOutput>
return (true);
}
/***************************************************************************
*
***************************************************************************/
default void setSourceClass(Class<?> sourceClass)
{
//////////
// noop //
//////////
}
/***************************************************************************
**
***************************************************************************/
default Class<?> getSourceClass()
{
return null;
}
}

View File

@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.SourceQBitAware;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -32,12 +31,10 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
** Output object for a MetaDataProducer, which contains multiple meta-data
** objects.
*******************************************************************************/
public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, SourceQBitAware
public class MetaDataProducerMultiOutput implements MetaDataProducerOutput
{
private List<MetaDataProducerOutput> contents;
private String sourceQBitName;
/*******************************************************************************
@ -101,48 +98,4 @@ public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, Sour
return (rs);
}
/***************************************************************************
**
***************************************************************************/
@Override
public String getSourceQBitName()
{
return (this.sourceQBitName);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setSourceQBitName(String sourceQBitName)
{
this.sourceQBitName = sourceQBitName;
/////////////////////////////////////////////
// propagate the name down to the children //
/////////////////////////////////////////////
for(MetaDataProducerOutput content : contents)
{
if(content instanceof SourceQBitAware aware)
{
aware.setSourceQBitName(sourceQBitName);
}
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public MetaDataProducerMultiOutput withSourceQBitName(String sourceQBitName)
{
setSourceQBitName(sourceQBitName);
return this;
}
}

View File

@ -56,7 +56,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRule
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.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
@ -90,7 +89,6 @@ public class QInstance
////////////////////////////////////////////////////////////////////////////////////////////
// Important to use LinkedHashmap here, to preserve the order in which entries are added. //
////////////////////////////////////////////////////////////////////////////////////////////
private Map<String, QBitMetaData> qBits = new LinkedHashMap<>();
private Map<String, QTableMetaData> tables = new LinkedHashMap<>();
private Map<String, QJoinMetaData> joins = new LinkedHashMap<>();
private Map<String, QPossibleValueSource> possibleValueSources = new LinkedHashMap<>();
@ -1491,7 +1489,6 @@ public class QInstance
}
/*******************************************************************************
** Getter for metaDataFilter
*******************************************************************************/
@ -1522,68 +1519,4 @@ public class QInstance
}
/*******************************************************************************
**
*******************************************************************************/
public void addQBit(QBitMetaData qBitMetaData)
{
List<String> missingParts = new ArrayList<>();
if(!StringUtils.hasContent(qBitMetaData.getGroupId()))
{
missingParts.add("groupId");
}
if(!StringUtils.hasContent(qBitMetaData.getArtifactId()))
{
missingParts.add("artifactId");
}
if(!StringUtils.hasContent(qBitMetaData.getVersion()))
{
missingParts.add("version");
}
if(!missingParts.isEmpty())
{
throw (new IllegalArgumentException("Attempted to add a qBit without a " + StringUtils.joinWithCommasAndAnd(missingParts)));
}
String name = qBitMetaData.getName();
if(this.qBits.containsKey(name))
{
throw (new IllegalArgumentException("Attempted to add a second qBit with name (formed from 'groupId:artifactId:version[:namespace]'): " + name));
}
this.qBits.put(name, qBitMetaData);
}
/*******************************************************************************
** Getter for qBits
*******************************************************************************/
public Map<String, QBitMetaData> getQBits()
{
return (this.qBits);
}
/*******************************************************************************
** Setter for qBits
*******************************************************************************/
public void setQBits(Map<String, QBitMetaData> qBits)
{
this.qBits = qBits;
}
/*******************************************************************************
** Fluent setter for qBits
*******************************************************************************/
public QInstance withQBits(Map<String, QBitMetaData> qBits)
{
this.qBits = qBits;
return (this);
}
}

View File

@ -23,12 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
@ -46,21 +42,20 @@ public enum AdornmentType
REVEAL,
FILE_DOWNLOAD,
FILE_UPLOAD,
TOOLTIP,
ERROR;
//////////////////////////////////////////////////////////////////////////
// keep these values in sync with AdornmentType.ts in qqq-frontend-core //
//////////////////////////////////////////////////////////////////////////
/*******************************************************************************
**
*******************************************************************************/
public interface LinkValues
{
String TARGET = "target";
String TO_RECORD_FROM_TABLE = "toRecordFromTable";
String TO_RECORD_FROM_TABLE_DYNAMIC = "toRecordFromTableDynamic";
String TARGET = "target";
String TO_RECORD_FROM_TABLE = "toRecordFromTable";
}
@ -77,8 +72,6 @@ public enum AdornmentType
String SUPPLEMENTAL_PROCESS_NAME = "supplementalProcessName";
String SUPPLEMENTAL_CODE_REFERENCE = "supplementalCodeReference";
String DOWNLOAD_URL_DYNAMIC = "downloadUrlDynamic";
////////////////////////////////////////////////////
// use these two together, as in: //
// FILE_NAME_FORMAT = "Order %s Packing Slip.pdf" //
@ -86,17 +79,6 @@ public enum AdornmentType
////////////////////////////////////////////////////
String FILE_NAME_FORMAT = "fileNameFormat";
String FILE_NAME_FORMAT_FIELDS = "fileNameFormatFields";
/***************************************************************************
**
***************************************************************************/
static String makeFieldDownloadUrl(String tableName, Serializable primaryKey, String fieldName, String fileName)
{
return ("/data/" + tableName + "/"
+ URLEncoder.encode(Objects.requireNonNullElse(ValueUtils.getValueAsString(primaryKey), ""), StandardCharsets.UTF_8).replace("+", "%20") + "/"
+ fieldName + "/"
+ URLEncoder.encode(Objects.requireNonNullElse(fileName, ""), StandardCharsets.UTF_8).replace("+", "%20"));
}
}
@ -247,15 +229,4 @@ public enum AdornmentType
}
}
/*******************************************************************************
**
*******************************************************************************/
public interface TooltipValues
{
String STATIC_TEXT = "staticText";
String TOOLTIP_DYNAMIC = "tooltipDynamic";
}
}

View File

@ -37,7 +37,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
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.qbits.SourceQBitAware;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -47,14 +46,11 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** Meta-Data to define a process in a QQQ instance.
**
*******************************************************************************/
public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface, SourceQBitAware
public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface
{
private String name;
private String label;
private String tableName;
private String sourceQBitName;
private String name;
private String label;
private String tableName;
private boolean isHidden = false;
private BasepullConfiguration basepullConfiguration;
private QPermissionRules permissionRules;
@ -874,7 +870,6 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
}
/*******************************************************************************
** Getter for processTracerCodeReference
*******************************************************************************/
@ -905,37 +900,4 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
}
/*******************************************************************************
** Getter for sourceQBitName
*******************************************************************************/
@Override
public String getSourceQBitName()
{
return (this.sourceQBitName);
}
/*******************************************************************************
** Setter for sourceQBitName
*******************************************************************************/
@Override
public void setSourceQBitName(String sourceQBitName)
{
this.sourceQBitName = sourceQBitName;
}
/*******************************************************************************
** Fluent setter for sourceQBitName
*******************************************************************************/
@Override
public QProcessMetaData withSourceQBitName(String sourceQBitName)
{
this.sourceQBitName = sourceQBitName;
return (this);
}
}

View File

@ -62,7 +62,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
private String parentTableName; // e.g., order
private String foreignKeyFieldName; // e.g., orderId
private Class<?> sourceClass;
/***************************************************************************
@ -103,37 +102,4 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
return (join);
}
/*******************************************************************************
** Getter for sourceClass
**
*******************************************************************************/
public Class<?> getSourceClass()
{
return sourceClass;
}
/*******************************************************************************
** Setter for sourceClass
**
*******************************************************************************/
public void setSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
}
/*******************************************************************************
** Fluent setter for sourceClass
**
*******************************************************************************/
public ChildJoinFromRecordEntityGenericMetaDataProducer withSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
return (this);
}
}

View File

@ -57,8 +57,6 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
private ChildRecordListWidget childRecordListWidget;
private Class<?> sourceClass;
/***************************************************************************
@ -113,36 +111,4 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
return (widget);
}
/*******************************************************************************
** Getter for sourceClass
**
*******************************************************************************/
public Class<?> getSourceClass()
{
return sourceClass;
}
/*******************************************************************************
** Setter for sourceClass
**
*******************************************************************************/
public void setSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
}
/*******************************************************************************
** Fluent setter for sourceClass
**
*******************************************************************************/
public ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer withSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
return (this);
}
}

View File

@ -40,10 +40,6 @@ public class PossibleValueSourceOfEnumGenericMetaDataProducer<T extends Serializ
private final String name;
private final PossibleValueEnum<T>[] values;
private Class<?> sourceClass;
/*******************************************************************************
@ -66,37 +62,4 @@ public class PossibleValueSourceOfEnumGenericMetaDataProducer<T extends Serializ
{
return (QPossibleValueSource.newForEnum(name, values));
}
/*******************************************************************************
** Getter for sourceClass
**
*******************************************************************************/
public Class<?> getSourceClass()
{
return sourceClass;
}
/*******************************************************************************
** Setter for sourceClass
**
*******************************************************************************/
public void setSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
}
/*******************************************************************************
** Fluent setter for sourceClass
**
*******************************************************************************/
public PossibleValueSourceOfEnumGenericMetaDataProducer<T> withSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
return (this);
}
}

View File

@ -37,7 +37,6 @@ public class PossibleValueSourceOfTableGenericMetaDataProducer implements MetaDa
{
private final String tableName;
private Class<?> sourceClass;
/*******************************************************************************
@ -59,38 +58,4 @@ public class PossibleValueSourceOfTableGenericMetaDataProducer implements MetaDa
{
return (QPossibleValueSource.newForTable(tableName));
}
/*******************************************************************************
** Getter for sourceClass
**
*******************************************************************************/
public Class<?> getSourceClass()
{
return sourceClass;
}
/*******************************************************************************
** Setter for sourceClass
**
*******************************************************************************/
public void setSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
}
/*******************************************************************************
** Fluent setter for sourceClass
**
*******************************************************************************/
public PossibleValueSourceOfTableGenericMetaDataProducer withSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
return (this);
}
}

View File

@ -48,7 +48,6 @@ public class RecordEntityToTableGenericMetaDataProducer implements MetaDataProdu
private static MetaDataCustomizerInterface<QTableMetaData> defaultMetaDataCustomizer = null;
private Class<?> sourceClass;
/*******************************************************************************
@ -155,37 +154,4 @@ public class RecordEntityToTableGenericMetaDataProducer implements MetaDataProdu
RecordEntityToTableGenericMetaDataProducer.defaultMetaDataCustomizer = defaultMetaDataCustomizer;
}
/*******************************************************************************
** Getter for sourceClass
**
*******************************************************************************/
public Class<?> getSourceClass()
{
return sourceClass;
}
/*******************************************************************************
** Setter for sourceClass
**
*******************************************************************************/
public void setSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
}
/*******************************************************************************
** Fluent setter for sourceClass
**
*******************************************************************************/
public RecordEntityToTableGenericMetaDataProducer withSourceClass(Class<?> sourceClass)
{
this.sourceClass = sourceClass;
return (this);
}
}

View File

@ -1,122 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
/***************************************************************************
** Common (maybe)? qbit config pattern, where the qbit may be able to provide
** a particular table, or, the application may supply it itself.
**
** If the qbit provides it, then we need to be told (by the application)
** what backendName to use for the table.
**
** Else if the application supplies it, it needs to tell the qBit what the
** tableName is.
***************************************************************************/
public class ProvidedOrSuppliedTableConfig
{
private boolean doProvideTable;
private String backendName;
private String tableName;
/***************************************************************************
**
***************************************************************************/
public ProvidedOrSuppliedTableConfig(boolean doProvideTable, String backendName, String tableName)
{
this.doProvideTable = doProvideTable;
this.backendName = backendName;
this.tableName = tableName;
}
/***************************************************************************
**
***************************************************************************/
public static ProvidedOrSuppliedTableConfig provideTableUsingBackendNamed(String backendName)
{
return (new ProvidedOrSuppliedTableConfig(true, backendName, null));
}
/***************************************************************************
**
***************************************************************************/
public static ProvidedOrSuppliedTableConfig useSuppliedTaleNamed(String tableName)
{
return (new ProvidedOrSuppliedTableConfig(false, null, tableName));
}
/***************************************************************************
**
***************************************************************************/
public String getEffectiveTableName(String tableNameIfProviding)
{
if (getDoProvideTable())
{
return tableNameIfProviding;
}
else
{
return getTableName();
}
}
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Getter for doProvideTable
**
*******************************************************************************/
public boolean getDoProvideTable()
{
return doProvideTable;
}
/*******************************************************************************
** Getter for backendName
**
*******************************************************************************/
public String getBackendName()
{
return backendName;
}
}

View File

@ -1,71 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
/*******************************************************************************
** extension of MetaDataProducerInterface, designed for producing meta data
** within a (java-defined, at this time) QBit.
**
** Specifically exists to accept the QBitConfig as a type parameter and a value,
** easily accessed in the producer's methods as getQBitConfig()
*******************************************************************************/
public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOutput, C extends QBitConfig> implements MetaDataProducerInterface<T>
{
private C qBitConfig = null;
/*******************************************************************************
** Getter for qBitConfig
*******************************************************************************/
public C getQBitConfig()
{
return (this.qBitConfig);
}
/*******************************************************************************
** Setter for qBitConfig
*******************************************************************************/
public void setQBitConfig(C qBitConfig)
{
this.qBitConfig = qBitConfig;
}
/*******************************************************************************
** Fluent setter for qBitConfig
*******************************************************************************/
public QBitComponentMetaDataProducer<T, C> withQBitConfig(C qBitConfig)
{
this.qBitConfig = qBitConfig;
return (this);
}
}

View File

@ -1,110 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
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.producers.MetaDataCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Interface for configuration settings used both in the production of meta-data
** for a QBit, but also at runtime, e.g., to be aware of exactly how the qbit
** has been incorporated into an application.
**
** For example:
** - should the QBit define certain tables, or will they be supplied by the application?
** - what other meta-data names should the qbit reference (backends, schedulers)
** - what meta-data-customizer(s) should be used?
**
** When implementing a QBit, you'll implement this interface - adding whatever
** (if any) properties you need, and if you have any rules, then overriding
** the validate method (ideally the one that takes the List-of-String errors)
**
** When using a QBit, you'll create an instance of the QBit's config object,
** and pass it through to the QBit producer.
*******************************************************************************/
public interface QBitConfig extends Serializable
{
QLogger LOG = QLogger.getLogger(QBitConfig.class);
/***************************************************************************
**
***************************************************************************/
default void validate(QInstance qInstance) throws QBitConfigValidationException
{
List<String> errors = new ArrayList<>();
try
{
validate(qInstance, errors);
}
catch(Exception e)
{
LOG.warn("Error validating QBitConfig: " + this.getClass().getName(), e);
}
if(!errors.isEmpty())
{
throw (new QBitConfigValidationException(this, errors));
}
}
/***************************************************************************
**
***************************************************************************/
default void validate(QInstance qInstance, List<String> errors)
{
/////////////////////////////////////
// nothing to validate by default! //
/////////////////////////////////////
}
/***************************************************************************
**
***************************************************************************/
default boolean assertCondition(boolean condition, String message, List<String> errors)
{
if(!condition)
{
errors.add(message);
}
return (condition);
}
/***************************************************************************
**
***************************************************************************/
default MetaDataCustomizerInterface<QTableMetaData> getTableMetaDataCustomizer()
{
return (null);
}
}

View File

@ -1,44 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** thrown by QBitConfig.validate() if there's an issue.
*******************************************************************************/
public class QBitConfigValidationException extends QException
{
/***************************************************************************
**
***************************************************************************/
public QBitConfigValidationException(QBitConfig qBitConfig, List<String> errors)
{
super("Validation failed for QBitConfig: " + qBitConfig.getClass().getName() + ":\n" + StringUtils.join("\n", errors));
}
}

View File

@ -1,237 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Meta-data to define an active QBit in a QQQ Instance.
**
** The unique "name" for the QBit is composed of its groupId and artifactId
** (maven style). There is also a version - but it is not part of the unique
** name. But - there is also a namespace attribute, which IS part of the
** unique name. This will (eventually?) allow us to have multiple instances
** of the same qbit in a qInstance at the same time (e.g., 2 versions of some
** table, which should be namespace-prefixed);
**
** QBitMetaData also retains the QBitConfig that was used to produce the QBit.
**
** Some meta-data objects are aware of the fact that they may have come from a
** QBit - see SourceQBitAware interface. These objects can get their source
** QBitMetaData (this object) and its config,via that interface.
*******************************************************************************/
public class QBitMetaData implements TopLevelMetaDataInterface
{
private String groupId;
private String artifactId;
private String version;
private String namespace;
private QBitConfig config;
/***************************************************************************
**
***************************************************************************/
@Override
public String getName()
{
String name = groupId + ":" + artifactId;
if(StringUtils.hasContent(namespace))
{
name += ":" + namespace;
}
return name;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void addSelfToInstance(QInstance qInstance)
{
qInstance.addQBit(this);
}
/*******************************************************************************
** Getter for config
*******************************************************************************/
public QBitConfig getConfig()
{
return (this.config);
}
/*******************************************************************************
** Setter for config
*******************************************************************************/
public void setConfig(QBitConfig config)
{
this.config = config;
}
/*******************************************************************************
** Fluent setter for config
*******************************************************************************/
public QBitMetaData withConfig(QBitConfig config)
{
this.config = config;
return (this);
}
/*******************************************************************************
** Getter for groupId
*******************************************************************************/
public String getGroupId()
{
return (this.groupId);
}
/*******************************************************************************
** Setter for groupId
*******************************************************************************/
public void setGroupId(String groupId)
{
this.groupId = groupId;
}
/*******************************************************************************
** Fluent setter for groupId
*******************************************************************************/
public QBitMetaData withGroupId(String groupId)
{
this.groupId = groupId;
return (this);
}
/*******************************************************************************
** Getter for artifactId
*******************************************************************************/
public String getArtifactId()
{
return (this.artifactId);
}
/*******************************************************************************
** Setter for artifactId
*******************************************************************************/
public void setArtifactId(String artifactId)
{
this.artifactId = artifactId;
}
/*******************************************************************************
** Fluent setter for artifactId
*******************************************************************************/
public QBitMetaData withArtifactId(String artifactId)
{
this.artifactId = artifactId;
return (this);
}
/*******************************************************************************
** Getter for version
*******************************************************************************/
public String getVersion()
{
return (this.version);
}
/*******************************************************************************
** Setter for version
*******************************************************************************/
public void setVersion(String version)
{
this.version = version;
}
/*******************************************************************************
** Fluent setter for version
*******************************************************************************/
public QBitMetaData withVersion(String version)
{
this.version = version;
return (this);
}
/*******************************************************************************
** Getter for namespace
*******************************************************************************/
public String getNamespace()
{
return (this.namespace);
}
/*******************************************************************************
** Setter for namespace
*******************************************************************************/
public void setNamespace(String namespace)
{
this.namespace = namespace;
}
/*******************************************************************************
** Fluent setter for namespace
*******************************************************************************/
public QBitMetaData withNamespace(String namespace)
{
this.namespace = namespace;
return (this);
}
}

View File

@ -1,117 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** interface for how a QBit's meta-data gets produced and added to a QInstance.
**
** When implementing a QBit, you'll implement this interface:
** - adding a QBitConfig subclass as a property
** - overriding the produce(qInstance, namespace) method - where you'll:
** -- create and add your QBitMetaData
** -- call MetaDataProducerHelper.findProducers
** -- hand off to finishProducing() in this interface
**
** When using a QBit, you'll create an instance of the QBit's config object,
** pass it in to the producer, then call produce, ala:
**
** new SomeQBitProducer()
** .withQBitConfig(someQBitConfig)
** .produce(qInstance);
**
*******************************************************************************/
public interface QBitProducer
{
QLogger LOG = QLogger.getLogger(QBitProducer.class);
/***************************************************************************
**
***************************************************************************/
default void produce(QInstance qInstance) throws QException
{
produce(qInstance, null);
}
/***************************************************************************
**
***************************************************************************/
void produce(QInstance qInstance, String namespace) throws QException;
/***************************************************************************
*
***************************************************************************/
default <C extends QBitConfig> void finishProducing(QInstance qInstance, QBitMetaData qBitMetaData, C qBitConfig, List<MetaDataProducerInterface<?>> producers) throws QException
{
qBitConfig.validate(qInstance);
///////////////////////////////
// todo - move to base class //
///////////////////////////////
for(MetaDataProducerInterface<?> producer : producers)
{
if(producer instanceof QBitComponentMetaDataProducer<?, ?>)
{
QBitComponentMetaDataProducer<?, C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducer<?, C>) producer;
qBitComponentMetaDataProducer.setQBitConfig(qBitConfig);
}
if(!producer.isEnabled())
{
LOG.debug("Not using producer which is not enabled", logPair("producer", producer.getClass().getSimpleName()));
continue;
}
MetaDataProducerOutput output = producer.produce(qInstance);
/////////////////////////////////////////
// apply table customizer, if provided //
/////////////////////////////////////////
if(qBitConfig.getTableMetaDataCustomizer() != null && output instanceof QTableMetaData table)
{
output = qBitConfig.getTableMetaDataCustomizer().customizeMetaData(qInstance, table);
}
/////////////////////////////////////////////////
// set source qbit, if output is aware of such //
/////////////////////////////////////////////////
if(output instanceof SourceQBitAware sourceQBitAware)
{
sourceQBitAware.setSourceQBitName(qBitMetaData.getName());
}
output.addSelfToInstance(qInstance);
}
}
}

View File

@ -1,77 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import com.kingsrook.qqq.backend.core.context.QContext;
/*******************************************************************************
** interface for meta data objects that may have come from a qbit, and where we
** might want to get data about that qbit (e.g., config or meta-data).
*******************************************************************************/
public interface SourceQBitAware
{
/*******************************************************************************
** Getter for sourceQBitName
*******************************************************************************/
String getSourceQBitName();
/*******************************************************************************
** Setter for sourceQBitName
*******************************************************************************/
void setSourceQBitName(String sourceQBitName);
/*******************************************************************************
** Fluent setter for sourceQBitName
*******************************************************************************/
Object withSourceQBitName(String sourceQBitName);
/***************************************************************************
**
***************************************************************************/
default QBitMetaData getSourceQBit()
{
String qbitName = getSourceQBitName();
return (QContext.getQInstance().getQBits().get(qbitName));
}
/***************************************************************************
**
***************************************************************************/
default QBitConfig getSourceQBitConfig()
{
QBitMetaData sourceQBit = getSourceQBit();
if(sourceQBit == null)
{
return null;
}
else
{
return sourceQBit.getConfig();
}
}
}

View File

@ -50,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
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.qbits.SourceQBitAware;
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;
@ -63,7 +62,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
** Meta-Data to define a table in a QQQ instance.
**
*******************************************************************************/
public class QTableMetaData implements QAppChildMetaData, Serializable, MetaDataWithPermissionRules, TopLevelMetaDataInterface, SourceQBitAware
public class QTableMetaData implements QAppChildMetaData, Serializable, MetaDataWithPermissionRules, TopLevelMetaDataInterface
{
private static final QLogger LOG = QLogger.getLogger(QTableMetaData.class);
@ -74,8 +73,6 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private String primaryKeyField;
private boolean isHidden = false;
private String sourceQBitName;
private Map<String, QFieldMetaData> fields;
private List<UniqueKey> uniqueKeys;
private List<Association> associations;
@ -1059,7 +1056,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
{
for(Capability disabledCapability : disabledCapabilities)
{
withoutCapability(disabledCapability);
withCapability(disabledCapability);
}
return (this);
}
@ -1557,38 +1554,4 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
/*******************************************************************************
** Getter for sourceQBitName
*******************************************************************************/
@Override
public String getSourceQBitName()
{
return (this.sourceQBitName);
}
/*******************************************************************************
** Setter for sourceQBitName
*******************************************************************************/
@Override
public void setSourceQBitName(String sourceQBitName)
{
this.sourceQBitName = sourceQBitName;
}
/*******************************************************************************
** Fluent setter for sourceQBitName
*******************************************************************************/
@Override
public QTableMetaData withSourceQBitName(String sourceQBitName)
{
this.sourceQBitName = sourceQBitName;
return (this);
}
}

View File

@ -1,89 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
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.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** possible-value source provider for the `Tables` PVS - a list of all tables
** in an application/qInstance.
*******************************************************************************/
public class TablesCustomPossibleValueProvider implements QCustomPossibleValueProvider<String>
{
/***************************************************************************
**
***************************************************************************/
@Override
public QPossibleValue<String> getPossibleValue(Serializable idValue)
{
QTableMetaData table = QContext.getQInstance().getTable(ValueUtils.getValueAsString(idValue));
if(table != null && !table.getIsHidden())
{
PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table);
if(PermissionCheckResult.ALLOW.equals(permissionCheckResult))
{
return (new QPossibleValue<>(table.getName(), table.getLabel()));
}
}
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////
// build all of the possible values (note, will be filtered by user's permissions) //
/////////////////////////////////////////////////////////////////////////////////////
List<QPossibleValue<String>> allPossibleValues = new ArrayList<>();
for(QTableMetaData table : QContext.getQInstance().getTables().values())
{
QPossibleValue<String> possibleValue = getPossibleValue(table.getName());
if(possibleValue != null)
{
allPossibleValues.add(possibleValue);
}
}
return completeCustomPVSSearch(input, allPossibleValues);
}
}

View File

@ -22,11 +22,17 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
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.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
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.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
@ -45,10 +51,22 @@ public class TablesPossibleValueSourceMetaDataProvider
{
QPossibleValueSource possibleValueSource = new QPossibleValueSource()
.withName(NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(TablesCustomPossibleValueProvider.class))
.withType(QPossibleValueSourceType.ENUM)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
List<QPossibleValue<?>> enumValues = new ArrayList<>();
for(QTableMetaData table : qInstance.getTables().values())
{
if(BooleanUtils.isNotTrue(table.getIsHidden()))
{
String label = StringUtils.hasContent(table.getLabel()) ? table.getLabel() : QInstanceEnricher.nameToLabel(table.getName());
enumValues.add(new QPossibleValue<>(table.getName(), label));
}
}
enumValues.sort(Comparator.comparing(QPossibleValue::getLabel));
possibleValueSource.withEnumValues(enumValues);
return (possibleValueSource);
}

View File

@ -56,16 +56,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Note - exists under 2 names, for the RenderSavedReport process, and for the
** ScheduledReport table (and can be used in your custom code too:
*
** by default, in qqq backend core, we'll assume this widget is being used on the
** view screen for a ScheduledReport, with field names that we know from that table.
** But, allow it to be used on a different table (optionally with different field names),
** coming from the input map.
**
** e.g., that one may set in widget metaData as:
** .withDefaultValue("tableName", "myTable")
** .withDefaultValue("fieldNameId", "identifier"), etc.
** ScheduledReport table
*******************************************************************************/
public class ReportValuesDynamicFormWidgetRenderer extends AbstractWidgetRenderer
{
@ -97,16 +88,11 @@ public class ReportValuesDynamicFormWidgetRenderer extends AbstractWidgetRendere
}
else if(input.getQueryParams().containsKey("id"))
{
String tableName = input.getQueryParams().getOrDefault("tableName", ScheduledReport.TABLE_NAME);
String fieldNameId = input.getQueryParams().getOrDefault("fieldNameId", "id");
String fieldNameSavedReportId = input.getQueryParams().getOrDefault("fieldNameSavedReportId", "savedReportId");
String fieldNameInputValues = input.getQueryParams().getOrDefault("fieldNameInputValues", "inputValues");
QRecord hostRecord = new GetAction().executeForRecord(new GetInput(tableName).withPrimaryKey(ValueUtils.getValueAsInteger(input.getQueryParams().get(fieldNameId))));
QRecord record = new GetAction().executeForRecord(new GetInput(SavedReport.TABLE_NAME).withPrimaryKey(ValueUtils.getValueAsInteger(hostRecord.getValueInteger(fieldNameSavedReportId))));
QRecord scheduledReportRecord = new GetAction().executeForRecord(new GetInput(ScheduledReport.TABLE_NAME).withPrimaryKey(ValueUtils.getValueAsInteger(input.getQueryParams().get("id"))));
QRecord record = new GetAction().executeForRecord(new GetInput(SavedReport.TABLE_NAME).withPrimaryKey(ValueUtils.getValueAsInteger(scheduledReportRecord.getValueInteger("savedReportId"))));
savedReport = new SavedReport(record);
String inputValues = hostRecord.getValueString(fieldNameInputValues);
String inputValues = scheduledReportRecord.getValueString("inputValues");
if(StringUtils.hasContent(inputValues))
{
JSONObject jsonObject = JsonUtils.toJSONObject(inputValues);
@ -211,8 +197,8 @@ public class ReportValuesDynamicFormWidgetRenderer extends AbstractWidgetRendere
}
catch(Exception e)
{
LOG.warn("Error rendering report values dynamic form widget", e, logPair("queryParams", String.valueOf(input.getQueryParams())));
throw (new QException("Error rendering report values dynamic form widget", e));
LOG.warn("Error rendering scheduled report values dynamic form widget", e, logPair("queryParams", String.valueOf(input.getQueryParams())));
throw (new QException("Error rendering scheduled report values dynamic form widget", e));
}
}

View File

@ -50,6 +50,7 @@ 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;
@ -350,7 +351,8 @@ public class SavedReportsMetaDataProvider
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "savedReportId", "renderedReportStatusId")))
.withSection(new QFieldSection("input", new QIcon().withName("input"), Tier.T2, List.of("userId", "reportFormat")))
.withSection(new QFieldSection("output", new QIcon().withName("output"), Tier.T2, List.of("jobUuid", "resultPath", "rowCount", "errorMessage", "startTime", "endTime")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
.withoutCapabilities(Capability.allWriteCapabilities());
table.getField("renderedReportStatusId").setAdornments(List.of(new FieldAdornment(AdornmentType.CHIP)
.withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.RUNNING.getId(), "pending", AdornmentType.ChipValues.COLOR_SECONDARY))

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -111,10 +110,6 @@ public class BulkInsertLoadStep extends LoadViaInsertStep implements ProcessSumm
if(field.getType().isNumeric())
{
ProcessSummaryLine idsLine = new ProcessSummaryLine(Status.INFO, "Inserted " + field.getLabel() + " values between " + firstInsertedPrimaryKey + " and " + lastInsertedPrimaryKey);
if(Objects.equals(firstInsertedPrimaryKey, lastInsertedPrimaryKey))
{
idsLine.setMessage("Inserted " + field.getLabel() + " " + firstInsertedPrimaryKey);
}
idsLine.setCount(null);
processSummary.add(idsLine);
}

View File

@ -92,21 +92,16 @@ public class RenderSavedReportExecuteStep implements BackendStep
////////////////////////////////
// read inputs, set up params //
////////////////////////////////
String sesProviderName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.SES_PROVIDER_NAME);
String fromEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FROM_EMAIL_ADDRESS);
String replyToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.REPLY_TO_EMAIL_ADDRESS);
String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
String sendToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_ADDRESS);
String emailSubject = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_SUBJECT);
SavedReport savedReport = new SavedReport(runBackendStepInput.getRecords().get(0));
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
String storageReference = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_REFERENCE);
if(!StringUtils.hasContent(storageReference))
{
storageReference = LocalDate.now() + "/" + LocalTime.now().toString().replaceAll(":", "").replaceFirst("\\..*", "") + "/" + UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension();
}
String sesProviderName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.SES_PROVIDER_NAME);
String fromEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FROM_EMAIL_ADDRESS);
String replyToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.REPLY_TO_EMAIL_ADDRESS);
String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
String sendToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_ADDRESS);
String emailSubject = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_SUBJECT);
SavedReport savedReport = new SavedReport(runBackendStepInput.getRecords().get(0));
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
String storageReference = LocalDate.now() + "/" + LocalTime.now().toString().replaceAll(":", "").replaceFirst("\\..*", "") + "/" + UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if sending an email (or emails), validate the addresses before doing anything so user gets error and can fix //
@ -246,7 +241,7 @@ public class RenderSavedReportExecuteStep implements BackendStep
/*******************************************************************************
**
*******************************************************************************/
public static String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, SavedReport report)
private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, SavedReport report)
{
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault());
String datePart = formatter.format(Instant.now());

View File

@ -56,7 +56,6 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
public static final String FROM_EMAIL_ADDRESS = "fromEmailAddress";
public static final String REPLY_TO_EMAIL_ADDRESS = "replyToEmailAddress";
public static final String FIELD_NAME_STORAGE_TABLE_NAME = "storageTableName";
public static final String FIELD_NAME_STORAGE_REFERENCE = "storageReference";
public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat";
public static final String FIELD_NAME_EMAIL_ADDRESS = "reportDestinationEmailAddress";
public static final String FIELD_NAME_EMAIL_SUBJECT = "emailSubject";
@ -82,7 +81,6 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
.withField(new QFieldMetaData(FROM_EMAIL_ADDRESS, QFieldType.STRING))
.withField(new QFieldMetaData(REPLY_TO_EMAIL_ADDRESS, QFieldType.STRING))
.withField(new QFieldMetaData(FIELD_NAME_STORAGE_TABLE_NAME, QFieldType.STRING))
.withField(new QFieldMetaData(FIELD_NAME_STORAGE_REFERENCE, QFieldType.STRING))
.withRecordListMetaData(new QRecordListMetaData().withTableName(SavedReport.TABLE_NAME)))
.withCode(new QCodeReference(RenderSavedReportPreStep.class)))

View File

@ -173,21 +173,8 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
*******************************************************************************/
protected QQueryFilter getExistingRecordQueryFilter(RunBackendStepInput runBackendStepInput, List<Serializable> sourceKeyList)
{
String destinationTableForeignKeyFieldName = getSyncProcessConfig().destinationTableForeignKey;
String destinationTableName = getSyncProcessConfig().destinationTable;
QFieldMetaData destinationForeignKeyField = QContext.getQInstance().getTable(destinationTableName).getField(destinationTableForeignKeyFieldName);
List<Serializable> sourceKeysInDestinationKeyTypeList = null;
if(sourceKeyList != null)
{
sourceKeysInDestinationKeyTypeList = new ArrayList<>();
for(Serializable sourceKey : sourceKeyList)
{
sourceKeysInDestinationKeyTypeList.add(ValueUtils.getValueAsFieldType(destinationForeignKeyField.getType(), sourceKey));
}
}
return new QQueryFilter().withCriteria(new QFilterCriteria(destinationTableForeignKeyFieldName, QCriteriaOperator.IN, sourceKeysInDestinationKeyTypeList));
String destinationTableForeignKeyField = getSyncProcessConfig().destinationTableForeignKey;
return new QQueryFilter().withCriteria(new QFilterCriteria(destinationTableForeignKeyField, QCriteriaOperator.IN, sourceKeyList));
}

View File

@ -1,343 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** class to give a human-friendly descriptive string from a cron expression.
** (written in half by my friend Mr. Chatty G)
*******************************************************************************/
public class CronDescriber
{
private static final Map<String, String> DAY_OF_WEEK_MAP = new HashMap<>();
private static final Map<String, String> MONTH_MAP = new HashMap<>();
static
{
DAY_OF_WEEK_MAP.put("1", "Sunday");
DAY_OF_WEEK_MAP.put("2", "Monday");
DAY_OF_WEEK_MAP.put("3", "Tuesday");
DAY_OF_WEEK_MAP.put("4", "Wednesday");
DAY_OF_WEEK_MAP.put("5", "Thursday");
DAY_OF_WEEK_MAP.put("6", "Friday");
DAY_OF_WEEK_MAP.put("7", "Saturday");
////////////////////////////////
// Quartz also allows SUN-SAT //
////////////////////////////////
DAY_OF_WEEK_MAP.put("SUN", "Sunday");
DAY_OF_WEEK_MAP.put("MON", "Monday");
DAY_OF_WEEK_MAP.put("TUE", "Tuesday");
DAY_OF_WEEK_MAP.put("WED", "Wednesday");
DAY_OF_WEEK_MAP.put("THU", "Thursday");
DAY_OF_WEEK_MAP.put("FRI", "Friday");
DAY_OF_WEEK_MAP.put("SAT", "Saturday");
MONTH_MAP.put("1", "January");
MONTH_MAP.put("2", "February");
MONTH_MAP.put("3", "March");
MONTH_MAP.put("4", "April");
MONTH_MAP.put("5", "May");
MONTH_MAP.put("6", "June");
MONTH_MAP.put("7", "July");
MONTH_MAP.put("8", "August");
MONTH_MAP.put("9", "September");
MONTH_MAP.put("10", "October");
MONTH_MAP.put("11", "November");
MONTH_MAP.put("12", "December");
////////////////////////////////
// Quartz also allows JAN-DEC //
////////////////////////////////
MONTH_MAP.put("JAN", "January");
MONTH_MAP.put("FEB", "February");
MONTH_MAP.put("MAR", "March");
MONTH_MAP.put("APR", "April");
MONTH_MAP.put("MAY", "May");
MONTH_MAP.put("JUN", "June");
MONTH_MAP.put("JUL", "July");
MONTH_MAP.put("AUG", "August");
MONTH_MAP.put("SEP", "September");
MONTH_MAP.put("OCT", "October");
MONTH_MAP.put("NOV", "November");
MONTH_MAP.put("DEC", "December");
}
/***************************************************************************
**
***************************************************************************/
public static String getDescription(String cronExpression) throws ParseException
{
String[] parts = cronExpression.trim().toUpperCase().split("\\s+");
if(parts.length < 6 || parts.length > 7)
{
throw new ParseException("Invalid cron expression: " + cronExpression, 0);
}
String seconds = parts[0];
String minutes = parts[1];
String hours = parts[2];
String dayOfMonth = parts[3];
String month = parts[4];
String dayOfWeek = parts[5];
String year = parts.length == 7 ? parts[6] : "*";
StringBuilder description = new StringBuilder();
description.append("At ");
description.append(describeTime(seconds, minutes, hours));
description.append(", on ");
description.append(describeDayOfMonth(dayOfMonth));
description.append(" of ");
description.append(describeMonth(month));
description.append(", ");
description.append(describeDayOfWeek(dayOfWeek));
if(!year.equals("*"))
{
description.append(", in ").append(year);
}
description.append(".");
return description.toString();
}
/***************************************************************************
**
***************************************************************************/
private static String describeTime(String seconds, String minutes, String hours)
{
return String.format("%s, %s, %s", describePart(seconds, "second"), describePart(minutes, "minute"), describePart(hours, "hour"));
}
/***************************************************************************
**
***************************************************************************/
private static String describeDayOfMonth(String dayOfMonth)
{
if(dayOfMonth.equals("?"))
{
return "every day";
}
else if(dayOfMonth.equals("L"))
{
return "the last day";
}
else if(dayOfMonth.contains("W"))
{
return "the nearest weekday to day " + dayOfMonth.replace("W", "");
}
else
{
return (describePart(dayOfMonth, "day"));
}
}
/***************************************************************************
**
***************************************************************************/
private static String describeMonth(String month)
{
if(month.equals("*"))
{
return "every month";
}
else if(month.contains("-"))
{
String[] parts = month.split("-");
return String.format("%s to %s", MONTH_MAP.getOrDefault(parts[0], parts[0]), MONTH_MAP.getOrDefault(parts[1], parts[1]));
}
else
{
String[] months = month.split(",");
List<String> monthNames = Arrays.stream(months).map(m -> MONTH_MAP.getOrDefault(m, m)).toList();
return StringUtils.joinWithCommasAndAnd(monthNames);
}
}
/***************************************************************************
**
***************************************************************************/
private static String describeDayOfWeek(String dayOfWeek)
{
if(dayOfWeek.equals("?") || dayOfWeek.equals("*"))
{
return "every day of the week";
}
else if(dayOfWeek.equals("L"))
{
return "the last day of the week";
}
else if(dayOfWeek.contains("#"))
{
String[] parts = dayOfWeek.split("#");
return String.format("the %s %s of the month", ordinal(parts[1]), DAY_OF_WEEK_MAP.getOrDefault(parts[0], parts[0]));
}
else if(dayOfWeek.contains("-"))
{
String[] parts = dayOfWeek.split("-");
return String.format("from %s to %s", DAY_OF_WEEK_MAP.getOrDefault(parts[0], parts[0]), DAY_OF_WEEK_MAP.getOrDefault(parts[1], parts[1]));
}
else
{
String[] days = dayOfWeek.split(",");
List<String> dayNames = Arrays.stream(days).map(d -> DAY_OF_WEEK_MAP.getOrDefault(d, d)).toList();
return StringUtils.joinWithCommasAndAnd(dayNames);
}
}
/***************************************************************************
**
***************************************************************************/
private static String describePart(String part, String label)
{
if(part.equals("*"))
{
return "every " + label;
}
else if(part.contains("/"))
{
String[] parts = part.split("/");
if(parts[0].equals("*"))
{
parts[0] = "0";
}
return String.format("every %s " + label + "s starting at %s", parts[1], parts[0]);
}
else if(part.contains(","))
{
List<String> partsList = Arrays.stream(part.split(",")).toList();
if(label.equals("hour"))
{
List<String> hourNames = partsList.stream().map(p -> hourToAmPm(p)).toList();
return StringUtils.joinWithCommasAndAnd(hourNames);
}
else
{
if(label.equals("day"))
{
return "days " + StringUtils.joinWithCommasAndAnd(partsList);
}
else
{
return StringUtils.joinWithCommasAndAnd(partsList) + " " + label + "s";
}
}
}
else if(part.contains("-"))
{
String[] parts = part.split("-");
if(label.equals("day"))
{
return String.format("%ss from %s to %s", label, parts[0], parts[1]);
}
else if(label.equals("hour"))
{
return String.format("from %s to %s", hourToAmPm(parts[0]), hourToAmPm(parts[1]));
}
else
{
return String.format("from %s to %s %s", parts[0], parts[1], label + "s");
}
}
else
{
if(label.equals("day"))
{
return label + " " + part;
}
if(label.equals("hour"))
{
return hourToAmPm(part);
}
else
{
return part + " " + label + "s";
}
}
}
/***************************************************************************
**
***************************************************************************/
private static String hourToAmPm(String part)
{
try
{
int hour = Integer.parseInt(part);
return switch(hour)
{
case 0 -> "midnight";
case 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 -> hour + " AM";
case 12 -> "noon";
case 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 -> (hour - 12) + " PM";
default -> hour + " hours";
};
}
catch(Exception e)
{
return part + " hours";
}
}
/***************************************************************************
**
***************************************************************************/
private static String ordinal(String number)
{
int n = Integer.parseInt(number);
if(n >= 11 && n <= 13)
{
return n + "th";
}
return switch(n % 10)
{
case 1 -> n + "st";
case 2 -> n + "nd";
case 3 -> n + "rd";
default -> n + "th";
};
}
}

View File

@ -1,90 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Field display behavior, to add a human-redable tooltip to cron-expressions.
*******************************************************************************/
public class CronExpressionTooltipFieldBehavior implements FieldDisplayBehavior<CronExpressionTooltipFieldBehavior>
{
/***************************************************************************
** Add both this behavior, and the tooltip adornment to a field
** Note, if either was already there, then that part is left alone.
***************************************************************************/
public static void addToField(QFieldMetaData fieldMetaData)
{
CronExpressionTooltipFieldBehavior existingBehavior = fieldMetaData.getBehaviorOnlyIfSet(CronExpressionTooltipFieldBehavior.class);
if(existingBehavior == null)
{
fieldMetaData.withBehavior(new CronExpressionTooltipFieldBehavior());
}
if(fieldMetaData.getAdornment(AdornmentType.TOOLTIP).isEmpty())
{
fieldMetaData.withFieldAdornment((new FieldAdornment(AdornmentType.TOOLTIP)
.withValue(AdornmentType.TooltipValues.TOOLTIP_DYNAMIC, true)));
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : recordList)
{
try
{
String cronExpression = record.getValueString(field.getName());
if(StringUtils.hasContent(cronExpression))
{
String description = CronDescriber.getDescription(cronExpression);
record.setDisplayValue(field.getName() + ":" + AdornmentType.TooltipValues.TOOLTIP_DYNAMIC, description);
}
}
catch(Exception e)
{
/////////////////////
// just leave null //
/////////////////////
}
}
}
}

View File

@ -247,16 +247,6 @@ public class Memoization<K, V>
/*******************************************************************************
**
*******************************************************************************/
public void clearKey(K key)
{
this.map.remove(key);
}
/*******************************************************************************
** Setter for timeoutSeconds
**

View File

@ -1,188 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.dashboard.widgets;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
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.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
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 RecordListWidgetRenderer
*******************************************************************************/
class RecordListWidgetRendererTest extends BaseTest
{
/***************************************************************************
**
***************************************************************************/
private QWidgetMetaData defineWidget()
{
return RecordListWidgetRenderer.widgetMetaDataBuilder("testRecordListWidget")
.withTableName(TestUtils.TABLE_NAME_SHAPE)
.withMaxRows(20)
.withLabel("Some Shapes")
.withFilter(new QQueryFilter()
.withCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, "${input.maxShapeId}")
.withCriteria("name", QCriteriaOperator.NOT_EQUALS, "Square")
.withOrderBy(new QFilterOrderBy("id", false))
).getWidgetMetaData();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testValidation() throws QInstanceValidationException
{
{
QInstance qInstance = TestUtils.defineInstance();
QWidgetMetaData widgetMetaData = defineWidget();
widgetMetaData.getDefaultValues().remove("tableName");
qInstance.addWidget(widgetMetaData);
assertThatThrownBy(() -> new QInstanceValidator().validate(qInstance))
.isInstanceOf(QInstanceValidationException.class)
.hasMessageContaining("defaultValue for tableName must be given");
}
{
QInstance qInstance = TestUtils.defineInstance();
QWidgetMetaData widgetMetaData = defineWidget();
widgetMetaData.getDefaultValues().remove("filter");
qInstance.addWidget(widgetMetaData);
assertThatThrownBy(() -> new QInstanceValidator().validate(qInstance))
.isInstanceOf(QInstanceValidationException.class)
.hasMessageContaining("defaultValue for filter must be given");
}
{
QInstance qInstance = TestUtils.defineInstance();
QWidgetMetaData widgetMetaData = defineWidget();
widgetMetaData.getDefaultValues().remove("tableName");
widgetMetaData.getDefaultValues().remove("filter");
qInstance.addWidget(widgetMetaData);
assertThatThrownBy(() -> new QInstanceValidator().validate(qInstance))
.isInstanceOf(QInstanceValidationException.class)
.hasMessageContaining("defaultValue for filter must be given")
.hasMessageContaining("defaultValue for tableName must be given");
}
{
QInstance qInstance = TestUtils.defineInstance();
QWidgetMetaData widgetMetaData = defineWidget();
QQueryFilter filter = (QQueryFilter) widgetMetaData.getDefaultValues().get("filter");
filter.addCriteria(new QFilterCriteria("noField", QCriteriaOperator.EQUALS, "noValue"));
qInstance.addWidget(widgetMetaData);
assertThatThrownBy(() -> new QInstanceValidator().validate(qInstance))
.isInstanceOf(QInstanceValidationException.class)
.hasMessageContaining("Criteria fieldName noField is not a field in this table");
}
{
QInstance qInstance = TestUtils.defineInstance();
QWidgetMetaData widgetMetaData = defineWidget();
qInstance.addWidget(widgetMetaData);
//////////////////////////////////
// make sure valid setup passes //
//////////////////////////////////
new QInstanceValidator().validate(qInstance);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRender() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QWidgetMetaData widgetMetaData = defineWidget();
qInstance.addWidget(widgetMetaData);
TestUtils.insertDefaultShapes(qInstance);
TestUtils.insertExtraShapes(qInstance);
{
RecordListWidgetRenderer recordListWidgetRenderer = new RecordListWidgetRenderer();
RenderWidgetInput input = new RenderWidgetInput();
input.setWidgetMetaData(widgetMetaData);
input.setQueryParams(Map.of("maxShapeId", "1"));
RenderWidgetOutput output = recordListWidgetRenderer.render(input);
ChildRecordListData widgetData = (ChildRecordListData) output.getWidgetData();
assertEquals(1, widgetData.getTotalRows());
assertEquals(1, widgetData.getQueryOutput().getRecords().get(0).getValue("id"));
assertEquals("Triangle", widgetData.getQueryOutput().getRecords().get(0).getValue("name"));
}
{
RecordListWidgetRenderer recordListWidgetRenderer = new RecordListWidgetRenderer();
RenderWidgetInput input = new RenderWidgetInput();
input.setWidgetMetaData(widgetMetaData);
input.setQueryParams(Map.of("maxShapeId", "4"));
RenderWidgetOutput output = recordListWidgetRenderer.render(input);
ChildRecordListData widgetData = (ChildRecordListData) output.getWidgetData();
assertEquals(3, widgetData.getTotalRows());
/////////////////////////////////////////////////////////////////////////
// id=2,name=Square was skipped due to NOT_EQUALS Square in the filter //
// max-shape-id applied we don't get id=5 or 6 //
// and they're ordered as specified in the filter (id desc) //
/////////////////////////////////////////////////////////////////////////
assertEquals(4, widgetData.getQueryOutput().getRecords().get(0).getValue("id"));
assertEquals("Rectangle", widgetData.getQueryOutput().getRecords().get(0).getValue("name"));
assertEquals(3, widgetData.getQueryOutput().getRecords().get(1).getValue("id"));
assertEquals("Circle", widgetData.getQueryOutput().getRecords().get(1).getValue("name"));
assertEquals(1, widgetData.getQueryOutput().getRecords().get(2).getValue("id"));
assertEquals("Triangle", widgetData.getQueryOutput().getRecords().get(2).getValue("name"));
}
}
}

View File

@ -23,14 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.BaseTest;
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.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -54,18 +50,4 @@ class CountActionTest extends BaseTest
CountOutput result = new CountAction().execute(request);
assertNotNull(result);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStaticWrapper() throws QException
{
TestUtils.insertDefaultShapes(QContext.getQInstance());
assertEquals(3, CountAction.execute(TestUtils.TABLE_NAME_SHAPE, null));
assertEquals(3, CountAction.execute(TestUtils.TABLE_NAME_SHAPE, new QQueryFilter()));
}
}

View File

@ -30,23 +30,19 @@ import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
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.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DateTimeDisplayValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -241,102 +237,4 @@ class QValueFormatterTest extends BaseTest
assertEquals("2024-04-04 02:12:00 PM CDT", record.getDisplayValue("createDate"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBlobValuesToDownloadUrls()
{
byte[] blobBytes = "hello".getBytes();
{
QTableMetaData table = new QTableMetaData()
.withName("testTable")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("blobField", QFieldType.BLOB)
.withFieldAdornment(new FieldAdornment().withType(AdornmentType.FILE_DOWNLOAD)
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT, "blob-%s.txt")
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS, new ArrayList<>(List.of("id")))));
//////////////////////////////////////////////////////////////////
// verify display value gets set to formated file-name + fields //
// and raw value becomes URL for downloading the byte //
//////////////////////////////////////////////////////////////////
QRecord record = new QRecord().withValue("id", 47).withValue("blobField", blobBytes);
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
assertEquals("/data/testTable/47/blobField/blob-47.txt", record.getValueString("blobField"));
assertEquals("blob-47.txt", record.getDisplayValue("blobField"));
////////////////////////////////////////////////////////
// verify that w/ no blob value, we don't do anything //
////////////////////////////////////////////////////////
QRecord recordWithoutBlobValue = new QRecord().withValue("id", 47);
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(recordWithoutBlobValue));
assertNull(recordWithoutBlobValue.getValue("blobField"));
assertNull(recordWithoutBlobValue.getDisplayValue("blobField"));
}
{
FieldAdornment adornment = new FieldAdornment().withType(AdornmentType.FILE_DOWNLOAD)
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FIELD, "fileName");
QTableMetaData table = new QTableMetaData()
.withName("testTable")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("fileName", QFieldType.STRING))
.withField(new QFieldMetaData("blobField", QFieldType.BLOB)
.withFieldAdornment(adornment));
////////////////////////////////////////////////////
// here get the file name directly from one field //
////////////////////////////////////////////////////
QRecord record = new QRecord().withValue("id", 47).withValue("blobField", blobBytes).withValue("fileName", "myBlob.txt");
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
assertEquals("/data/testTable/47/blobField/myBlob.txt", record.getValueString("blobField"));
assertEquals("myBlob.txt", record.getDisplayValue("blobField"));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// switch to use dynamic url, rerun, and assert we get the values as they were on the record before the call //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
adornment.withValue(AdornmentType.FileDownloadValues.DOWNLOAD_URL_DYNAMIC, true);
record = new QRecord().withValue("id", 47).withValue("blobField", blobBytes).withValue("fileName", "myBlob.txt")
.withDisplayValue("blobField:" + AdornmentType.FileDownloadValues.DOWNLOAD_URL_DYNAMIC, "/something-custom/")
.withDisplayValue("blobField", "myDisplayValue");
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
assertArrayEquals(blobBytes, record.getValueByteArray("blobField"));
assertEquals("myDisplayValue", record.getDisplayValue("blobField"));
}
{
FieldAdornment adornment = new FieldAdornment().withType(AdornmentType.FILE_DOWNLOAD);
QTableMetaData table = new QTableMetaData()
.withName("testTable")
.withLabel("Test Table")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("blobField", QFieldType.BLOB).withLabel("Blob").withFieldAdornment(adornment));
///////////////////////////////////////////////////////////////////////////////////////////
// w/o file name format or whatever, generate a file name from table & id & field labels //
///////////////////////////////////////////////////////////////////////////////////////////
QRecord record = new QRecord().withValue("id", 47).withValue("blobField", blobBytes);
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
assertEquals("/data/testTable/47/blobField/Test%20Table%2047%20Blob", record.getValueString("blobField"));
assertEquals("Test Table 47 Blob", record.getDisplayValue("blobField"));
////////////////////////////////////////
// add a default extension and re-run //
////////////////////////////////////////
adornment.withValue(AdornmentType.FileDownloadValues.DEFAULT_EXTENSION, "html");
record = new QRecord().withValue("id", 47).withValue("blobField", blobBytes);
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
assertEquals("/data/testTable/47/blobField/Test%20Table%2047%20Blob.html", record.getValueString("blobField"));
assertEquals("Test Table 47 Blob.html", record.getDisplayValue("blobField"));
}
}
}

View File

@ -27,8 +27,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.enrichment.testplugins.TestEnricherPlugin;
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
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.AdornmentType;
@ -596,31 +595,27 @@ class QInstanceEnricherTest extends BaseTest
{
QInstance qInstance = TestUtils.defineInstance();
QInstanceEnricher.addEnricherPlugin(new TestEnricherPlugin());
QInstanceEnricher.addEnricherPlugin(new QInstanceEnricherPluginInterface<QFieldMetaData>()
{
/***************************************************************************
*
***************************************************************************/
@Override
public void enrich(QFieldMetaData field, QInstance qInstance)
{
if(field != null)
{
field.setLabel(field.getLabel() + " Plugged");
}
}
});
new QInstanceEnricher(qInstance).enrich();
qInstance.getTables().values().forEach(table -> table.getFields().values().forEach(field -> assertThat(field.getLabel()).endsWith("Plugged")));
qInstance.getProcesses().values().forEach(process -> process.getInputFields().forEach(field -> assertThat(field.getLabel()).endsWith("Plugged")));
qInstance.getProcesses().values().forEach(process -> process.getOutputFields().forEach(field -> assertThat(field.getLabel()).endsWith("Plugged")));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDiscoverAndAddPlugins() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
new QInstanceEnricher(qInstance).enrich();
qInstance.getTables().values().forEach(table -> table.getFields().values().forEach(field -> assertThat(field.getLabel()).doesNotEndWith("Plugged")));
qInstance = TestUtils.defineInstance();
QInstanceEnricher.discoverAndAddPluginsInPackage(getClass().getPackageName() + ".enrichment.testplugins");
new QInstanceEnricher(qInstance).enrich();
qInstance.getTables().values().forEach(table -> table.getFields().values().forEach(field -> assertThat(field.getLabel()).endsWith("Plugged")));
}
}

View File

@ -1,48 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.instances.enrichment.testplugins;
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class TestEnricherPlugin implements QInstanceEnricherPluginInterface<QFieldMetaData>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void enrich(QFieldMetaData field, QInstance qInstance)
{
if(field != null)
{
field.setLabel(field.getLabel() + " Plugged");
}
}
}

View File

@ -66,7 +66,7 @@ class NowWithOffsetTest extends BaseTest
assertThat(twoWeeksFromNowMillis).isCloseTo(now + (14 * DAY_IN_MILLIS), allowedDiff);
long oneMonthAgoMillis = ((Instant) NowWithOffset.minus(1, ChronoUnit.MONTHS).evaluate(dateTimeField)).toEpochMilli();
assertThat(oneMonthAgoMillis).isCloseTo(now - (30 * DAY_IN_MILLIS), allowedDiffPlusTwoDays); // two days, to work on 3/1...
assertThat(oneMonthAgoMillis).isCloseTo(now - (30 * DAY_IN_MILLIS), allowedDiffPlusOneDay);
long twoMonthsFromNowMillis = ((Instant) NowWithOffset.plus(2, ChronoUnit.MONTHS).evaluate(dateTimeField)).toEpochMilli();
assertThat(twoMonthsFromNowMillis).isCloseTo(now + (60 * DAY_IN_MILLIS), allowedDiffPlusTwoDays);

View File

@ -1,151 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import java.util.LinkedHashMap;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.testqbit.TestQBitConfig;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.testqbit.TestQBitProducer;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.testqbit.metadata.OtherTableMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.testqbit.metadata.SomeTableMetaDataProducer;
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 org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for QBitProducer
*******************************************************************************/
class QBitProducerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
TestQBitConfig config = new TestQBitConfig()
.withOtherTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed(TestUtils.MEMORY_BACKEND_NAME))
.withIsSomeTableEnabled(true)
.withSomeSetting("yes")
.withTableMetaDataCustomizer((i, table) ->
{
if(table.getBackendName() == null)
{
table.setBackendName(TestUtils.DEFAULT_BACKEND_NAME);
}
table.addField(new QFieldMetaData("custom", QFieldType.STRING));
return (table);
});
QInstance qInstance = QContext.getQInstance();
new TestQBitProducer().withTestQBitConfig(config).produce(qInstance);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// OtherTable should have been provided by the qbit, with the backend name we told it above (MEMORY) //
///////////////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData otherTable = qInstance.getTable(OtherTableMetaDataProducer.NAME);
assertNotNull(otherTable);
assertEquals(TestUtils.MEMORY_BACKEND_NAME, otherTable.getBackendName());
assertNotNull(otherTable.getField("custom"));
QBitMetaData sourceQBit = otherTable.getSourceQBit();
assertEquals("testQBit", sourceQBit.getArtifactId());
////////////////////////////////////////////////////////////////////////////////
// SomeTable should have been provided, w/ backend name set by the customizer //
////////////////////////////////////////////////////////////////////////////////
QTableMetaData someTable = qInstance.getTable(SomeTableMetaDataProducer.NAME);
assertNotNull(someTable);
assertEquals(TestUtils.DEFAULT_BACKEND_NAME, someTable.getBackendName());
assertNotNull(otherTable.getField("custom"));
TestQBitConfig qBitConfig = (TestQBitConfig) someTable.getSourceQBitConfig();
assertEquals("yes", qBitConfig.getSomeSetting());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDisableThings() throws QException
{
TestQBitConfig config = new TestQBitConfig()
.withOtherTableConfig(ProvidedOrSuppliedTableConfig.useSuppliedTaleNamed(TestUtils.TABLE_NAME_PERSON_MEMORY))
.withIsSomeTableEnabled(false);
QInstance qInstance = QContext.getQInstance();
new TestQBitProducer().withTestQBitConfig(config).produce(qInstance);
//////////////////////////////////////
// neither table should be produced //
//////////////////////////////////////
QTableMetaData otherTable = qInstance.getTable(OtherTableMetaDataProducer.NAME);
assertNull(otherTable);
QTableMetaData someTable = qInstance.getTable(SomeTableMetaDataProducer.NAME);
assertNull(someTable);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testValidationErrors() throws QException
{
QInstance qInstance = QContext.getQInstance();
TestQBitConfig config = new TestQBitConfig();
assertThatThrownBy(() -> new TestQBitProducer().withTestQBitConfig(config).produce(qInstance))
.isInstanceOf(QBitConfigValidationException.class)
.hasMessageContaining("otherTableConfig must be set")
.hasMessageContaining("isSomeTableEnabled must be set");
qInstance.setQBits(new LinkedHashMap<>());
config.setIsSomeTableEnabled(true);
assertThatThrownBy(() -> new TestQBitProducer().withTestQBitConfig(config).produce(qInstance))
.isInstanceOf(QBitConfigValidationException.class)
.hasMessageContaining("otherTableConfig must be set");
qInstance.setQBits(new LinkedHashMap<>());
config.setOtherTableConfig(ProvidedOrSuppliedTableConfig.useSuppliedTaleNamed(TestUtils.TABLE_NAME_PERSON_MEMORY));
new TestQBitProducer().withTestQBitConfig(config).produce(qInstance);
}
}

View File

@ -1,181 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits.testqbit;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.ProvidedOrSuppliedTableConfig;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class TestQBitConfig implements QBitConfig
{
private MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer;
private Boolean isSomeTableEnabled;
private ProvidedOrSuppliedTableConfig otherTableConfig;
private String someSetting;
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QInstance qInstance, List<String> errors)
{
assertCondition(otherTableConfig != null, "otherTableConfig must be set", errors);
assertCondition(isSomeTableEnabled != null, "isSomeTableEnabled must be set", errors);
}
/*******************************************************************************
** Getter for otherTableConfig
*******************************************************************************/
public ProvidedOrSuppliedTableConfig getOtherTableConfig()
{
return (this.otherTableConfig);
}
/*******************************************************************************
** Setter for otherTableConfig
*******************************************************************************/
public void setOtherTableConfig(ProvidedOrSuppliedTableConfig otherTableConfig)
{
this.otherTableConfig = otherTableConfig;
}
/*******************************************************************************
** Fluent setter for otherTableConfig
*******************************************************************************/
public TestQBitConfig withOtherTableConfig(ProvidedOrSuppliedTableConfig otherTableConfig)
{
this.otherTableConfig = otherTableConfig;
return (this);
}
/*******************************************************************************
** Getter for isSomeTableEnabled
*******************************************************************************/
public Boolean getIsSomeTableEnabled()
{
return (this.isSomeTableEnabled);
}
/*******************************************************************************
** Setter for isSomeTableEnabled
*******************************************************************************/
public void setIsSomeTableEnabled(Boolean isSomeTableEnabled)
{
this.isSomeTableEnabled = isSomeTableEnabled;
}
/*******************************************************************************
** Fluent setter for isSomeTableEnabled
*******************************************************************************/
public TestQBitConfig withIsSomeTableEnabled(Boolean isSomeTableEnabled)
{
this.isSomeTableEnabled = isSomeTableEnabled;
return (this);
}
/*******************************************************************************
** Getter for tableMetaDataCustomizer
*******************************************************************************/
public MetaDataCustomizerInterface<QTableMetaData> getTableMetaDataCustomizer()
{
return (this.tableMetaDataCustomizer);
}
/*******************************************************************************
** Setter for tableMetaDataCustomizer
*******************************************************************************/
public void setTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
this.tableMetaDataCustomizer = tableMetaDataCustomizer;
}
/*******************************************************************************
** Fluent setter for tableMetaDataCustomizer
*******************************************************************************/
public TestQBitConfig withTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
this.tableMetaDataCustomizer = tableMetaDataCustomizer;
return (this);
}
/*******************************************************************************
** Getter for someSetting
*******************************************************************************/
public String getSomeSetting()
{
return (this.someSetting);
}
/*******************************************************************************
** Setter for someSetting
*******************************************************************************/
public void setSomeSetting(String someSetting)
{
this.someSetting = someSetting;
}
/*******************************************************************************
** Fluent setter for someSetting
*******************************************************************************/
public TestQBitConfig withSomeSetting(String someSetting)
{
this.someSetting = someSetting;
return (this);
}
}

View File

@ -1,92 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits.testqbit;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProducer;
/*******************************************************************************
**
*******************************************************************************/
public class TestQBitProducer implements QBitProducer
{
private TestQBitConfig testQBitConfig;
/***************************************************************************
**
***************************************************************************/
@Override
public void produce(QInstance qInstance, String namespace) throws QException
{
QBitMetaData qBitMetaData = new QBitMetaData()
.withGroupId("test.com.kingsrook.qbits")
.withArtifactId("testQBit")
.withVersion("0.1.0")
.withNamespace(namespace)
.withConfig(testQBitConfig);
qInstance.addQBit(qBitMetaData);
List<MetaDataProducerInterface<?>> producers = MetaDataProducerHelper.findProducers(getClass().getPackageName() + ".metadata");
finishProducing(qInstance, qBitMetaData, testQBitConfig, producers);
}
/*******************************************************************************
** Getter for testQBitConfig
*******************************************************************************/
public TestQBitConfig getTestQBitConfig()
{
return (this.testQBitConfig);
}
/*******************************************************************************
** Setter for testQBitConfig
*******************************************************************************/
public void setTestQBitConfig(TestQBitConfig testQBitConfig)
{
this.testQBitConfig = testQBitConfig;
}
/*******************************************************************************
** Fluent setter for testQBitConfig
*******************************************************************************/
public TestQBitProducer withTestQBitConfig(TestQBitConfig testQBitConfig)
{
this.testQBitConfig = testQBitConfig;
return (this);
}
}

View File

@ -1,69 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits.testqbit.metadata;
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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitComponentMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.testqbit.TestQBitConfig;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Meta Data Producer for OtherTable
*******************************************************************************/
public class OtherTableMetaDataProducer extends QBitComponentMetaDataProducer<QTableMetaData, TestQBitConfig>
{
public static final String NAME = "otherTable";
/***************************************************************************
**
***************************************************************************/
@Override
public boolean isEnabled()
{
return (getQBitConfig().getOtherTableConfig().getDoProvideTable());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QTableMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData qTableMetaData = new QTableMetaData()
.withName(NAME)
.withPrimaryKeyField("id")
.withBackendName(getQBitConfig().getOtherTableConfig().getBackendName())
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
return (qTableMetaData);
}
}

View File

@ -1,68 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits.testqbit.metadata;
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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitComponentMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.testqbit.TestQBitConfig;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Meta Data Producer for SomeTable
*******************************************************************************/
public class SomeTableMetaDataProducer extends QBitComponentMetaDataProducer<QTableMetaData, TestQBitConfig>
{
public static final String NAME = "someTable";
/***************************************************************************
**
***************************************************************************/
@Override
public boolean isEnabled()
{
return (getQBitConfig().getIsSomeTableEnabled());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QTableMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData qTableMetaData = new QTableMetaData()
.withName(NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
return (qTableMetaData);
}
}

View File

@ -1,157 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.tables;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
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.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
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.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for TablesCustomPossibleValueProvider
*******************************************************************************/
class TablesCustomPossibleValueProviderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(new QTableMetaData()
.withName("hidden")
.withIsHidden(true)
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
qInstance.addTable(new QTableMetaData()
.withName("restricted")
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION))
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
qInstance.addPossibleValueSource(TablesPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(qInstance));
QContext.init(qInstance, newSession());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetPossibleValue()
{
TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider();
QPossibleValue<String> possibleValue = provider.getPossibleValue(TestUtils.TABLE_NAME_PERSON);
assertEquals(TestUtils.TABLE_NAME_PERSON, possibleValue.getId());
assertEquals("Person", possibleValue.getLabel());
assertNull(provider.getPossibleValue("no-such-table"));
assertNull(provider.getPossibleValue("hidden"));
assertNull(provider.getPossibleValue("restricted"));
QContext.getQSession().withPermission("restricted.hasAccess");
assertNotNull(provider.getPossibleValue("restricted"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSearchPossibleValue() throws QException
{
TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider();
List<QPossibleValue<String>> list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME));
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
assertThat(list).noneMatch(p -> p.getId().equals("no-such-table"));
assertThat(list).noneMatch(p -> p.getId().equals("hidden"));
assertThat(list).noneMatch(p -> p.getId().equals("restricted"));
assertNull(provider.getPossibleValue("restricted"));
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
.withIdList(List.of(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_SHAPE, "hidden")));
assertEquals(2, list.size());
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE));
assertThat(list).noneMatch(p -> p.getId().equals("hidden"));
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
.withLabelList(List.of("Person", "Shape", "Restricted")));
assertEquals(2, list.size());
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE));
assertThat(list).noneMatch(p -> p.getId().equals("restricted"));
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
.withSearchTerm("restricted"));
assertEquals(0, list.size());
/////////////////////////////////////////
// add permission for restricted table //
/////////////////////////////////////////
QContext.getQSession().withPermission("restricted.hasAccess");
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
.withSearchTerm("restricted"));
assertEquals(1, list.size());
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
.withLabelList(List.of("Person", "Shape", "Restricted")));
assertEquals(3, list.size());
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE));
assertThat(list).anyMatch(p -> p.getId().equals("restricted"));
}
}

View File

@ -22,21 +22,24 @@
package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryAssert;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
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.frontend.QFrontendFieldMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile;
@ -58,9 +61,6 @@ import static org.junit.jupiter.api.Assertions.assertNull;
*******************************************************************************/
class BulkInsertFullProcessTest extends BaseTest
{
private static final String defaultEmail = "noone@kingsrook.com";
/*******************************************************************************
**
@ -116,20 +116,48 @@ class BulkInsertFullProcessTest extends BaseTest
@Test
void test() throws Exception
{
String defaultEmail = "noone@kingsrook.com";
///////////////////////////////////////
// make sure table is empty to start //
///////////////////////////////////////
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
QInstance qInstance = QContext.getQInstance();
String processName = "PersonBulkInsertV2";
new QInstanceEnricher(qInstance).defineTableBulkInsert(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), processName);
/////////////////////////////////////////////////////////
// start the process - expect to go to the upload step //
/////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(processName);
runProcessInput.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
String processUUID = runProcessOutput.getProcessUUID();
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("upload");
//////////////////////////////
// simulate the file upload //
//////////////////////////////
String storageReference = UUID.randomUUID() + ".csv";
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
{
outputStream.write((getPersonCsvHeaderUsingLabels() + getPersonCsvRow1() + getPersonCsvRow2()).getBytes());
}
catch(IOException e)
{
throw (e);
}
//////////////////////////
// continue post-upload //
//////////////////////////
runProcessOutput = continueProcessPostUpload(runProcessInput, processUUID, simulateFileUpload(2));
runProcessInput.setProcessUUID(processUUID);
runProcessInput.setStartAfterStep("upload");
runProcessInput.addValue("theFile", new ArrayList<>(List.of(storageInput)));
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertEquals(List.of("Id", "Create Date", "Modify Date", "First Name", "Last Name", "Birth Date", "Email", "Home State", "noOfShoes"), runProcessOutput.getValue("headerValues"));
assertEquals(List.of("A", "B", "C", "D", "E", "F", "G", "H", "I"), runProcessOutput.getValue("headerLetters"));
@ -148,10 +176,29 @@ class BulkInsertFullProcessTest extends BaseTest
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("fileMapping");
////////////////////////////////////////////////////////////////////////////////
// all subsequent steps will want these data - so set up a lambda to set them //
////////////////////////////////////////////////////////////////////////////////
Consumer<RunProcessInput> addProfileToRunProcessInput = (RunProcessInput input) ->
{
input.addValue("version", "v1");
input.addValue("layout", "FLAT");
input.addValue("hasHeaderRow", "true");
input.addValue("fieldListJSON", JsonUtils.toJson(List.of(
new BulkLoadProfileField().withFieldName("firstName").withColumnIndex(3),
new BulkLoadProfileField().withFieldName("lastName").withColumnIndex(4),
new BulkLoadProfileField().withFieldName("email").withDefaultValue(defaultEmail),
new BulkLoadProfileField().withFieldName("homeStateId").withColumnIndex(7).withDoValueMapping(true).withValueMappings(Map.of("Illinois", 1)),
new BulkLoadProfileField().withFieldName("noOfShoes").withColumnIndex(8)
)));
};
////////////////////////////////
// continue post file-mapping //
////////////////////////////////
runProcessOutput = continueProcessPostFileMapping(runProcessInput);
runProcessInput.setStartAfterStep("fileMapping");
addProfileToRunProcessInput.accept(runProcessInput);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
Serializable valueMappingField = runProcessOutput.getValue("valueMappingField");
assertThat(valueMappingField).isInstanceOf(QFrontendFieldMetaData.class);
assertEquals("homeStateId", ((QFrontendFieldMetaData) valueMappingField).getName());
@ -164,20 +211,23 @@ class BulkInsertFullProcessTest extends BaseTest
/////////////////////////////////
// continue post value-mapping //
/////////////////////////////////
runProcessOutput = continueProcessPostValueMapping(runProcessInput);
runProcessInput.setStartAfterStep("valueMapping");
runProcessInput.addValue("mappedValuesJSON", JsonUtils.toJson(Map.of("Illinois", 1, "Missouri", 2)));
addProfileToRunProcessInput.accept(runProcessInput);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review");
/////////////////////////////////
// continue post review screen //
/////////////////////////////////
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
runProcessInput.setStartAfterStep("review");
addProfileToRunProcessInput.accept(runProcessInput);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertThat(runProcessOutput.getRecords()).hasSize(2);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("result");
assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY)).isNotNull().isInstanceOf(List.class);
assertThat(runProcessOutput.getException()).isEmpty();
ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Inserted Id values between 1 and 2");
////////////////////////////////////
// query for the inserted records //
////////////////////////////////////
@ -199,136 +249,4 @@ class BulkInsertFullProcessTest extends BaseTest
assertNull(records.get(1).getValue("noOfShoes"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneRow() throws Exception
{
///////////////////////////////////////
// make sure table is empty to start //
///////////////////////////////////////
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
RunProcessInput runProcessInput = new RunProcessInput();
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
String processUUID = runProcessOutput.getProcessUUID();
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUpload(1));
continueProcessPostFileMapping(runProcessInput);
continueProcessPostValueMapping(runProcessInput);
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// all that just so we can make sure this message is right (because it was wrong when we first wrote it, lol) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Inserted Id 1");
}
/***************************************************************************
**
***************************************************************************/
private static RunProcessOutput continueProcessPostReviewScreen(RunProcessInput runProcessInput) throws QException
{
RunProcessOutput runProcessOutput;
runProcessInput.setStartAfterStep("review");
addProfileToRunProcessInput(runProcessInput);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
return runProcessOutput;
}
/***************************************************************************
**
***************************************************************************/
private static RunProcessOutput continueProcessPostValueMapping(RunProcessInput runProcessInput) throws QException
{
runProcessInput.setStartAfterStep("valueMapping");
runProcessInput.addValue("mappedValuesJSON", JsonUtils.toJson(Map.of("Illinois", 1, "Missouri", 2)));
addProfileToRunProcessInput(runProcessInput);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
return (runProcessOutput);
}
/***************************************************************************
**
***************************************************************************/
private static RunProcessOutput continueProcessPostFileMapping(RunProcessInput runProcessInput) throws QException
{
RunProcessOutput runProcessOutput;
runProcessInput.setStartAfterStep("fileMapping");
addProfileToRunProcessInput(runProcessInput);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
return runProcessOutput;
}
/***************************************************************************
**
***************************************************************************/
private static RunProcessOutput continueProcessPostUpload(RunProcessInput runProcessInput, String processUUID, StorageInput storageInput) throws QException
{
runProcessInput.setProcessUUID(processUUID);
runProcessInput.setStartAfterStep("upload");
runProcessInput.addValue("theFile", new ArrayList<>(List.of(storageInput)));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
return (runProcessOutput);
}
/***************************************************************************
**
***************************************************************************/
private static StorageInput simulateFileUpload(int noOfRows) throws Exception
{
String storageReference = UUID.randomUUID() + ".csv";
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
{
outputStream.write((getPersonCsvHeaderUsingLabels() + getPersonCsvRow1() + (noOfRows == 2 ? getPersonCsvRow2() : "")).getBytes());
}
return storageInput;
}
/***************************************************************************
**
***************************************************************************/
private static RunProcessOutput startProcess(RunProcessInput runProcessInput) throws QException
{
runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkInsert");
runProcessInput.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
return runProcessOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void addProfileToRunProcessInput(RunProcessInput input)
{
input.addValue("version", "v1");
input.addValue("layout", "FLAT");
input.addValue("hasHeaderRow", "true");
input.addValue("fieldListJSON", JsonUtils.toJson(List.of(
new BulkLoadProfileField().withFieldName("firstName").withColumnIndex(3),
new BulkLoadProfileField().withFieldName("lastName").withColumnIndex(4),
new BulkLoadProfileField().withFieldName("email").withDefaultValue(defaultEmail),
new BulkLoadProfileField().withFieldName("homeStateId").withColumnIndex(7).withDoValueMapping(true).withValueMappings(Map.of("Illinois", 1)),
new BulkLoadProfileField().withFieldName("noOfShoes").withColumnIndex(8)
)));
}
}

View File

@ -1,75 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
import java.text.ParseException;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for CronDescriber
*******************************************************************************/
class CronDescriberTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws ParseException
{
assertEquals("At every second, every minute, every hour, on every day of every month, every day of the week.", CronDescriber.getDescription("* * * * * ?"));
assertEquals("At 0 seconds, every minute, every hour, on every day of every month, every day of the week.", CronDescriber.getDescription("0 * * * * ?"));
assertEquals("At 0 seconds, 0 minutes, every hour, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 * * * ?"));
assertEquals("At 0 seconds, 0 and 30 minutes, every hour, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0,30 * * * ?"));
assertEquals("At 0 seconds, 0 minutes, midnight, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 0 * * ?"));
assertEquals("At 0 seconds, 0 minutes, 1 AM, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 1 * * ?"));
assertEquals("At 0 seconds, 0 minutes, 11 AM, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 11 * * ?"));
assertEquals("At 0 seconds, 0 minutes, noon, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 12 * * ?"));
assertEquals("At 0 seconds, 0 minutes, 1 PM, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 13 * * ?"));
assertEquals("At 0 seconds, 0 minutes, 11 PM, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 23 * * ?"));
assertEquals("At 0 seconds, 0 minutes, midnight, on day 10 of every month, every day of the week.", CronDescriber.getDescription("0 0 0 10 * ?"));
assertEquals("At 0 seconds, 0 minutes, midnight, on days 10 and 20 of every month, every day of the week.", CronDescriber.getDescription("0 0 0 10,20 * ?"));
assertEquals("At 0 seconds, 0 minutes, midnight, on days from 10 to 15 of every month, every day of the week.", CronDescriber.getDescription("0 0 0 10-15 * ?"));
assertEquals("At from 10 to 15 seconds, 0 minutes, midnight, on every day of every month, every day of the week.", CronDescriber.getDescription("10-15 0 0 * * ?"));
assertEquals("At 30 seconds, 30 minutes, from 8 AM to 4 PM, on every day of every month, every day of the week.", CronDescriber.getDescription("30 30 8-16 * * ?"));
assertEquals("At 0 seconds, 0 minutes, midnight, on every 3 days starting at 0 of every month, every day of the week.", CronDescriber.getDescription("0 0 0 */3 * ?"));
assertEquals("At every 5 seconds starting at 0, 0 minutes, midnight, on every day of every month, every day of the week.", CronDescriber.getDescription("0/5 0 0 * * ?"));
assertEquals("At 0 seconds, every 30 minutes starting at 3, midnight, on every day of every month, every day of the week.", CronDescriber.getDescription("0 3/30 0 * * ?"));
assertEquals("At 0 seconds, 0 minutes, midnight, on every day of every month, Monday, Wednesday, and Friday.", CronDescriber.getDescription("0 0 0 * * MON,WED,FRI"));
assertEquals("At 0 seconds, 0 minutes, midnight, on every day of every month, from Monday to Friday.", CronDescriber.getDescription("0 0 0 * * MON-FRI"));
assertEquals("At 0 seconds, 0 minutes, midnight, on every day of every month, Sunday and Saturday.", CronDescriber.getDescription("0 0 0 * * 1,7"));
assertEquals("At 0 seconds, 0 minutes, 2 AM, 6 AM, noon, 4 PM, and 8 PM, on every day of every month, every day of the week.", CronDescriber.getDescription("0 0 2,6,12,16,20 * * ?"));
assertEquals("At every 5 seconds starting at 0, 14, 18, 3-39, and 52 minutes, every hour, on every day of January, March, and September, from Monday to Friday, in 2002-2010.", CronDescriber.getDescription("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"));
assertEquals("At every second, every minute, every hour, on every day of every month, every day of the week.", CronDescriber.getDescription("* * * ? * *"));
assertEquals("At every second, every minute, every hour, on every day of January to June, every day of the week.", CronDescriber.getDescription("* * * ? 1-6 *"));
assertEquals("At every second, every minute, every hour, on days 1, 3, and 5 of every month, every day of the week.", CronDescriber.getDescription("* * * 1,3,5 * *"));
// todo fix has 2-4 hours and 3 PM, s/b 2 AM to 4 AM and 3 PM assertEquals("At every second, every minute, every hour, on days 1, 3, and 5 of every month, every day of the week.", CronDescriber.getDescription("* * 2-4,15 1,3,5 * *"));
// hour failing on 3,2-7 (at least in TS side?)
// 3,2-7 makes 3,2 to July
}
}

View File

@ -1,67 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
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.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.metadata.fields.AdornmentType;
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.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for CronExpressionTooltipFieldBehavior
*******************************************************************************/
class CronExpressionTooltipFieldBehaviorTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QFieldMetaData field = new QFieldMetaData("cronExpression", QFieldType.STRING);
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE)
.addField(field);
CronExpressionTooltipFieldBehavior.addToField(field);
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(
new QRecord().withValue("name", "Square").withValue("cronExpression", "* * * * * ?")));
QRecord record = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_SHAPE).withPrimaryKey(1).withShouldGenerateDisplayValues(true));
assertThat(record.getDisplayValue("cronExpression:" + AdornmentType.TooltipValues.TOOLTIP_DYNAMIC))
.contains("every second");
}
}

View File

@ -75,7 +75,6 @@
<scope>test</scope>
</dependency>
<dependency>
<!-- this was added to help make testcontainers work -->
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.7.0</version>

View File

@ -40,8 +40,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
@ -59,8 +57,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantSett
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsUtil;
import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
@ -114,10 +111,9 @@ public abstract class AbstractBaseFilesystemAction<FILE>
public abstract Instant getFileModifyDate(FILE file);
/*******************************************************************************
** List the files for a table - or optionally, just a single file name -
** to be implemented in module-specific subclasses.
** List the files for a table - WITH an input filter - to be implemented in module-specific subclasses.
*******************************************************************************/
public abstract List<FILE> listFiles(QTableMetaData table, QBackendMetaData backendBase, String requestedSingleFileName) throws QException;
public abstract List<FILE> listFiles(QTableMetaData table, QBackendMetaData backendBase, QQueryFilter filter) throws QException;
/*******************************************************************************
** Read the contents of a file - to be implemented in module-specific subclasses.
@ -141,7 +137,7 @@ public abstract class AbstractBaseFilesystemAction<FILE>
**
** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
*******************************************************************************/
public abstract void deleteFile(QTableMetaData table, String fileReference) throws FilesystemException;
public abstract void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException;
/*******************************************************************************
** Move a file from a source path, to a destination path.
@ -282,13 +278,11 @@ public abstract class AbstractBaseFilesystemAction<FILE>
try
{
QTableMetaData table = queryInput.getTable();
AbstractFilesystemTableBackendDetails tableDetails = getTableBackendDetails(AbstractFilesystemTableBackendDetails.class, table);
QueryOutput queryOutput = new QueryOutput(queryInput);
String requestedPath = null;
List<FILE> files = listFiles(table, queryInput.getBackend(), requestedPath);
QTableMetaData table = queryInput.getTable();
AbstractFilesystemTableBackendDetails tableDetails = getTableBackendDetails(AbstractFilesystemTableBackendDetails.class, table);
List<FILE> files = listFiles(table, queryInput.getBackend(), queryInput.getFilter());
switch(tableDetails.getCardinality())
{
@ -311,7 +305,6 @@ public abstract class AbstractBaseFilesystemAction<FILE>
}
/***************************************************************************
**
***************************************************************************/
@ -331,7 +324,6 @@ public abstract class AbstractBaseFilesystemAction<FILE>
}
/***************************************************************************
**
***************************************************************************/
@ -390,12 +382,13 @@ public abstract class AbstractBaseFilesystemAction<FILE>
// if so, remove that criteria here, so that its presence doesn't cause all records to be filtered away //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter filterForRecords = queryInput.getFilter();
// if(filterForRecords != null)
// {
// filterForRecords = filterForRecords.clone();
// CollectionUtils.nonNullList(filterForRecords.getCriteria())
// .removeIf(AbstractBaseFilesystemAction::isPathEqualsCriteria);
// }
if(filterForRecords != null)
{
filterForRecords = filterForRecords.clone();
CollectionUtils.nonNullList(filterForRecords.getCriteria())
.removeIf(AbstractBaseFilesystemAction::isPathEqualsCriteria);
}
if(BackendQueryFilterUtils.doesRecordMatch(filterForRecords, null, record))
{
@ -567,7 +560,6 @@ public abstract class AbstractBaseFilesystemAction<FILE>
}
/***************************************************************************
** Method that subclasses can override to add post-action things (e.g., closing resources)
***************************************************************************/
@ -579,7 +571,6 @@ public abstract class AbstractBaseFilesystemAction<FILE>
}
/*******************************************************************************
**
*******************************************************************************/
@ -660,57 +651,4 @@ public abstract class AbstractBaseFilesystemAction<FILE>
}
}
/*******************************************************************************
**
*******************************************************************************/
protected DeleteOutput executeDelete(DeleteInput deleteInput) throws QException
{
try
{
preAction(deleteInput.getBackend());
DeleteOutput output = new DeleteOutput();
output.setRecordsWithErrors(new ArrayList<>());
QTableMetaData table = deleteInput.getTable();
QBackendMetaData backend = deleteInput.getBackend();
AbstractFilesystemTableBackendDetails tableDetails = getTableBackendDetails(AbstractFilesystemTableBackendDetails.class, table);
if(tableDetails.getCardinality().equals(Cardinality.ONE))
{
int deletedCount = 0;
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
{
try
{
deleteFile(table, stripDuplicatedSlashes(getFullBasePath(table, backend) + "/" + primaryKey));
deletedCount++;
}
catch(Exception e)
{
String message = ObjectUtils.tryElse(() -> ExceptionUtils.getRootException(e).getMessage(), "Message not available");
output.addRecordWithError(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withError(new SystemErrorStatusMessage("Error deleting file: " + message)));
}
}
output.setDeletedRecordCount(deletedCount);
}
else
{
throw (new NotImplementedException("Delete is not implemented for filesystem tables with cardinality: " + tableDetails.getCardinality()));
}
return (output);
}
catch(Exception e)
{
throw new QException("Error executing delete: " + e.getMessage(), e);
}
finally
{
postAction();
}
}
}

View File

@ -22,15 +22,12 @@
package com.kingsrook.qqq.backend.module.filesystem.base.model.metadata;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
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.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
import com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule;
@ -62,8 +59,6 @@ public class FilesystemTableMetaDataBuilder
private String basePath;
private String glob;
private String contentsAdornmentFileNameField = "baseName";
/*******************************************************************************
@ -71,64 +66,46 @@ public class FilesystemTableMetaDataBuilder
*******************************************************************************/
public QTableMetaData buildStandardCardinalityOneTable()
{
boolean includeCreateDate = true;
AbstractFilesystemTableBackendDetails tableBackendDetails = switch(backend.getBackendType())
{
case S3BackendModule.BACKEND_TYPE ->
{
includeCreateDate = false;
yield new S3TableBackendDetails();
}
case S3BackendModule.BACKEND_TYPE -> new S3TableBackendDetails();
case FilesystemBackendModule.BACKEND_TYPE -> new FilesystemTableBackendDetails();
case SFTPBackendModule.BACKEND_TYPE -> new SFTPTableBackendDetails();
default -> throw new IllegalStateException("Unexpected value: " + backend.getBackendType());
};
List<QFieldMetaData> fields = new ArrayList<>();
fields.add((new QFieldMetaData("fileName", QFieldType.STRING)));
fields.add((new QFieldMetaData("baseName", QFieldType.STRING)));
fields.add((new QFieldMetaData("size", QFieldType.LONG).withDisplayFormat(DisplayFormat.COMMAS)));
fields.add((new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)));
fields.add((new QFieldMetaData("contents", QFieldType.BLOB)
.withIsHeavy(true)
.withFieldAdornment(new FieldAdornment(AdornmentType.FILE_DOWNLOAD)
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT, "%s")
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FIELD, contentsAdornmentFileNameField
))));
QFieldSection t3Section = SectionFactory.defaultT3("modifyDate");
AbstractFilesystemTableBackendDetails backendDetails = tableBackendDetails
.withCardinality(Cardinality.ONE)
.withFileNameFieldName("fileName")
.withBaseNameFieldName("baseName")
.withContentsFieldName("contents")
.withSizeFieldName("size")
.withModifyDateFieldName("modifyDate")
.withBasePath(basePath)
.withGlob(glob);
if(includeCreateDate)
{
fields.add((new QFieldMetaData("createDate", QFieldType.DATE_TIME)));
backendDetails.setCreateDateFieldName("createDate");
ArrayList<String> t3FieldNames = new ArrayList<>(t3Section.getFieldNames());
t3FieldNames.add(0, "createDate");
t3Section.setFieldNames(t3FieldNames);
}
return new QTableMetaData()
.withName(name)
.withIsHidden(true)
.withBackendName(backend.getName())
.withPrimaryKeyField("fileName")
.withFields(fields)
.withField(new QFieldMetaData("fileName", QFieldType.STRING))
.withField(new QFieldMetaData("baseName", QFieldType.STRING))
.withField(new QFieldMetaData("size", QFieldType.LONG).withDisplayFormat(DisplayFormat.COMMAS))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("contents", QFieldType.BLOB)
.withIsHeavy(true)
.withFieldAdornment(new FieldAdornment(AdornmentType.FILE_DOWNLOAD)
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT, "%s")
.withValue(AdornmentType.FileDownloadValues.FILE_NAME_FIELD, "fileName")
))
.withSection(SectionFactory.defaultT1("fileName"))
.withSection(SectionFactory.defaultT2("baseName", "contents", "size"))
.withSection(t3Section)
.withBackendDetails(backendDetails);
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"))
.withBackendDetails(tableBackendDetails
.withCardinality(Cardinality.ONE)
.withFileNameFieldName("fileName")
.withBaseNameFieldName("baseName")
.withContentsFieldName("contents")
.withSizeFieldName("size")
.withCreateDateFieldName("createDate")
.withModifyDateFieldName("modifyDate")
.withBasePath(basePath)
.withGlob(glob));
}
@ -255,35 +232,4 @@ public class FilesystemTableMetaDataBuilder
return (this);
}
/*******************************************************************************
** Getter for contentsAdornmentFileNameField
*******************************************************************************/
public String getContentsAdornmentFileNameField()
{
return (this.contentsAdornmentFileNameField);
}
/*******************************************************************************
** Setter for contentsAdornmentFileNameField
*******************************************************************************/
public void setContentsAdornmentFileNameField(String contentsAdornmentFileNameField)
{
this.contentsAdornmentFileNameField = contentsAdornmentFileNameField;
}
/*******************************************************************************
** Fluent setter for contentsAdornmentFileNameField
*******************************************************************************/
public FilesystemTableMetaDataBuilder withContentsAdornmentFileNameField(String contentsAdornmentFileNameField)
{
this.contentsAdornmentFileNameField = contentsAdornmentFileNameField;
return (this);
}
}

View File

@ -130,11 +130,7 @@ public class SharedFilesystemBackendModuleUtils
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////
// this happens in base class now, like, for query action, so, we think okay to just ignore. //
///////////////////////////////////////////////////////////////////////////////////////////////
// throw (new QException("Unable to query filesystem table by field: " + criteria.getFieldName()));
return (true);
throw (new QException("Unable to query filesystem table by field: " + criteria.getFieldName()));
}
}

View File

@ -41,8 +41,6 @@ import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -113,7 +111,7 @@ public class AbstractFilesystemAction extends AbstractBaseFilesystemAction<File>
** List the files for this table.
*******************************************************************************/
@Override
public List<File> listFiles(QTableMetaData table, QBackendMetaData backendBase, String requestedPath) throws QException
public List<File> listFiles(QTableMetaData table, QBackendMetaData backendBase, QQueryFilter filter) throws QException
{
try
{
@ -132,14 +130,7 @@ public class AbstractFilesystemAction extends AbstractBaseFilesystemAction<File>
for(String matchedFile : matchedFiles)
{
boolean isMatch = true;
if(StringUtils.hasContent(requestedPath))
{
QQueryFilter filter = new QQueryFilter(new QFilterCriteria(tableBackendDetails.getFileNameFieldName(), QCriteriaOperator.EQUALS, requestedPath));
isMatch = SharedFilesystemBackendModuleUtils.doesFilePathMatchFilter(matchedFile, filter, tableBackendDetails);
}
if(isMatch)
if(SharedFilesystemBackendModuleUtils.doesFilePathMatchFilter(matchedFile, filter, tableBackendDetails))
{
rs.add(new File(fullPath + File.separatorChar + matchedFile));
}
@ -230,7 +221,7 @@ public class AbstractFilesystemAction extends AbstractBaseFilesystemAction<File>
** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
*******************************************************************************/
@Override
public void deleteFile(QTableMetaData table, String fileReference) throws FilesystemException
public void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException
{
File file = new File(fileReference);
if(!file.exists())

View File

@ -26,12 +26,13 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
**
*******************************************************************************/
public class FilesystemDeleteAction extends AbstractFilesystemAction implements DeleteInterface
public class FilesystemDeleteAction implements DeleteInterface
{
/*******************************************************************************
@ -39,19 +40,21 @@ public class FilesystemDeleteAction extends AbstractFilesystemAction implements
*******************************************************************************/
public DeleteOutput execute(DeleteInput deleteInput) throws QException
{
return (executeDelete(deleteInput));
}
throw new NotImplementedException("Filesystem delete not implemented");
/*
try
{
DeleteResult rs = new DeleteResult();
QTableMetaData table = deleteRequest.getTable();
/*******************************************************************************
** Specify whether this particular module's update action can & should fetch
** records before updating them, e.g., for audits or "not-found-checks"
*******************************************************************************/
@Override
public boolean supportsPreFetchQuery()
{
return (false);
// return rs;
}
catch(Exception e)
{
throw new QException("Error executing delete: " + e.getMessage(), e);
}
*/
}
}

View File

@ -94,7 +94,7 @@ public class BasicETLCleanupSourceFilesStep implements BackendStep
if(VALUE_DELETE.equals(moveOrDelete))
{
LOG.info("Deleting ETL source file: " + sourceFile);
actionBase.deleteFile(table, sourceFile);
actionBase.deleteFile(QContext.getQInstance(), table, sourceFile);
}
else if(VALUE_MOVE.equals(moveOrDelete))
{

View File

@ -319,7 +319,7 @@ public class FilesystemImporterStep implements BackendStep
{
String fullBasePath = sourceActionBase.getFullBasePath(sourceTable, sourceBackend);
LOG.info("Removing source file", logPair("path", fullBasePath + "/" + sourceFileName), logPair("sourceTable", sourceTable.getName()));
sourceActionBase.deleteFile(sourceTable, fullBasePath + "/" + sourceFileName);
sourceActionBase.deleteFile(QContext.getQInstance(), sourceTable, fullBasePath + "/" + sourceFileName);
}
else
{

View File

@ -31,9 +31,9 @@ import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.S3ObjectSummary;
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.tables.query.QQueryFilter;
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.tables.QTableMetaData;
@ -163,7 +163,7 @@ public class AbstractS3Action extends AbstractBaseFilesystemAction<S3ObjectSumma
** List the files for a table.
*******************************************************************************/
@Override
public List<S3ObjectSummary> listFiles(QTableMetaData table, QBackendMetaData backendBase, String requestedPath) throws QException
public List<S3ObjectSummary> listFiles(QTableMetaData table, QBackendMetaData backendBase, QQueryFilter filter) throws QException
{
S3BackendMetaData s3BackendMetaData = getBackendMetaData(S3BackendMetaData.class, backendBase);
AbstractFilesystemTableBackendDetails tableDetails = getTableBackendDetails(AbstractFilesystemTableBackendDetails.class, table);
@ -175,7 +175,7 @@ public class AbstractS3Action extends AbstractBaseFilesystemAction<S3ObjectSumma
////////////////////////////////////////////////////////////////////
// todo - look at metadata to configure the s3 client here? //
////////////////////////////////////////////////////////////////////
return getS3Utils().listObjectsInBucketMatchingGlob(bucketName, fullPath, glob, requestedPath, tableDetails);
return getS3Utils().listObjectsInBucketMatchingGlob(bucketName, fullPath, glob, filter, tableDetails);
}
@ -248,9 +248,9 @@ public class AbstractS3Action extends AbstractBaseFilesystemAction<S3ObjectSumma
** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
*******************************************************************************/
@Override
public void deleteFile(QTableMetaData table, String fileReference) throws FilesystemException
public void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException
{
QBackendMetaData backend = QContext.getQInstance().getBackend(table.getBackendName());
QBackendMetaData backend = instance.getBackend(table.getBackendName());
String bucketName = ((S3BackendMetaData) backend).getBucketName();
String cleanedPath = stripLeadingSlash(stripDuplicatedSlashes(fileReference));

View File

@ -26,12 +26,13 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
**
*******************************************************************************/
public class S3DeleteAction extends AbstractS3Action implements DeleteInterface
public class S3DeleteAction implements DeleteInterface
{
/*******************************************************************************
@ -39,19 +40,21 @@ public class S3DeleteAction extends AbstractS3Action implements DeleteInterface
*******************************************************************************/
public DeleteOutput execute(DeleteInput deleteInput) throws QException
{
return (executeDelete(deleteInput));
}
throw new NotImplementedException("S3 delete not implemented");
/*
try
{
DeleteResult rs = new DeleteResult();
QTableMetaData table = deleteRequest.getTable();
/*******************************************************************************
** Specify whether this particular module's update action can & should fetch
** records before updating them, e.g., for audits or "not-found-checks"
*******************************************************************************/
@Override
public boolean supportsPreFetchQuery()
{
return (false);
// return rs;
}
catch(Exception e)
{
throw new QException("Error executing delete: " + e.getMessage(), e);
}
*/
}
}

View File

@ -39,8 +39,12 @@ import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
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.module.filesystem.base.model.metadata.AbstractFilesystemTableBackendDetails;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
import com.kingsrook.qqq.backend.module.filesystem.base.utils.SharedFilesystemBackendModuleUtils;
import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.AbstractFilesystemAction;
@ -76,7 +80,7 @@ public class S3Utils
** https://docs.oracle.com/javase/7/docs/api/java/nio/file/FileSystem.html#getPathMatcher(java.lang.String)
** and also - (possibly) apply a file-name filter (based on the table's details).
*******************************************************************************/
public List<S3ObjectSummary> listObjectsInBucketMatchingGlob(String bucketName, String path, String glob, String requestedPath, AbstractFilesystemTableBackendDetails tableDetails) throws QException
public List<S3ObjectSummary> listObjectsInBucketMatchingGlob(String bucketName, String path, String glob, QQueryFilter filter, AbstractFilesystemTableBackendDetails tableDetails) throws QException
{
//////////////////////////////////////////////////////////////////////////////////////////////////
// s3 list requests find nothing if the path starts with a /, so strip away any leading slashes //
@ -92,20 +96,38 @@ public class S3Utils
prefix = prefix.substring(0, prefix.indexOf('*'));
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// optimization, to avoid listing whole bucket, for use-case where less than a whole bucket is requested //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(requestedPath))
///////////////////////////////////////////////////////////////////////////////////////////////////////
// for a file-per-record (ONE) table, we may need to apply the filter to listing. //
// but for MANY tables, the filtering would be done on the records after they came out of the files. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
boolean useQQueryFilter = false;
if(tableDetails != null && Cardinality.ONE.equals(tableDetails.getCardinality()))
{
if(!prefix.isEmpty())
{
///////////////////////////////////////////////////////
// remember, a prefix starting with / finds nothing! //
///////////////////////////////////////////////////////
prefix += "/";
}
useQQueryFilter = true;
}
prefix += requestedPath;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a filter for single file, make that file name the "prefix" that we send to s3, so we just get back that 1 file. //
// as this will be a common case. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(filter != null && useQQueryFilter)
{
if(filter.getCriteria() != null && filter.getCriteria().size() == 1)
{
QFilterCriteria criteria = filter.getCriteria().get(0);
if(tableDetails.getFileNameFieldName().equals(criteria.getFieldName()) && criteria.getOperator().equals(QCriteriaOperator.EQUALS))
{
if(!prefix.isEmpty())
{
///////////////////////////////////////////////////////
// remember, a prefix starting with / finds nothing! //
///////////////////////////////////////////////////////
prefix += "/";
}
prefix += criteria.getValues().get(0);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -181,7 +203,27 @@ public class S3Utils
continue;
}
///////////////////////////////////////////////////////////////////////////////////
// if we're a file-per-record table, and we have a filter, compare the key to it //
///////////////////////////////////////////////////////////////////////////////////
if(!SharedFilesystemBackendModuleUtils.doesFilePathMatchFilter(key, filter, tableDetails))
{
continue;
}
rs.add(objectSummary);
/////////////////////////////////////////////////////////////////
// if we have a limit, and we've hit it, break out of the loop //
/////////////////////////////////////////////////////////////////
if(filter != null && useQQueryFilter && filter.getLimit() != null)
{
if(rs.size() >= filter.getLimit())
{
break;
}
}
}
}
while(listObjectsV2Result.isTruncated());

View File

@ -26,30 +26,23 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.model.metadata.QBackendMetaData;
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.model.metadata.variants.BackendVariantSetting;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsUtil;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
import com.kingsrook.qqq.backend.module.filesystem.sftp.model.SFTPDirEntryWithPath;
@ -57,10 +50,8 @@ import com.kingsrook.qqq.backend.module.filesystem.sftp.model.metadata.SFTPBacke
import com.kingsrook.qqq.backend.module.filesystem.sftp.model.metadata.SFTPBackendVariantSetting;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.apache.sshd.sftp.common.SftpException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -71,8 +62,6 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
{
private static final QLogger LOG = QLogger.getLogger(AbstractSFTPAction.class);
/***************************************************************************
** singleton implementing Initialization-on-Demand Holder idiom
** to help ensure only a single SshClient object exists in a server.
@ -104,6 +93,8 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
}
}
////////////////////////////////////////////////////////////////
// open clientSessionFirst, then sftpClient //
// and close them in reverse (sftpClient, then clientSession) //
@ -131,11 +122,10 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
{
SFTPBackendMetaData sftpBackendMetaData = getBackendMetaData(SFTPBackendMetaData.class, backendMetaData);
String username = sftpBackendMetaData.getUsername();
String password = sftpBackendMetaData.getPassword();
String hostName = sftpBackendMetaData.getHostName();
Integer port = sftpBackendMetaData.getPort();
byte[] privateKey = sftpBackendMetaData.getPrivateKey();
String username = sftpBackendMetaData.getUsername();
String password = sftpBackendMetaData.getPassword();
String hostName = sftpBackendMetaData.getHostName();
Integer port = sftpBackendMetaData.getPort();
if(backendMetaData.getUsesVariants())
{
@ -156,11 +146,6 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
password = variantRecord.getValueString(fieldNameMap.get(SFTPBackendVariantSetting.PASSWORD));
}
if(fieldNameMap.containsKey(SFTPBackendVariantSetting.PRIVATE_KEY))
{
privateKey = variantRecord.getValueByteArray(fieldNameMap.get(SFTPBackendVariantSetting.PRIVATE_KEY));
}
if(fieldNameMap.containsKey(SFTPBackendVariantSetting.HOSTNAME))
{
hostName = variantRecord.getValueString(fieldNameMap.get(SFTPBackendVariantSetting.HOSTNAME));
@ -172,9 +157,9 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
}
}
makeConnection(username, hostName, port, password, privateKey);
makeConnection(username, hostName, port, password);
}
catch(Exception e)
catch(IOException e)
{
throw (new QException("Error setting up SFTP connection", e));
}
@ -212,30 +197,10 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
/***************************************************************************
**
***************************************************************************/
protected SftpClient makeConnection(String username, String hostName, Integer port, String password, byte[] privateKeyBytes) throws Exception
protected SftpClient makeConnection(String username, String hostName, Integer port, String password) throws IOException
{
this.clientSession = SshClientManager.getInstance().connect(username, hostName, port).verify().getSession();
//////////////////////////////////////////////////////////////////////
// if we have private key bytes, use them to add publicKey identity //
//////////////////////////////////////////////////////////////////////
if(privateKeyBytes != null && privateKeyBytes.length > 0)
{
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
PublicKey publicKey = KeyUtils.recoverPublicKey(privateKey);
clientSession.addPublicKeyIdentity(new KeyPair(publicKey, privateKey));
}
//////////////////////////////////////////////////
// if we have a password, add password identity //
//////////////////////////////////////////////////
if(StringUtils.hasContent(password))
{
clientSession.addPasswordIdentity(password);
}
clientSession.addPasswordIdentity(password);
clientSession.auth().verify();
this.sftpClient = SftpClientFactory.instance().createSftpClient(clientSession);
@ -302,29 +267,27 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
**
***************************************************************************/
@Override
public List<SFTPDirEntryWithPath> listFiles(QTableMetaData table, QBackendMetaData backendBase, String requestedPath) throws QException
public List<SFTPDirEntryWithPath> listFiles(QTableMetaData table, QBackendMetaData backendBase, QQueryFilter filter) throws QException
{
String fullPath = null;
try
{
fullPath = getFullBasePath(table, backendBase);
if(StringUtils.hasContent(requestedPath))
String fullPath = getFullBasePath(table, backendBase);
// todo - move somewhere shared
// todo - should all do this?
if(filter != null)
{
fullPath = stripDuplicatedSlashes(fullPath + File.separatorChar + requestedPath + File.separatorChar);
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
{
if(isPathEqualsCriteria(criteria))
{
fullPath = stripDuplicatedSlashes(fullPath + File.separatorChar + criteria.getValues().get(0) + File.separatorChar);
}
}
}
List<SFTPDirEntryWithPath> rs = new ArrayList<>();
/////////////////////////////////////////////////////////////////////////////////////
// at least in some cases, listing / seems to be interpreted by the server as //
// a listing from the root of the system, not just the user's dir. so, converting //
// paths starting with / to instead be ./ is giving us better results. //
/////////////////////////////////////////////////////////////////////////////////////
if(fullPath.startsWith("/"))
{
fullPath = "." + fullPath;
}
for(SftpClient.DirEntry dirEntry : sftpClient.readDir(fullPath))
{
if(".".equals(dirEntry.getFilename()) || "..".equals(dirEntry.getFilename()))
@ -345,12 +308,6 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
}
catch(Exception e)
{
SftpException sftpException = ExceptionUtils.findClassInRootChain(e, SftpException.class);
if(sftpException != null)
{
throw new QUserFacingException("SFTP Exception listing [" + Objects.requireNonNullElse(fullPath, "") + "]: " + sftpException.getMessage());
}
throw (new QException("Error listing files", e));
}
}
@ -394,16 +351,9 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
**
***************************************************************************/
@Override
public void deleteFile(QTableMetaData table, String fileReference) throws FilesystemException
public void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException
{
try
{
sftpClient.remove(fileReference);
}
catch(Exception e)
{
throw (new FilesystemException("Error deleting file from SFTP", e));
}
throw (new QRuntimeException("Not yet implemented"));
}
@ -431,19 +381,4 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
return (sftpClient);
}
/***************************************************************************
** take a string, which is the contents of a PEM file (like a private key)
** - and if it has the -----BEGIN...----- and -----END...---- lines, strip
** them away, and strip away any whitespace, and then base-64 decode it.
***************************************************************************/
public static byte[] pemStringToDecodedBytes(String pemString)
{
String base64 = pemString.replaceAll("-----BEGIN (.*?)-----", "")
.replaceAll("-----END (.*?)-----", "")
.replaceAll("\\s", "");
return Base64.getDecoder().decode(base64);
}
}

View File

@ -26,12 +26,13 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
**
*******************************************************************************/
public class SFTPDeleteAction extends AbstractSFTPAction implements DeleteInterface
public class SFTPDeleteAction implements DeleteInterface
{
/*******************************************************************************
@ -39,19 +40,21 @@ public class SFTPDeleteAction extends AbstractSFTPAction implements DeleteInterf
*******************************************************************************/
public DeleteOutput execute(DeleteInput deleteInput) throws QException
{
return (executeDelete(deleteInput));
}
throw new NotImplementedException("SFTP delete not implemented");
/*
try
{
DeleteResult rs = new DeleteResult();
QTableMetaData table = deleteRequest.getTable();
/*******************************************************************************
** Specify whether this particular module's update action can & should fetch
** records before updating them, e.g., for audits or "not-found-checks"
*******************************************************************************/
@Override
public boolean supportsPreFetchQuery()
{
return (false);
// return rs;
}
catch(Exception e)
{
throw new QException("Error executing delete: " + e.getMessage(), e);
}
*/
}
}

View File

@ -37,7 +37,7 @@ public class SFTPTestConnectionAction extends AbstractSFTPAction
***************************************************************************/
public SFTPTestConnectionTestOutput testConnection(SFTPTestConnectionTestInput input)
{
try(SftpClient sftpClient = super.makeConnection(input.getUsername(), input.getHostName(), input.getPort(), input.getPassword(), input.getPrivateKey()))
try(SftpClient sftpClient = super.makeConnection(input.getUsername(), input.getHostName(), input.getPort(), input.getPassword()))
{
SFTPTestConnectionTestOutput output = new SFTPTestConnectionTestOutput().withIsConnectionSuccess(true);
@ -80,7 +80,6 @@ public class SFTPTestConnectionAction extends AbstractSFTPAction
private Integer port;
private String password;
private String basePath;
private byte[] privateKey;
@ -252,39 +251,6 @@ public class SFTPTestConnectionAction extends AbstractSFTPAction
return (this);
}
/*******************************************************************************
** Getter for privateKey
**
*******************************************************************************/
public byte[] getPrivateKey()
{
return privateKey;
}
/*******************************************************************************
** Setter for privateKey
**
*******************************************************************************/
public void setPrivateKey(byte[] privateKey)
{
this.privateKey = privateKey;
}
/*******************************************************************************
** Fluent setter for privateKey
**
*******************************************************************************/
public SFTPTestConnectionTestInput withPrivateKey(byte[] privateKey)
{
this.privateKey = privateKey;
return (this);
}
}

View File

@ -34,7 +34,6 @@ public class SFTPBackendMetaData extends AbstractFilesystemBackendMetaData
private String username;
private String password;
private String hostName;
private byte[] privateKey;
private Integer port;
@ -196,35 +195,4 @@ public class SFTPBackendMetaData extends AbstractFilesystemBackendMetaData
return (this);
}
/*******************************************************************************
** Getter for privateKey
*******************************************************************************/
public byte[] getPrivateKey()
{
return (this.privateKey);
}
/*******************************************************************************
** Setter for privateKey
*******************************************************************************/
public void setPrivateKey(byte[] privateKey)
{
this.privateKey = privateKey;
}
/*******************************************************************************
** Fluent setter for privateKey
*******************************************************************************/
public SFTPBackendMetaData withPrivateKey(byte[] privateKey)
{
this.privateKey = privateKey;
return (this);
}
}

View File

@ -34,6 +34,5 @@ public enum SFTPBackendVariantSetting implements BackendVariantSetting
PASSWORD,
HOSTNAME,
PORT,
BASE_PATH,
PRIVATE_KEY
BASE_PATH
}

View File

@ -34,9 +34,7 @@ import java.util.concurrent.atomic.AtomicReference;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.common.SftpException;
import org.jetbrains.annotations.NotNull;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -47,7 +45,6 @@ public class SFTPOutputStream extends PipedOutputStream
private static final QLogger LOG = QLogger.getLogger(SFTPOutputStream.class);
private final SftpClient sftpClient;
private final String path;
private final PipedInputStream pipedInputStream;
private final Future<?> putFuture;
@ -65,14 +62,12 @@ public class SFTPOutputStream extends PipedOutputStream
pipedInputStream = new PipedInputStream(this, 32 * 1024);
this.sftpClient = sftpClient;
this.path = path;
putFuture = Executors.newSingleThreadExecutor().submit(() ->
{
try
{
started.set(true);
LOG.debug("Starting sftp put", logPair("path", path));
sftpClient.put(pipedInputStream, path);
}
catch(Exception e)
@ -110,10 +105,6 @@ public class SFTPOutputStream extends PipedOutputStream
{
if(putException.get() != null)
{
if(putException.get() instanceof SftpException sftpException)
{
throw new IOException("Error performing SFTP put for path [" + path + "]: " + sftpException.getMessage());
}
throw new IOException("Error performing SFTP put", putException.get());
}

View File

@ -26,6 +26,9 @@ import java.io.File;
import java.io.IOException;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -38,6 +41,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
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;
@ -93,59 +97,55 @@ public class FilesystemBackendModuleTest
/////////////////////////////////////////
// filter for a file name that's found //
/////////////////////////////////////////
files = abstractFilesystemAction.listFiles(table, backend, "BLOB-2.txt");
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt")));
assertEquals(1, files.size());
assertEquals("BLOB-2.txt", files.get(0).getName());
files = abstractFilesystemAction.listFiles(table, backend, "BLOB-1.txt");
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt")));
assertEquals(1, files.size());
assertEquals("BLOB-1.txt", files.get(0).getName());
///////////////////////////
// not supported anymore //
///////////////////////////
// ///////////////////////////////////
// // filter for 2 names that exist //
// ///////////////////////////////////
// files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-1.txt", "BLOB-2.txt")));
// assertEquals(2, files.size());
///////////////////////////////////
// filter for 2 names that exist //
///////////////////////////////////
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-1.txt", "BLOB-2.txt")));
assertEquals(2, files.size());
/////////////////////////////////////////////
// filter for a file name that isn't found //
/////////////////////////////////////////////
files = abstractFilesystemAction.listFiles(table, backend, "NOT-FOUND.txt");
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "NOT-FOUND.txt")));
assertEquals(0, files.size());
///////////////////////////
// not supported anymore //
///////////////////////////
// files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-2.txt", "NOT-FOUND.txt")));
// assertEquals(1, files.size());
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-2.txt", "NOT-FOUND.txt")));
assertEquals(1, files.size());
///////////////////////////
// not supported anymore //
///////////////////////////
// ////////////////////////////////////////////////////
// // 2 criteria, and'ed, and can't match, so find 0 //
// ////////////////////////////////////////////////////
// files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt")));
// assertEquals(0, files.size());
////////////////////////////////////////////////////
// 2 criteria, and'ed, and can't match, so find 0 //
////////////////////////////////////////////////////
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt")));
assertEquals(0, files.size());
// //////////////////////////////////////////////////
// // 2 criteria, or'ed, and both match, so find 2 //
// //////////////////////////////////////////////////
// files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt"))
// .withBooleanOperator(QQueryFilter.BooleanOperator.OR));
// assertEquals(2, files.size());
//////////////////////////////////////////////////
// 2 criteria, or'ed, and both match, so find 2 //
//////////////////////////////////////////////////
files = abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt"))
.withBooleanOperator(QQueryFilter.BooleanOperator.OR));
assertEquals(2, files.size());
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// note that we used to try unsupported filters here, expecting them to throw - but those are //
// more-or-less now implemented in the base class's query method, so, no longer expected to throw here. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////
// ensure unsupported filters throw //
//////////////////////////////////////
assertThatThrownBy(() -> abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("foo", QCriteriaOperator.GREATER_THAN, 42))))
.rootCause()
.hasMessageContaining("Unable to query filesystem table by field");
assertThatThrownBy(() -> abstractFilesystemAction.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IS_BLANK))))
.rootCause()
.hasMessageContaining("Unable to query filename field using operator");
}
@ -165,7 +165,7 @@ public class FilesystemBackendModuleTest
List<File> filesBeforeDelete = new AbstractFilesystemAction().listFiles(table, qInstance.getBackendForTable(table.getName()));
FilesystemBackendModule filesystemBackendModule = new FilesystemBackendModule();
filesystemBackendModule.getActionBase().deleteFile(table, filesBeforeDelete.get(0).getAbsolutePath());
filesystemBackendModule.getActionBase().deleteFile(qInstance, table, filesBeforeDelete.get(0).getAbsolutePath());
List<File> filesAfterDelete = new AbstractFilesystemAction().listFiles(table, qInstance.getBackendForTable(table.getName()));
Assertions.assertEquals(filesBeforeDelete.size() - 1, filesAfterDelete.size(),
@ -191,7 +191,7 @@ public class FilesystemBackendModuleTest
List<File> filesBeforeDelete = new AbstractFilesystemAction().listFiles(table, qInstance.getBackendForTable(table.getName()));
FilesystemBackendModule filesystemBackendModule = new FilesystemBackendModule();
filesystemBackendModule.getActionBase().deleteFile(table, PATH_THAT_WONT_EXIST);
filesystemBackendModule.getActionBase().deleteFile(qInstance, table, PATH_THAT_WONT_EXIST);
List<File> filesAfterDelete = new AbstractFilesystemAction().listFiles(table, qInstance.getBackendForTable(table.getName()));
Assertions.assertEquals(filesBeforeDelete.size(), filesAfterDelete.size(),

View File

@ -22,19 +22,11 @@
package com.kingsrook.qqq.backend.module.filesystem.local.actions;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
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.module.filesystem.TestUtils;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
@ -42,25 +34,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
*******************************************************************************/
public class FilesystemDeleteActionTest extends FilesystemActionTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testSuccessfulDeleteMultiple() throws QException
public void test() throws QException
{
int initialCount = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_BLOB_LOCAL_FS)).getCount();
String filename1 = "A.txt";
String filename2 = "B.txt";
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_BLOB_LOCAL_FS).withRecords(List.of(
new QRecord().withValue("fileName", filename1).withValue("contents", "bytes"),
new QRecord().withValue("fileName", filename2).withValue("contents", "bytes"))));
assertEquals(initialCount + 2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_BLOB_LOCAL_FS)).getCount());
DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(TestUtils.TABLE_NAME_BLOB_LOCAL_FS).withPrimaryKeys(List.of(filename1, filename2)));
assertEquals(2, deleteOutput.getDeletedRecordCount());
assertEquals(0, deleteOutput.getRecordsWithErrors().size());
assertEquals(initialCount, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_BLOB_LOCAL_FS)).getCount());
assertThrows(NotImplementedException.class, () -> new FilesystemDeleteAction().execute(new DeleteInput()));
}
}

View File

@ -26,6 +26,9 @@ import java.util.List;
import java.util.UUID;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -35,6 +38,7 @@ import com.kingsrook.qqq.backend.module.filesystem.s3.actions.AbstractS3Action;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -73,59 +77,53 @@ public class S3BackendModuleTest extends BaseS3Test
/////////////////////////////////////////
// filter for a file name that's found //
/////////////////////////////////////////
files = actionBase.listFiles(table, backend, "BLOB-2.txt");
files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt")));
assertEquals(1, files.size());
assertThat(files.get(0).getKey()).contains("BLOB-2.txt");
files = actionBase.listFiles(table, backend, "BLOB-1.txt");
files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt")));
assertEquals(1, files.size());
assertThat(files.get(0).getKey()).contains("BLOB-1.txt");
///////////////////////////
// not supported anymore //
///////////////////////////
// ///////////////////////////////////
// // filter for 2 names that exist //
// ///////////////////////////////////
// files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-1.txt", "BLOB-2.txt")));
// assertEquals(2, files.size());
///////////////////////////////////
// filter for 2 names that exist //
///////////////////////////////////
files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-1.txt", "BLOB-2.txt")));
assertEquals(2, files.size());
/////////////////////////////////////////////
// filter for a file name that isn't found //
/////////////////////////////////////////////
files = actionBase.listFiles(table, backend, "NOT-FOUND.txt");
files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "NOT-FOUND.txt")));
assertEquals(0, files.size());
///////////////////////////
// not supported anymore //
///////////////////////////
// files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-2.txt", "NOT-FOUND.txt")));
// assertEquals(1, files.size());
files = actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IN, "BLOB-2.txt", "NOT-FOUND.txt")));
assertEquals(1, files.size());
///////////////////////////
// not supported anymore //
///////////////////////////
// ////////////////////////////////////////////////////
// // 2 criteria, and'ed, and can't match, so find 0 //
// ////////////////////////////////////////////////////
// files = actionBase.listFiles(table, backend, new QQueryFilter(
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt")));
// assertEquals(0, files.size());
////////////////////////////////////////////////////
// 2 criteria, and'ed, and can't match, so find 0 //
////////////////////////////////////////////////////
files = actionBase.listFiles(table, backend, new QQueryFilter(
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt")));
assertEquals(0, files.size());
// //////////////////////////////////////////////////
// // 2 criteria, or'ed, and both match, so find 2 //
// //////////////////////////////////////////////////
// files = actionBase.listFiles(table, backend, new QQueryFilter(
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
// new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt"))
// .withBooleanOperator(QQueryFilter.BooleanOperator.OR));
// assertEquals(2, files.size());
//////////////////////////////////////////////////
// 2 criteria, or'ed, and both match, so find 2 //
//////////////////////////////////////////////////
files = actionBase.listFiles(table, backend, new QQueryFilter(
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"),
new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-2.txt"))
.withBooleanOperator(QQueryFilter.BooleanOperator.OR));
assertEquals(2, files.size());
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// note that we used to try unsupported filters here, expecting them to throw - but those are //
// more-or-less now implemented in the base class's query method, so, no longer expected to throw here. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////
// ensure unsupported filters throw //
//////////////////////////////////////
assertThatThrownBy(() -> actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("foo", QCriteriaOperator.GREATER_THAN, 42))))
.hasMessageContaining("Unable to query filesystem table by field");
assertThatThrownBy(() -> actionBase.listFiles(table, backend, new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.IS_BLANK))))
.hasMessageContaining("Unable to query filename field using operator");
}
@ -147,7 +145,7 @@ public class S3BackendModuleTest extends BaseS3Test
S3BackendModule s3BackendModule = new S3BackendModule();
AbstractS3Action actionBase = (AbstractS3Action) s3BackendModule.getActionBase();
actionBase.setS3Utils(getS3Utils());
actionBase.deleteFile(table, s3ObjectSummariesBeforeDelete.get(0).getKey());
actionBase.deleteFile(qInstance, table, s3ObjectSummariesBeforeDelete.get(0).getKey());
List<S3ObjectSummary> s3ObjectSummariesAfterDelete = getS3Utils().listObjectsInBucketMatchingGlob(BUCKET_NAME, TEST_FOLDER, "");
Assertions.assertEquals(s3ObjectSummariesBeforeDelete.size() - 1, s3ObjectSummariesAfterDelete.size(),
@ -176,7 +174,7 @@ public class S3BackendModuleTest extends BaseS3Test
AbstractS3Action actionBase = (AbstractS3Action) s3BackendModule.getActionBase();
actionBase.setS3Utils(getS3Utils());
String path = "//" + s3ObjectSummariesBeforeDelete.get(0).getKey().replaceAll("/", "//");
actionBase.deleteFile(table, "//" + path);
actionBase.deleteFile(qInstance, table, "//" + path);
List<S3ObjectSummary> s3ObjectSummariesAfterDelete = getS3Utils().listObjectsInBucketMatchingGlob(BUCKET_NAME, TEST_FOLDER, "");
Assertions.assertEquals(s3ObjectSummariesBeforeDelete.size() - 1, s3ObjectSummariesAfterDelete.size(),
@ -203,7 +201,7 @@ public class S3BackendModuleTest extends BaseS3Test
S3BackendModule s3BackendModule = new S3BackendModule();
AbstractS3Action actionBase = (AbstractS3Action) s3BackendModule.getActionBase();
actionBase.setS3Utils(getS3Utils());
actionBase.deleteFile(table, PATH_THAT_WONT_EXIST);
actionBase.deleteFile(qInstance, table, PATH_THAT_WONT_EXIST);
List<S3ObjectSummary> s3ObjectSummariesAfterDelete = getS3Utils().listObjectsInBucketMatchingGlob(BUCKET_NAME, TEST_FOLDER, "");
Assertions.assertEquals(s3ObjectSummariesBeforeDelete.size(), s3ObjectSummariesAfterDelete.size(),

View File

@ -22,19 +22,12 @@
package com.kingsrook.qqq.backend.module.filesystem.s3.actions;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
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.metadata.QInstance;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
@ -49,42 +42,7 @@ public class S3DeleteActionTest extends BaseS3Test
@Test
public void test() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
int initialCount = count(TestUtils.TABLE_NAME_BLOB_S3);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_BLOB_S3);
insertInput.setRecords(List.of(
new QRecord().withValue("fileName", "file2.txt").withValue("contents", "Hi, Bob.")));
S3InsertAction insertAction = new S3InsertAction();
insertAction.setS3Utils(getS3Utils());
insertAction.execute(insertInput);
assertEquals(initialCount + 1, count(TestUtils.TABLE_NAME_BLOB_S3));
S3DeleteAction deleteAction = new S3DeleteAction();
deleteAction.setS3Utils(getS3Utils());
DeleteOutput deleteOutput = deleteAction.execute(new DeleteInput(TestUtils.TABLE_NAME_BLOB_S3).withPrimaryKeys(List.of("file2.txt")));
assertEquals(1, deleteOutput.getDeletedRecordCount());
assertEquals(0, deleteOutput.getRecordsWithErrors().size());
assertEquals(initialCount, count(TestUtils.TABLE_NAME_BLOB_S3));
}
/***************************************************************************
**
***************************************************************************/
private Integer count(String tableName) throws QException
{
CountInput countInput = new CountInput();
countInput.setTableName(tableName);
S3CountAction s3CountAction = new S3CountAction();
s3CountAction.setS3Utils(getS3Utils());
CountOutput countOutput = s3CountAction.execute(countInput);
return countOutput.getCount();
assertThrows(NotImplementedException.class, () -> new S3DeleteAction().execute(new DeleteInput()));
}
}

View File

@ -43,8 +43,8 @@ public class BaseSFTPTest extends BaseTest
public static final String TABLE_FOLDER = "files";
public static final String REMOTE_DIR = "/home/" + USERNAME + "/" + BACKEND_FOLDER + "/" + TABLE_FOLDER;
protected static GenericContainer<?> sftpContainer;
private static Integer currentPort;
private static GenericContainer<?> sftpContainer;
private static Integer currentPort;
@ -67,22 +67,10 @@ public class BaseSFTPTest extends BaseTest
grantUploadFilesDirWritePermission();
///////////////////////////////////////////////////
// add our test-only public key to the container //
///////////////////////////////////////////////////
String sshDir = "/home/" + USERNAME + "/.ssh";
sftpContainer.execInContainer("mkdir", sshDir);
sftpContainer.execInContainer("chmod", "700", sshDir);
sftpContainer.execInContainer("chown", USERNAME, sshDir);
copyFileToContainer("test-only-key.pub", sshDir + "/authorized_keys");
sftpContainer.execInContainer("chmod", "600", sshDir + "/authorized_keys");
sftpContainer.execInContainer("chown", USERNAME, sshDir + "/authorized_keys");
currentPort = sftpContainer.getMappedPort(22);
}
/***************************************************************************
**
***************************************************************************/
@ -92,7 +80,6 @@ public class BaseSFTPTest extends BaseTest
}
/***************************************************************************
**
***************************************************************************/
@ -102,7 +89,6 @@ public class BaseSFTPTest extends BaseTest
}
/***************************************************************************
**
***************************************************************************/

View File

@ -22,23 +22,12 @@
package com.kingsrook.qqq.backend.module.filesystem.sftp.actions;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
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.module.filesystem.TestUtils;
import com.kingsrook.qqq.backend.module.filesystem.sftp.BaseSFTPTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.apache.commons.lang.NotImplementedException;
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.assertThrows;
/*******************************************************************************
@ -46,69 +35,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
*******************************************************************************/
public class SFTPDeleteActionTest extends BaseSFTPTest
{
private String filesBasename = "delete-test-";
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfterEach() throws Exception
{
rmrfInContainer(REMOTE_DIR + "/" + filesBasename + "*");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testSuccessfulDeleteMultiple() throws QException
public void test() throws QException
{
int initialCount = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_SFTP_FILE)).getCount();
String filename1 = filesBasename + "A.txt";
String filename2 = filesBasename + "B.txt";
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SFTP_FILE).withRecords(List.of(
new QRecord().withValue("fileName", filename1).withValue("contents", "bytes"),
new QRecord().withValue("fileName", filename2).withValue("contents", "bytes"))));
assertEquals(initialCount + 2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_SFTP_FILE)).getCount());
DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(TestUtils.TABLE_NAME_SFTP_FILE).withPrimaryKeys(List.of(filename1, filename2)));
assertEquals(2, deleteOutput.getDeletedRecordCount());
assertEquals(0, deleteOutput.getRecordsWithErrors().size());
assertEquals(initialCount, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_SFTP_FILE)).getCount());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testFailedDelete() throws Exception
{
int initialCount = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_SFTP_FILE)).getCount();
String filename1 = filesBasename + "C.txt";
String filename2 = filesBasename + "D.txt";
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SFTP_FILE).withRecords(List.of(
new QRecord().withValue("fileName", filename1).withValue("contents", "bytes"),
new QRecord().withValue("fileName", filename2).withValue("contents", "bytes"))));
assertEquals(initialCount + 2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_SFTP_FILE)).getCount());
sftpContainer.execInContainer("chmod", "000", REMOTE_DIR);
DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(TestUtils.TABLE_NAME_SFTP_FILE).withPrimaryKeys(List.of(filename1, filename2)));
sftpContainer.execInContainer("chmod", "777", REMOTE_DIR);
assertEquals(0, deleteOutput.getDeletedRecordCount());
assertEquals(2, deleteOutput.getRecordsWithErrors().size());
assertThat(deleteOutput.getRecordsWithErrors().get(0).getErrorsAsString()).contains("Error deleting file: Permission denied");
assertThat(deleteOutput.getRecordsWithErrors().get(1).getErrorsAsString()).contains("Error deleting file: Permission denied");
assertEquals(initialCount + 2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_SFTP_FILE)).getCount());
assertThrows(NotImplementedException.class, () -> new SFTPDeleteAction().execute(new DeleteInput()));
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.module.filesystem.sftp.actions;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -65,12 +66,61 @@ class SFTPQueryActionTest extends BaseSFTPTest
**
*******************************************************************************/
@Test
public void testSimpleQueryForOneFile() throws QException
void testQueryWithPath() throws Exception
{
QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_SFTP_FILE);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "testfile-1.txt")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows from unfiltered query");
String subfolderPath = "/home/" + USERNAME + "/" + BACKEND_FOLDER + "/" + TABLE_FOLDER + "/subfolder/";
try
{
copyFileToContainer("files/testfile.txt", subfolderPath + "/sub1.txt");
copyFileToContainer("files/testfile.txt", subfolderPath + "/sub2.txt");
QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_SFTP_FILE)
.withFilter(new QQueryFilter(new QFilterCriteria("path", QCriteriaOperator.EQUALS, "subfolder")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows from subfolder path query");
}
finally
{
rmrfInContainer(subfolderPath);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryWithPathAndNameLike() throws Exception
{
String subfolderPath = "/home/" + USERNAME + "/" + BACKEND_FOLDER + "/" + TABLE_FOLDER + "/subfolder/";
try
{
copyFileToContainer("files/testfile.txt", subfolderPath + "/sub1.txt");
copyFileToContainer("files/testfile.txt", subfolderPath + "/sub2.txt");
copyFileToContainer("files/testfile.txt", subfolderPath + "/who.txt");
Map<String, Integer> patternExpectedCountMap = Map.of(
"%.txt", 3,
"sub%", 2,
"%1%", 1,
"%", 3,
"*", 0
);
for(Map.Entry<String, Integer> entry : patternExpectedCountMap.entrySet())
{
QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_SFTP_FILE).withFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("path", QCriteriaOperator.EQUALS, "subfolder"))
.withCriteria(new QFilterCriteria("baseName", QCriteriaOperator.LIKE, entry.getKey())));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Assertions.assertEquals(entry.getValue(), queryOutput.getRecords().size(), "Expected # of rows from subfolder path, baseName like: " + entry.getKey());
}
}
finally
{
rmrfInContainer(subfolderPath);
}
}

View File

@ -22,11 +22,7 @@
package com.kingsrook.qqq.backend.module.filesystem.sftp.actions;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.filesystem.sftp.BaseSFTPTest;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -161,7 +157,6 @@ class SFTPTestConnectionActionTest extends BaseSFTPTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -180,27 +175,4 @@ class SFTPTestConnectionActionTest extends BaseSFTPTest
assertNull(output.getListBasePathErrorMessage());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testConnectViaPublicKey() throws Exception
{
try(InputStream resourceAsStream = getClass().getResourceAsStream("/test-only-key"))
{
byte[] privateKeyBytes = AbstractSFTPAction.pemStringToDecodedBytes(StringUtils.join("", IOUtils.readLines(resourceAsStream, StandardCharsets.UTF_8)));
SFTPTestConnectionAction.SFTPTestConnectionTestInput input = new SFTPTestConnectionAction.SFTPTestConnectionTestInput()
.withUsername(BaseSFTPTest.USERNAME)
.withPrivateKey(privateKeyBytes)
.withPort(BaseSFTPTest.getCurrentPort())
.withHostName(BaseSFTPTest.HOST_NAME);
SFTPTestConnectionAction.SFTPTestConnectionTestOutput output = new SFTPTestConnectionAction().testConnection(input);
assertTrue(output.getIsConnectionSuccess());
assertNull(output.getConnectionErrorMessage());
}
}
}

View File

@ -1,11 +0,0 @@
The `test-only-key` / `test-only-key.pub` key pair in this directory was generated via:
```shell
ssh-keygen -t rsa -b 4096 -m PEM -f test-only-key
openssl pkcs8 -topk8 -inform PEM -in test-only-key -outform PEM -nocrypt -out test-only-key-kpcs8.pem
cp test-only-key-kpcs8.pem .../src/test/resources/test-only-key
```
It is NOT meant to be used as a secure key in ANY environment.
It is included in this repo ONLY to be used for basic unit testing.

View File

@ -1,52 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDGEYxHZMPM6A+4
OtayotUrB94AqYow1PcK5HzY8psKuHYejurugwFBbrFHq+dOso6EkLKcSpjvEX+X
Se+m3CDIC6qVuLBS6fmWe6wo73fhlqyEP6rslz//v5+Di8fhCPSfa8y4X5RLbkDR
yrL/ilmIEHUcmCU82SFFp9C10W+Ir9SZbjAoiRHT+X/WWmXYnMrfLHl38xxdWr8O
qXEmLaVxmSaLnWAmMXRr4Nqrk7r7VVq899CZSJMVLSfqeiJPqyLkwOPcYjzOKft0
hYAhIXCUsEK4Wv7R1t9SooC7nqE2mY/htBcrPPyDhTT4Kl0zAD2MNloNUUQsTnic
t3FCUy0pB9zr26bZ4YHBIvdReTWcx6IlH2U28V9ktxWRJe/ESqay4RTfif24aWya
WzD9+yTowTdPJbMaQZI5XGYJBfFtXr8aC8Di+VuILbriiuLk1iPuj1p2R3JXepON
sVRDBm10SmHdETcsZXB3glqqiV+mFGmHguPXSo1WZmP79+kj/phwtn2wFKRLwi6k
QNeqOxLbamRtj7y4M2sphEC7Cih1w4J+CzjZE8PC7+Bck+ldvmdtW6+wkkhLgli9
SvDwe2pM8eTWTznzs1Qj9My5p1v0uvIo0xADtpITzb3kQtl9+2C7uqscjJLIpc0g
VM5q55bFgxUp32G+HKUT8TI98ZrbEwIDAQABAoICAQC1f2MCEO3zGDs/YHtYmimo
Er917+W3tY7jJljZHAbCniFvAxt4kAdYhCxjNrzwumIqS8W/vgPCHlDCu3eleVV4
umgIZoL8l3akVJN/t2AtEbroPMdNoZN9sYRkMHRqW6B9bXTdBoiHTnKLS6kWzRoZ
uqr2Ft0YkwcQIyT3VwFTSXwRVI1At8nkal6gd5mYEqU8OC7eoaG+Ued9cftDNtTB
8csGaKGwneTG7fay/t56bM6HFrbJn11YLFbFYEGMq49/+tlSG5sIeCP5tFOjCFd3
iMS61ndmpfVibZJ1Wnjz5WeZNUN91Za2lhvhxEA++dtsXmnKhktoJkgTo21fj4Ry
1DbdysR5W0vz+HQ5usT/fWAypKj4KSlP6rsL/zkqmlxW1qE/nwzK6UN6kVWvO1Sm
6EcZIQKTRpkGFXluOJL/Bkqg+4Ayl9G3veLON5yTOB367m1haI/oIgr+jOKrkzdG
PWWNZ78USCT9YD/P2vHkwVQ/uzF3kHhLL6n8hzunFpJk+5eScXsyXTUjDCn4HurE
A8Z1FHLOeu4EEKODEdg1PCa34/8Z0K+88G5G/sTDWLf2MjywdCSDzKxYiSpQW0gt
5I5WgQG8CiGHbysBWMnqjoG1J+QpAUDzT29CuLhVbn4i0uiTe0AZ0xLjravhd5dW
dlItzvOY1rj5jHPs5hiWkQKCAQEA97jAuCzPw2Ats2QUfpnqEd/ITKRYTtK6zo+c
sdc1JH0RnzxCeBr2BApSrfMbqAMrPaiUKk5FUqpF5q47AXGrYFlaAPe7cq+Lr97N
LRi7NHM9RdF1myollCghwgKkq8qUe64eNAypMtGOdjrMh7kC879udESYXLjqgT5H
wQbHF5IRvg5vBkVKxzLrg37lX1f0MVGdB8VxA+QRQ4egFuQApPvMLFVenYZL165r
u3OIpOQWcJ91L5VzN5jJMR1x8VWFR6iD4PxachdmD8qaNxfwWKTTWxkpwTXfW1EI
68NZ2s9RmuRbJEfOtiznfzdVL+lAibeMe7dvUMzIevr0hPzMNwKCAQEAzLADz691
9bJniRVFUkHlbXRV7k0sRxAXNzGmar8xpnG3wJaW+zSsj8Nnr+aSBGgGWmmldEn7
tiHlozrjWUgUEsRJKObiCrdGsTC7+0flQLs1bukLfgZSnP3oMDYAgq15Vaw49oCU
M4KxRGfkPEhwP/DZClHsYkPr6HegT2/21z8AFHTAxknGjWWGJJAFxwaog7Akugdy
gXLb7lU4SjCJdb5tR1c7aDEUOvDDu1iffhWtt5Tp9BL0dKlN4M+6XZvqSNiVlN6P
BB5gDuSa0qEewIbMWiT4rcvE7gCSXFEWPnGbtHU7QcI4Wx1F75Y4CRgs2rRlnj9j
bVAsRNIOTqkSBQKCAQEAliEL+xJ9X6TcTYnrucZBy09aLsizFCI2QJVcm5MXi+OY
WG7Gwc9lJZG0BeP98Nbqz9Vo5jLFZJH5BxK0g+2FtUCxgUCiA6FMAOwAYMJKQkFM
8xE8OytR1vZzbwb3EX4WetZNS7IYoMnLku+ToPWJSnvLzv77b8ZJqMY76knXQvut
cQeCVcSMyyia/vhavmupfHI/vsPz+C2yIMEDTpwjn9lSJdQfIUyQjkgQ1mvwdi4d
Q2gANzRVvW4FEJUNxvrTaVhBhIqrrdVsb0mUKKuDZ9WMmfsoCQZDNS5pP6kGvctD
Y6HdcqFqL5ILQlggcobkLBJnO1syRT+2iIGqyyYCBQKCAQAbwy/xJnJQbe8/F6R8
YLW2n9Xb6Zm81cDgWpqg1eftFHWA6Kv3zJAvO6i/of1iHZ3m+3dWi4ZZkMVt21nk
zTLzzK3Dn3U/UNaEyABnN7wviHTZ40AMyty/sGyixWBSWScg6KgdPxla1zol9hVt
28Fl2swFa1EtjtrbgAY9YAlR7pibLa7L9ku49/E22lX+RbfrjKOem837ItITxHlL
DsRGNRrrVziWjDmbOPbDXWTcnCIgyVDmKv//JsuKV4KGmdQwJzg6pekt/NS4kGcz
dGkQYfgrreIQ6JeAVJGFdfYXaB9fXZs48xfju9e1hGF7Uk0bKOazjRN2Sy6F8xu/
rYzlAoIBAEjY7u3Jmntn1AYsbuy9wTblKl1IaZP4ST+X0/dLtvW8ZLsx0jPGwMXx
xmOku5OGqPjCn5i8Ws2KS8O6O+7lGm/CHXvmDpozD3wpjnJ64SgoLnjrT8R78TEJ
UjsGQfR7ofSj4heR7TgEPp+n0SXse3qERd6VZ5YPuzGva1iVJogErwI58QU2QaxQ
0ONV6F8oZuXjUs9KRhXQ8W0i87m0P7/ZumhqPaQqY/MeAYF/ED5C6ETKISxaDqs/
zd/jf6uPZL6P4DPWcw7cSk5/aNZZ0P+/BkEX33WHBDSdVyHC+ydMcYZBrrlWKoSt
sNTITZbKrQB4hwHdawpMHxh+5mRXLk0=
-----END PRIVATE KEY-----

View File

@ -1 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGEYxHZMPM6A+4OtayotUrB94AqYow1PcK5HzY8psKuHYejurugwFBbrFHq+dOso6EkLKcSpjvEX+XSe+m3CDIC6qVuLBS6fmWe6wo73fhlqyEP6rslz//v5+Di8fhCPSfa8y4X5RLbkDRyrL/ilmIEHUcmCU82SFFp9C10W+Ir9SZbjAoiRHT+X/WWmXYnMrfLHl38xxdWr8OqXEmLaVxmSaLnWAmMXRr4Nqrk7r7VVq899CZSJMVLSfqeiJPqyLkwOPcYjzOKft0hYAhIXCUsEK4Wv7R1t9SooC7nqE2mY/htBcrPPyDhTT4Kl0zAD2MNloNUUQsTnict3FCUy0pB9zr26bZ4YHBIvdReTWcx6IlH2U28V9ktxWRJe/ESqay4RTfif24aWyaWzD9+yTowTdPJbMaQZI5XGYJBfFtXr8aC8Di+VuILbriiuLk1iPuj1p2R3JXepONsVRDBm10SmHdETcsZXB3glqqiV+mFGmHguPXSo1WZmP79+kj/phwtn2wFKRLwi6kQNeqOxLbamRtj7y4M2sphEC7Cih1w4J+CzjZE8PC7+Bck+ldvmdtW6+wkkhLgli9SvDwe2pM8eTWTznzs1Qj9My5p1v0uvIo0xADtpITzb3kQtl9+2C7uqscjJLIpc0gVM5q55bFgxUp32G+HKUT8TI98ZrbEw== test-only-key

View File

@ -82,7 +82,6 @@ public class FieldAdornment implements ToSchema
{
EnumSet<AdornmentType> subSet = EnumSet.allOf(AdornmentType.class);
subSet.remove(AdornmentType.FILE_UPLOAD); // todo - remove for next version!
subSet.remove(AdornmentType.TOOLTIP); // todo - remove for next version!
FieldAdornmentSubSet.subSet = subSet;
}

View File

@ -68,7 +68,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-material-dashboard</artifactId>
<version>0.24.0</version>
<version>0.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>