Merged feature/workflows-support into integration

This commit is contained in:
2025-06-02 09:02:39 -05:00
9 changed files with 293 additions and 14 deletions

View File

@ -1416,7 +1416,7 @@ public class QInstanceEnricher
if(table != null) if(table != null)
{ {
String primaryKeyField = table.getPrimaryKeyField(); String primaryKeyField = table.getPrimaryKeyField();
QFieldMetaData primaryKeyFieldMetaData = table.getFields().get(primaryKeyField); QFieldMetaData primaryKeyFieldMetaData = CollectionUtils.nonNullMap(table.getFields()).get(primaryKeyField);
if(primaryKeyFieldMetaData != null) if(primaryKeyFieldMetaData != null)
{ {
possibleValueSource.setIdType(primaryKeyFieldMetaData.getType()); possibleValueSource.setIdType(primaryKeyFieldMetaData.getType());

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata.qbits; 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; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
@ -33,7 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
** Specifically exists to accept the QBitConfig as a type parameter and a value, ** Specifically exists to accept the QBitConfig as a type parameter and a value,
** easily accessed in the producer's methods as getQBitConfig() ** easily accessed in the producer's methods as getQBitConfig()
*******************************************************************************/ *******************************************************************************/
public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOutput, C extends QBitConfig> implements MetaDataProducerInterface<T> public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOutput, C extends QBitConfig> implements QBitComponentMetaDataProducerInterface<T, C>
{ {
private C qBitConfig = null; private C qBitConfig = null;
@ -42,6 +41,7 @@ public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOu
/******************************************************************************* /*******************************************************************************
** Getter for qBitConfig ** Getter for qBitConfig
*******************************************************************************/ *******************************************************************************/
@Override
public C getQBitConfig() public C getQBitConfig()
{ {
return (this.qBitConfig); return (this.qBitConfig);
@ -52,6 +52,7 @@ public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOu
/******************************************************************************* /*******************************************************************************
** Setter for qBitConfig ** Setter for qBitConfig
*******************************************************************************/ *******************************************************************************/
@Override
public void setQBitConfig(C qBitConfig) public void setQBitConfig(C qBitConfig)
{ {
this.qBitConfig = qBitConfig; this.qBitConfig = qBitConfig;

View File

@ -0,0 +1,50 @@
/*
* 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 interface QBitComponentMetaDataProducerInterface<T extends MetaDataProducerOutput, C extends QBitConfig> extends MetaDataProducerInterface<T>
{
/*******************************************************************************
** Getter for qBitConfig
*******************************************************************************/
C getQBitConfig();
/*******************************************************************************
** Setter for qBitConfig
*******************************************************************************/
void setQBitConfig(C qBitConfig);
}

View File

@ -81,9 +81,9 @@ public interface QBitProducer
/////////////////////////////// ///////////////////////////////
for(MetaDataProducerInterface<?> producer : producers) for(MetaDataProducerInterface<?> producer : producers)
{ {
if(producer instanceof QBitComponentMetaDataProducer<?, ?>) if(producer instanceof QBitComponentMetaDataProducerInterface<?,?>)
{ {
QBitComponentMetaDataProducer<?, C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducer<?, C>) producer; QBitComponentMetaDataProducerInterface<?,C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducerInterface<?,C>) producer;
qBitComponentMetaDataProducer.setQBitConfig(qBitConfig); qBitComponentMetaDataProducer.setQBitConfig(qBitConfig);
} }

View File

@ -0,0 +1,169 @@
/*
* 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.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
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.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class QQueryFilterFormatter
{
private static final QLogger LOG = QLogger.getLogger(QQueryFilterFormatter.class);
/***************************************************************************
**
***************************************************************************/
public static String formatQueryFilter(String tableName, QQueryFilter filter)
{
List<String> parts = new ArrayList<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
{
parts.add(formatCriteria(table, criteria));
}
if(parts.isEmpty())
{
return ("Empty filter");
}
return String.join(" " + filter.getBooleanOperator() + " ", parts);
}
/***************************************************************************
**
***************************************************************************/
private static String formatCriteria(QTableMetaData table, QFilterCriteria criteria)
{
String fieldLabel = criteria.getFieldName();
QFieldMetaData field = null;
QFieldType fieldType = null;
try
{
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, criteria.getFieldName());
fieldLabel = fieldAndJoinTable.getLabel(table);
field = fieldAndJoinTable.field();
fieldType = field.getType();
}
catch(Exception e)
{
LOG.debug("Error getting field label for formatting criteria", e, logPair("fieldName", criteria.getFieldName()));
}
boolean isTemporal = fieldType != null && fieldType.isTemporal();
StringBuilder rs = new StringBuilder(fieldLabel);
switch(criteria.getOperator())
{
case EQUALS -> rs.append(" equals ");
case NOT_EQUALS, NOT_EQUALS_OR_IS_NULL -> rs.append(" does not equal ");
case IN -> rs.append(" is any of ");
case NOT_IN -> rs.append(" is none of ");
case IS_NULL_OR_IN -> rs.append(" is blank or any of ");
case LIKE -> rs.append(" is like ");
case NOT_LIKE -> rs.append(" is not like ");
case STARTS_WITH -> rs.append(" starts with ");
case ENDS_WITH -> rs.append(" ends with ");
case CONTAINS -> rs.append(" contains ");
case NOT_STARTS_WITH -> rs.append(" does not start with ");
case NOT_ENDS_WITH -> rs.append(" does not end with ");
case NOT_CONTAINS -> rs.append(" does not contain ");
case LESS_THAN -> rs.append(isTemporal ? " is before " : " is less than ");
case LESS_THAN_OR_EQUALS -> rs.append(isTemporal ? " is before or at " : " is less than or equal to ");
case GREATER_THAN -> rs.append(isTemporal ? " is after " : " is greater than ");
case GREATER_THAN_OR_EQUALS -> rs.append(isTemporal ? " is after or at " : " is greater than or equal to ");
case IS_BLANK -> rs.append(" is empty ");
case IS_NOT_BLANK -> rs.append(" is not empty ");
case BETWEEN -> rs.append(" is between ");
case NOT_BETWEEN -> rs.append(" is not ");
case TRUE -> rs.append(" is True ");
case FALSE -> rs.append(" is False ");
default -> rs.append(" ").append(StringUtils.allCapsToMixedCase(String.valueOf(criteria.getOperator())).replaceAll("_", " ")).append(" ");
}
List<Serializable> values = criteria.getValues();
if(values.size() == 1)
{
rs.append(formatValue(field, values.get(0)));
}
else if(values.size() == 2)
{
rs.append(formatValue(field, values.get(0))).append(" and ").append(formatValue(field, values.get(1)));
}
else if(values.size() > 1)
{
rs.append(formatValue(field, values.get(0))).append(" and ").append(values.size() - 1).append(" other values");
}
return (rs.toString());
}
/***************************************************************************
**
***************************************************************************/
private static String formatValue(QFieldMetaData field, Serializable value)
{
try
{
if(field != null && StringUtils.hasContent(field.getPossibleValueSourceName()))
{
SearchPossibleValueSourceOutput searchPossibleValueSourceOutput = new SearchPossibleValueSourceAction().execute(new SearchPossibleValueSourceInput()
.withIdList(List.of(value))
.withPossibleValueSourceName(field.getPossibleValueSourceName()));
if(CollectionUtils.nullSafeHasContents(searchPossibleValueSourceOutput.getResults()))
{
return searchPossibleValueSourceOutput.getResults().get(0).getLabel();
}
}
}
catch(Exception e)
{
LOG.debug("Error getting formatting value for criteria", e, logPair("field", () -> field.getName()), logPair("value", value));
}
return ValueUtils.getValueAsString(value);
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.utils;
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.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for QQueryFilterFormatter
*******************************************************************************/
class QQueryFilterFormatterTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
TestUtils.insertDefaultShapes(QContext.getQInstance());
QQueryFilter filter = new QQueryFilter()
.withCriteria("firstName", QCriteriaOperator.EQUALS, "Darin")
.withCriteria("lastName", QCriteriaOperator.IN, List.of("Kelkhoff", "Smellkhoff", "Dumbkhoff"))
.withCriteria("favoriteShapeId", QCriteriaOperator.NOT_EQUALS, List.of(1));
assertEquals("First Name equals Darin AND Last Name is any of Kelkhoff and 2 other values AND Favorite Shape does not equal Triangle", QQueryFilterFormatter.formatQueryFilter(TestUtils.TABLE_NAME_PERSON, filter));
}
}

View File

@ -29,6 +29,7 @@ import java.util.Objects;
import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.ApiImplementation;
import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction; import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction;
import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.ApiOperation;
import com.kingsrook.qqq.api.utils.ApiQueryFilterUtils;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
@ -81,12 +82,12 @@ public class ApiAwareTableCountExecutor extends TableCountExecutor implements Ap
// take care of managing criteria, which may not be in this version, etc // // take care of managing criteria, which may not be in this version, etc //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
QQueryFilter filter = Objects.requireNonNullElseGet(input.getFilter(), () -> new QQueryFilter()); QQueryFilter filter = Objects.requireNonNullElseGet(input.getFilter(), () -> new QQueryFilter());
QueryExecutorUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, countInput); ApiQueryFilterUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, countInput);
////////////////////////////////////////// //////////////////////////////////////////
// no more badRequest checks below here // // no more badRequest checks below here //
////////////////////////////////////////// //////////////////////////////////////////
QueryExecutorUtils.throwIfBadRequestMessages(badRequestMessages); ApiQueryFilterUtils.throwIfBadRequestMessages(badRequestMessages);
// //
CountAction countAction = new CountAction(); CountAction countAction = new CountAction();

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper;
import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.ApiOperation;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.api.utils.ApiQueryFilterUtils;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
@ -105,12 +106,12 @@ public class ApiAwareTableQueryExecutor extends TableQueryExecutor implements Ap
// take care of managing order-by fields and criteria, which may not be in this version, etc // // take care of managing order-by fields and criteria, which may not be in this version, etc //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
manageOrderByFields(filter, tableApiFields, badRequestMessages, apiName, queryInput); manageOrderByFields(filter, tableApiFields, badRequestMessages, apiName, queryInput);
QueryExecutorUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, queryInput); ApiQueryFilterUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, queryInput);
////////////////////////////////////////// //////////////////////////////////////////
// no more badRequest checks below here // // no more badRequest checks below here //
////////////////////////////////////////// //////////////////////////////////////////
QueryExecutorUtils.throwIfBadRequestMessages(badRequestMessages); ApiQueryFilterUtils.throwIfBadRequestMessages(badRequestMessages);
/////////////////////// ///////////////////////
// execute the query // // execute the query //

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.kingsrook.qqq.api.middleware.executors; package com.kingsrook.qqq.api.utils;
import java.util.List; import java.util.List;
@ -39,15 +39,15 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
/******************************************************************************* /*******************************************************************************
** shared code for query & count executors ** Utilities for working with Query Filters in the API (e.g., versioned field fun)
*******************************************************************************/ *******************************************************************************/
public class QueryExecutorUtils public class ApiQueryFilterUtils
{ {
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
static void manageCriteriaFields(QQueryFilter filter, Map<String, QFieldMetaData> tableApiFields, List<String> badRequestMessages, String apiName, QueryOrCountInputInterface input) public static void manageCriteriaFields(QQueryFilter filter, Map<String, QFieldMetaData> tableApiFields, List<String> badRequestMessages, String apiName, QueryOrCountInputInterface input)
{ {
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria())) for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
{ {
@ -85,7 +85,7 @@ public class QueryExecutorUtils
/*************************************************************************** /***************************************************************************
* *
***************************************************************************/ ***************************************************************************/
static void throwIfBadRequestMessages(List<String> badRequestMessages) throws QBadRequestException public static void throwIfBadRequestMessages(List<String> badRequestMessages) throws QBadRequestException
{ {
if(!badRequestMessages.isEmpty()) if(!badRequestMessages.isEmpty())
{ {