Merged dev into feature/meta-data-loaders

This commit is contained in:
2025-03-08 20:05:25 -06:00
346 changed files with 33987 additions and 5327 deletions

View File

@ -100,7 +100,12 @@
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel</artifactId>
<version>0.12.15</version>
<version>0.18.4</version>
</dependency>
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel-reader</artifactId>
<version>0.18.4</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
@ -112,6 +117,14 @@
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- adding to help FastExcel -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>

View File

@ -27,6 +27,7 @@ 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;
@ -160,4 +161,18 @@ 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

@ -22,15 +22,19 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -47,7 +51,6 @@ public interface TableCustomizerInterface
{
QLogger LOG = QLogger.getLogger(TableCustomizerInterface.class);
/*******************************************************************************
** custom actions to run after a query (or get!) takes place.
**
@ -77,8 +80,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
LOG.info("A default implementation of preInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
try
{
return (preInsertOrUpdate(insertInput, records, isPreview, Optional.empty()));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of preInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
}
}
@ -104,8 +114,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
LOG.info("A default implementation of postInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
try
{
return (postInsertOrUpdate(insertInput, records, Optional.empty()));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of postInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
}
}
@ -130,8 +147,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
LOG.info("A default implementation of preUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
try
{
return (preInsertOrUpdate(updateInput, records, isPreview, oldRecordList));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of preUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
}
}
@ -151,8 +175,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
LOG.info("A default implementation of postUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
try
{
return (postInsertOrUpdate(updateInput, records, oldRecordList));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of postUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
}
}
@ -199,4 +230,59 @@ public interface TableCustomizerInterface
return (records);
}
/***************************************************************************
** Optional method to override in a customizer that does the same thing for
** both preInsert & preUpdate.
***************************************************************************/
default List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
throw NotImplementedHereException.instance;
}
/***************************************************************************
** Optional method to override in a customizer that does the same thing for
** both postInsert & postUpdate.
***************************************************************************/
default List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
throw NotImplementedHereException.instance;
}
/***************************************************************************
**
***************************************************************************/
default Optional<Map<Serializable, QRecord>> oldRecordListToMap(String primaryKeyField, Optional<List<QRecord>> oldRecordList)
{
if(oldRecordList.isPresent())
{
return (Optional.of(CollectionUtils.listToMap(oldRecordList.get(), r -> r.getValue(primaryKeyField))));
}
else
{
return (Optional.empty());
}
}
/***************************************************************************
**
***************************************************************************/
class NotImplementedHereException extends QException
{
private static NotImplementedHereException instance = new NotImplementedHereException();
/***************************************************************************
**
***************************************************************************/
private NotImplementedHereException()
{
super("Not implemented here");
}
}
}

View File

@ -37,6 +37,8 @@ import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
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.get.GetInput;
@ -51,12 +53,15 @@ 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.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.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
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;
@ -83,7 +88,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
.withIsCard(true)
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withDefaultValue("joinName", join.getName())));
.withDefaultValue("joinName", join.getName())
.withValidatorPlugin(new ChildRecordListWidgetValidator())
));
}
@ -168,6 +175,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
widgetMetaData.withDefaultValue("manageAssociationName", manageAssociationName);
return (this);
}
}
@ -194,7 +202,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().get("maxRows"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -299,6 +307,13 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
}
if(widgetValues.containsKey("defaultValuesForNewChildRecordsFromParentFields"))
{
@SuppressWarnings("unchecked")
Map<String, String> defaultValuesForNewChildRecordsFromParentFields = (Map<String, String>) widgetValues.get("defaultValuesForNewChildRecordsFromParentFields");
widgetData.setDefaultValuesForNewChildRecordsFromParentFields(defaultValuesForNewChildRecordsFromParentFields);
}
}
widgetData.setAllowRecordEdit(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordEdit"))));
@ -313,4 +328,68 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
/***************************************************************************
**
***************************************************************************/
private static class ChildRecordListWidgetValidator implements QInstanceValidatorPluginInterface<QWidgetMetaDataInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QWidgetMetaDataInterface widgetMetaData, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
String prefix = "Widget " + widgetMetaData.getName() + ": ";
//////////////////////////////////
// make sure join name is given //
//////////////////////////////////
String joinName = ValueUtils.getValueAsString(CollectionUtils.nonNullMap(widgetMetaData.getDefaultValues()).get("joinName"));
if(qInstanceValidator.assertCondition(StringUtils.hasContent(joinName), prefix + "defaultValue for joinName must be given"))
{
///////////////////////////
// make sure join exists //
///////////////////////////
QJoinMetaData join = qInstance.getJoin(joinName);
if(qInstanceValidator.assertCondition(join != null, prefix + "No join named " + joinName + " exists in the instance"))
{
//////////////////////////////////////////////////////////////////////////////////
// if there's a manageAssociationName, make sure the table has that association //
//////////////////////////////////////////////////////////////////////////////////
String manageAssociationName = ValueUtils.getValueAsString(widgetMetaData.getDefaultValues().get("manageAssociationName"));
if(StringUtils.hasContent(manageAssociationName))
{
validateAssociationName(prefix, manageAssociationName, join, qInstance, qInstanceValidator);
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private void validateAssociationName(String prefix, String manageAssociationName, QJoinMetaData join, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
///////////////////////////////////
// make sure join's table exists //
///////////////////////////////////
QTableMetaData table = qInstance.getTable(join.getLeftTable());
if(table == null)
{
qInstanceValidator.getErrors().add(prefix + "Unable to validate manageAssociationName, as table [" + join.getLeftTable() + "] on left-side table of join [" + join.getName() + "] does not exist.");
}
else
{
if(CollectionUtils.nonNullList(table.getAssociations()).stream().noneMatch(a -> manageAssociationName.equals(a.getName())))
{
qInstanceValidator.getErrors().add(prefix + "an association named [" + manageAssociationName + "] does not exist on table [" + join.getLeftTable() + "]");
}
}
}
}
}

View File

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

@ -36,6 +36,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@ -46,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -188,21 +191,40 @@ public class RunBackendStepAction
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
QTableMetaData table = QContext.getQInstance().getTable(inputMetaData.getRecordListMetaData().getTableName());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
// todo - handle this being async (e.g., http)
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
// then this step can re-run, hopefully with the needed data.
QProcessCallback callback = runBackendStepInput.getCallback();
if(callback == null)
//////////////////////////////////////////////////
// look for record ids in the input data values //
//////////////////////////////////////////////////
String recordIds = (String) runBackendStepInput.getValue("recordIds");
if(recordIds == null)
{
throw (new QUserFacingException("Missing input records.",
new QException("Function is missing input records, but no callback was present to request fields from a user")));
recordIds = (String) runBackendStepInput.getValue("recordId");
}
queryInput.setFilter(callback.getQueryFilter());
///////////////////////////////////////////////////////////
// if records were found, add as criteria to query input //
///////////////////////////////////////////////////////////
if(recordIds != null)
{
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordIds.split(","))));
}
else
{
// todo - handle this being async (e.g., http)
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
// then this step can re-run, hopefully with the needed data.
QProcessCallback callback = runBackendStepInput.getCallback();
if(callback == null)
{
throw (new QUserFacingException("Missing input records.",
new QException("Function is missing input records, but no callback was present to request fields from a user")));
}
queryInput.setFilter(callback.getQueryFilter());
}
//////////////////////////////////////////////////////////////////////////////////////////
// if process has a max-no of records, set a limit on the process of that number plus 1 //
@ -210,7 +232,7 @@ public class RunBackendStepAction
//////////////////////////////////////////////////////////////////////////////////////////
if(process.getMaxInputRecords() != null)
{
if(callback.getQueryFilter() == null)
if(queryInput.getFilter() == null)
{
queryInput.setFilter(new QQueryFilter());
}

View File

@ -32,6 +32,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -53,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
@ -63,6 +65,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
@ -71,6 +74,7 @@ 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.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -87,12 +91,16 @@ public class RunProcessAction
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
public static final String PROCESS_TRACER_CODE_REFERENCE_FIELD = "processTracerCodeReference";
////////////////////////////////////////////////////////////////////////////////////////////////
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
////////////////////////////////////////////////////////////////////////////////////////////////
public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp";
public static final String BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD = "basepullDidQueryUsingTimestamp";
private ProcessTracerInterface processTracer;
/*******************************************************************************
@ -119,9 +127,17 @@ public class RunProcessAction
}
runProcessOutput.setProcessUUID(runProcessInput.getProcessUUID());
traceStartOrResume(runProcessInput, process);
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
ProcessState processState = primeProcessState(runProcessInput, stateKey, process);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// these should always be clear when we're starting a run - so make sure they haven't leaked from previous //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
processState.clearNextStepName();
processState.clearBackStepName();
/////////////////////////////////////////////////////////
// if process is 'basepull' style, keep track of 'now' //
/////////////////////////////////////////////////////////
@ -160,6 +176,7 @@ public class RunProcessAction
////////////////////////////////////////////////////////////
// upon exception (e.g., one thrown by a step), throw it. //
////////////////////////////////////////////////////////////
traceBreakOrFinish(runProcessInput, runProcessOutput, qe);
throw (qe);
}
catch(Exception e)
@ -167,6 +184,7 @@ public class RunProcessAction
////////////////////////////////////////////////////////////
// upon exception (e.g., one thrown by a step), throw it. //
////////////////////////////////////////////////////////////
traceBreakOrFinish(runProcessInput, runProcessOutput, e);
throw (new QException("Error running process", e));
}
finally
@ -177,6 +195,8 @@ public class RunProcessAction
runProcessOutput.setProcessState(processState);
}
traceBreakOrFinish(runProcessInput, runProcessOutput, null);
return (runProcessOutput);
}
@ -188,14 +208,35 @@ public class RunProcessAction
private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception
{
String lastStepName = runProcessInput.getStartAfterStep();
String startAtStep = runProcessInput.getStartAtStep();
while(true)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// always refresh the step list - as any step that runs can modify it (in the process state). //
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
// deal with if we were told, from the input, to start After a step, or start At a step. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
List<QStepMetaData> stepList;
if(startAtStep == null)
{
stepList = getAvailableStepList(processState, process, lastStepName, false);
}
else
{
stepList = getAvailableStepList(processState, process, startAtStep, true);
///////////////////////////////////////////////////////////////////////////////////
// clear this field - so after we run a step, we'll then loop in last-step mode. //
///////////////////////////////////////////////////////////////////////////////////
startAtStep = null;
///////////////////////////////////////////////////////////////////////////////////
// if we're going to run a backend step now, let it see that this is a step-back //
///////////////////////////////////////////////////////////////////////////////////
processState.setIsStepBack(true);
}
if(stepList.isEmpty())
{
break;
@ -232,7 +273,18 @@ public class RunProcessAction
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
////////////////////////////////////////////////////////////////////////////////////////
// only let this value be set for the original back step - don't let it stick around. //
// if a process wants to keep track of this itself, it can, but in a different slot. //
////////////////////////////////////////////////////////////////////////////////////////
processState.setIsStepBack(false);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case we broke from the loop above (e.g., by going directly into a frontend step), once again make sure to lower this flag. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
processState.setIsStepBack(false);
}
@ -264,6 +316,12 @@ public class RunProcessAction
processFrontendStepFieldDefaultValues(processState, step);
processFrontendComponents(processState, step);
processState.setNextStepName(step.getName());
if(StringUtils.hasContent(step.getBackStepName()) && processState.getBackStepName().isEmpty())
{
processState.setBackStepName(step.getBackStepName());
}
return LoopTodo.BREAK;
}
case SKIP ->
@ -317,6 +375,7 @@ public class RunProcessAction
// else run the given lastStepName //
/////////////////////////////////////
processState.clearNextStepName();
processState.clearBackStepName();
step = process.getStep(lastStepName);
if(step == null)
{
@ -398,6 +457,7 @@ public class RunProcessAction
// its sub-steps, or, to fall out of the loop and end the process. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
processState.clearNextStepName();
processState.clearBackStepName();
runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
return;
}
@ -584,6 +644,7 @@ public class RunProcessAction
runBackendStepInput.setCallback(runProcessInput.getCallback());
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
runBackendStepInput.setProcessTracer(processTracer);
runBackendStepInput.setTableName(process.getTableName());
if(!StringUtils.hasContent(runBackendStepInput.getTableName()))
@ -605,9 +666,13 @@ public class RunProcessAction
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
}
traceStepStart(runBackendStepInput);
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, runBackendStepOutput.getProcessState());
traceStepFinish(runBackendStepInput, runBackendStepOutput);
if(runBackendStepOutput.getException() != null)
{
runProcessOutput.setException(runBackendStepOutput.getException());
@ -621,8 +686,10 @@ public class RunProcessAction
/*******************************************************************************
** Get the list of steps which are eligible to run.
**
** lastStep will be included in the list, or not, based on includeLastStep.
*******************************************************************************/
private List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException
static List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep, boolean includeLastStep) throws QException
{
if(lastStep == null)
{
@ -649,6 +716,10 @@ public class RunProcessAction
if(stepName.equals(lastStep))
{
foundLastStep = true;
if(includeLastStep)
{
validStepNames.add(stepName);
}
}
}
return (stepNamesToSteps(process, validStepNames));
@ -660,7 +731,7 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
private List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
private static List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
{
List<QStepMetaData> result = new ArrayList<>();
@ -744,13 +815,14 @@ public class RunProcessAction
{
QSession session = QContext.getQSession();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
String variantTypeKey = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(variantTypeKey))
{
LOG.warn("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
}
else
{
basepullKeyValue += "-" + session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue());
basepullKeyValue += "-" + session.getBackendVariants().get(variantTypeKey);
}
}
@ -879,4 +951,153 @@ public class RunProcessAction
runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField());
runProcessInput.getValues().put(BASEPULL_CONFIGURATION, basepullConfiguration);
}
/***************************************************************************
**
***************************************************************************/
private void setupProcessTracer(RunProcessInput runProcessInput, QProcessMetaData process)
{
try
{
if(process.getProcessTracerCodeReference() != null)
{
processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, process.getProcessTracerCodeReference());
}
Serializable processTracerCodeReference = runProcessInput.getValue(PROCESS_TRACER_CODE_REFERENCE_FIELD);
if(processTracerCodeReference != null)
{
if(processTracerCodeReference instanceof QCodeReference codeReference)
{
processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, codeReference);
}
}
}
catch(Exception e)
{
LOG.warn("Error setting up processTracer", e, logPair("processName", runProcessInput.getProcessName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceStartOrResume(RunProcessInput runProcessInput, QProcessMetaData process)
{
setupProcessTracer(runProcessInput, process);
try
{
if(processTracer != null)
{
if(StringUtils.hasContent(runProcessInput.getStartAfterStep()) || StringUtils.hasContent(runProcessInput.getStartAtStep()))
{
processTracer.handleProcessResume(runProcessInput);
}
else
{
processTracer.handleProcessStart(runProcessInput);
}
}
}
catch(Exception e)
{
LOG.info("Error in traceStart", e, logPair("processName", runProcessInput.getProcessName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceBreakOrFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException)
{
try
{
if(processTracer != null)
{
ProcessState processState = runProcessOutput.getProcessState();
boolean isBreak = true;
/////////////////////////////////////////////////////////////
// if there's no next step, that means the process is done //
/////////////////////////////////////////////////////////////
if(processState.getNextStepName().isEmpty())
{
isBreak = false;
}
else
{
/////////////////////////////////////////////////////////////////
// or if the next step is the last index, then we're also done //
/////////////////////////////////////////////////////////////////
String nextStepName = processState.getNextStepName().get();
int nextStepIndex = processState.getStepList().indexOf(nextStepName);
if(nextStepIndex == processState.getStepList().size() - 1)
{
isBreak = false;
}
}
if(isBreak)
{
processTracer.handleProcessBreak(runProcessInput, runProcessOutput, processException);
}
else
{
processTracer.handleProcessFinish(runProcessInput, runProcessOutput, processException);
}
}
}
catch(Exception e)
{
LOG.info("Error in traceProcessFinish", e, logPair("processName", runProcessInput.getProcessName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceStepStart(RunBackendStepInput runBackendStepInput)
{
try
{
if(processTracer != null)
{
processTracer.handleStepStart(runBackendStepInput);
}
}
catch(Exception e)
{
LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput)
{
try
{
if(processTracer != null)
{
processTracer.handleStepFinish(runBackendStepInput, runBackendStepOutput);
}
}
catch(Exception e)
{
LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName()));
}
}
}

View File

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

View File

@ -71,6 +71,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.reporting.QReportDataSource;
@ -567,7 +568,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
// all pivotFields that are possible value sources are implicitly translated //
///////////////////////////////////////////////////////////////////////////////
QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(mainTable, summaryFieldName);
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{
fieldsToTranslatePossibleValues.add(summaryFieldName);
@ -580,32 +581,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
String joinTableName = fieldName.replaceAll("\\..*", "");
String joinFieldName = fieldName.replaceAll(".*\\.", "");
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
if(joinTable == null)
{
throw (new QException("Unrecognized join table name: " + joinTableName));
}
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
}
else
{
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -756,7 +731,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
SummaryKey key = new SummaryKey();
for(String summaryFieldName : view.getSummaryFields())
{
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
Serializable summaryValue = record.getValue(summaryFieldName);
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{
@ -811,7 +786,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////
// todo - memoize this, if we ever need to optimize //
//////////////////////////////////////////////////////
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
field = fieldAndJoinTable.field();
}
catch(Exception e)
@ -956,7 +931,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
List<QFieldMetaData> fields = new ArrayList<>();
for(String summaryFieldName : view.getSummaryFields())
{
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
fields.add(new QFieldMetaData(summaryFieldName, fieldAndJoinTable.field().getType()).withLabel(fieldAndJoinTable.field().getLabel())); // todo do we need the type? if so need table as input here
}
for(QReportField column : view.getColumns())
@ -1208,27 +1183,4 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
{
}
/*******************************************************************************
**
*******************************************************************************/
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}
}

View File

@ -32,6 +32,7 @@ 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;
@ -82,6 +83,22 @@ 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

@ -82,6 +82,11 @@ public class DeleteAction
{
ActionHelper.validateSession(deleteInput);
if(deleteInput.getTableName() == null)
{
throw (new QException("Table name was not specified in delete input"));
}
QTableMetaData table = deleteInput.getTable();
String primaryKeyFieldName = table.getPrimaryKeyField();
QFieldMetaData primaryKeyField = table.getField(primaryKeyFieldName);
@ -320,7 +325,7 @@ public class DeleteAction
QTableMetaData table = deleteInput.getTable();
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE, deleteInput.getTransaction());
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-delete customizer, if there is one //

View File

@ -238,6 +238,11 @@ 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

@ -67,6 +67,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
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;
@ -110,6 +111,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
public InsertOutput execute(InsertInput insertInput) throws QException
{
ActionHelper.validateSession(insertInput);
if(!StringUtils.hasContent(insertInput.getTableName()))
{
throw (new QException("Table name was not specified in insert input"));
}
QTableMetaData table = insertInput.getTable();
if(table == null)
@ -122,7 +129,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/////////////////////////////
// run standard validators //
/////////////////////////////
performValidations(insertInput, false);
performValidations(insertInput, false, false);
//////////////////////////////////////////////////////
// use the backend module to actually do the insert //
@ -225,7 +232,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
public void performValidations(InsertInput insertInput, boolean isPreview) throws QException
public void performValidations(InsertInput insertInput, boolean isPreview, boolean didAlreadyRunCustomizer) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
{
@ -237,12 +244,10 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
///////////////////////////////////////////////////////////////////
// load the pre-insert customizer and set it up, if there is one //
// then we'll run it based on its WhenToRun value //
// note - if we already ran it, then don't re-run it! //
///////////////////////////////////////////////////////////////////
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
if(preInsertCustomizer.isPresent())
{
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
}
Optional<TableCustomizerInterface> preInsertCustomizer = didAlreadyRunCustomizer ? Optional.empty() : QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
setDefaultValuesInRecords(table, insertInput.getRecords());
@ -258,7 +263,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT, insertInput.getTransaction());
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
}

View File

@ -47,7 +47,8 @@ public class StorageAction
{
/*******************************************************************************
**
** create an output stream in the storage backend - that can be written to,
** for the purpose of inserting or writing a file into storage.
*******************************************************************************/
public OutputStream createOutputStream(StorageInput storageInput) throws QException
{
@ -59,7 +60,8 @@ public class StorageAction
/*******************************************************************************
**
** create an input stream in the storage backend - that can be read from,
** for the purpose of getting or reading a file from storage.
*******************************************************************************/
public InputStream getInputStream(StorageInput storageInput) throws QException
{

View File

@ -74,6 +74,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
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.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -118,6 +119,11 @@ public class UpdateAction
{
ActionHelper.validateSession(updateInput);
if(!StringUtils.hasContent(updateInput.getTableName()))
{
throw (new QException("Table name was not specified in update input"));
}
QTableMetaData table = updateInput.getTable();
//////////////////////////////////////////////////////
@ -261,7 +267,7 @@ public class UpdateAction
}
else
{
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE, updateInput.getTransaction());
}
if(updateInput.getInputSource().shouldValidateRequiredFields())
@ -374,7 +380,7 @@ public class UpdateAction
}
}
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE, updateInput.getTransaction());
for(QRecord record : page)
{

View File

@ -31,16 +31,12 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
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.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -54,12 +50,10 @@ import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -371,7 +365,7 @@ public class QueryStatManager
//////////////////////
// set the table id //
//////////////////////
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
Integer qqqTableId = QQQTableTableManager.getQQQTableId(getInstance().qInstance, queryStat.getTableName());
queryStat.setQqqTableId(qqqTableId);
//////////////////////////////
@ -382,7 +376,7 @@ public class QueryStatManager
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
for(String joinTableName : queryStat.getJoinTableNames())
{
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, joinTableName)));
}
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
}
@ -460,7 +454,7 @@ public class QueryStatManager
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
queryStatCriteriaField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0]));
queryStatCriteriaField.setName(parts[1]);
}
}
@ -498,7 +492,7 @@ public class QueryStatManager
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
queryStatOrderByField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0]));
queryStatOrderByField.setName(parts[1]);
}
}
@ -512,44 +506,6 @@ public class QueryStatManager
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer getQQQTableId(String tableName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", tableName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord().getValueInteger("id");
}
}

View File

@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -83,7 +84,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action, QBackendTransaction transaction) throws QException
{
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
@ -101,7 +102,7 @@ public class ValidateRecordSecurityLockHelper
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction);
/////////////////////////////////
// propagate errors to records //
@ -141,7 +142,7 @@ public class ValidateRecordSecurityLockHelper
** BUT - WRITE locks - in their case, we read the record no matter what, and in
** here we need to verify we have a key that allows us to WRITE the record.
*******************************************************************************/
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys) throws QException
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
@ -152,7 +153,7 @@ public class ValidateRecordSecurityLockHelper
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction);
treePosition.remove(treePosition.size() - 1);
i++;
}
@ -225,6 +226,7 @@ public class ValidateRecordSecurityLockHelper
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(transaction);
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);

View File

@ -104,10 +104,21 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
/*******************************************************************************
** Static wrapper to render a Velocity template.
*******************************************************************************/
@Deprecated(since = "Call the version that doesn't take an ActionInput")
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
{
return (renderVelocity(context, code));
}
/*******************************************************************************
** Most convenient static wrapper to render a Velocity template.
*******************************************************************************/
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
public static String renderVelocity(Map<String, Object> context, String code) throws QException
{
return (render(TemplateType.VELOCITY, context, code));
}

View File

@ -24,6 +24,7 @@ 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;
@ -74,6 +75,31 @@ 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

@ -28,6 +28,7 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -44,6 +45,7 @@ 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;
@ -484,6 +486,8 @@ 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))
@ -491,6 +495,11 @@ public class QValueFormatter
continue;
}
if(BooleanUtils.isTrue(downloadUrlDynamic))
{
continue;
}
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
String fileName = null;
@ -508,7 +517,7 @@ public class QValueFormatter
{
@SuppressWarnings("unchecked") // instance validation should make this safe!
List<String> fileNameFormatFields = (List<String>) adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS);
List<String> values = fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList();
List<String> values = CollectionUtils.nullSafeHasContents(fileNameFormatFields) ? fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList() : Collections.emptyList();
fileName = QValueFormatter.formatStringWithValues(fileNameFormat, values);
}
}
@ -531,7 +540,7 @@ public class QValueFormatter
////////////////////////////////////////////////////////////////////////////////////////////////
// if field type is blob OR if there's a supplemental process or code-ref that needs to run - //
// then update its value to be a callback-url that'll give access to the bytes to download //
// then update its value to be a callback-url that'll give access to the bytes to download. //
// implied here is that a String value (w/o supplemental code/proc) has its value stay as a //
// URL, which is where the file is directly downloaded from. And in the case of a String //
// with code-to-run, then the code should run, followed by a redirect to the value URL. //
@ -540,7 +549,7 @@ public class QValueFormatter
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_CODE_REFERENCE)
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_PROCESS_NAME))
{
record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName);
record.setValue(field.getName(), AdornmentType.FileDownloadValues.makeFieldDownloadUrl(table.getName(), primaryKey, field.getName(), fileName));
}
record.setDisplayValue(field.getName(), fileName);
}

View File

@ -26,8 +26,13 @@ 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;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -47,10 +52,9 @@ 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 org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -61,6 +65,9 @@ public class SearchPossibleValueSourceAction
{
private static final QLogger LOG = QLogger.getLogger(SearchPossibleValueSourceAction.class);
private static final Set<String> warnedAboutUnexpectedValueField = Collections.synchronizedSet(new HashSet<>());
private static final Set<String> warnedAboutUnexpectedNoOfFieldsToSearchByLabel = Collections.synchronizedSet(new HashSet<>());
private QPossibleValueTranslator possibleValueTranslator;
@ -101,47 +108,54 @@ 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());
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
boolean match = false;
if(input.getIdList() != null)
{
if(inputIdsAsCorrectType.contains(possibleValue.getId()))
{
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;
}
}
boolean match = doesPossibleValueMatchSearchInput(possibleValue, preparedSearchPossibleValueSourceInput);
if(match)
{
matchingIds.add((Serializable) possibleValue.getId());
matchingIds.add(possibleValue.getId());
}
// todo - skip & limit?
// todo - default filter
}
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds);
@ -152,37 +166,95 @@ 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 looks at the first id in the enum, and then
** maps all the inputIds to be of the same type.
** in an enum). So, this method type-converts them.
*******************************************************************************/
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
private static List<Object> convertInputIdsToPossibleValueSourceIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
{
List<Object> rs = new ArrayList<>();
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
if(inputIdList == null)
{
return (null);
}
else if(inputIdList.isEmpty())
{
return (rs);
}
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
QFieldType type = possibleValueSource.getIdType();
if(anIdFromTheEnum instanceof Integer)
for(Serializable inputId : inputIdList)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsInteger(id)));
}
else if(anIdFromTheEnum instanceof String)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsString(id)));
}
else if(anIdFromTheEnum instanceof Boolean)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsBoolean(id)));
}
else
{
LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName());
Object properlyTypedId = null;
try
{
if(type.equals(QFieldType.INTEGER))
{
properlyTypedId = ValueUtils.getValueAsInteger(inputId);
}
else if(type.isStringLike())
{
properlyTypedId = ValueUtils.getValueAsString(inputId);
}
else if(type.equals(QFieldType.BOOLEAN))
{
properlyTypedId = ValueUtils.getValueAsBoolean(inputId);
}
else
{
LOG.warn("Unexpected type [" + type + "] for ids in enum: " + possibleValueSource.getName());
}
}
catch(Exception e)
{
LOG.debug("Error converting possible value id to expected id type", e, logPair("value", inputId));
}
if(properlyTypedId != null)
{
rs.add(properlyTypedId);
}
}
return (rs);
@ -209,6 +281,53 @@ public class SearchPossibleValueSourceAction
{
queryFilter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList()));
}
else if(input.getLabelList() != null)
{
List<String> fieldNames = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// the 'value fields' will either be 'id' or 'label' (which means, use the fields from the tableMetaData's label fields) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(String valueField : possibleValueSource.getValueFields())
{
if("id".equals(valueField))
{
fieldNames.add(table.getPrimaryKeyField());
}
else if("label".equals(valueField))
{
if(table.getRecordLabelFields() != null)
{
fieldNames.addAll(table.getRecordLabelFields());
}
}
else
{
String message = "Unexpected valueField defined in possibleValueSource when searching possibleValueSource by label (required: 'id' or 'label')";
if(!warnedAboutUnexpectedValueField.contains(possibleValueSource.getName()))
{
LOG.warn(message, logPair("valueField", valueField), logPair("possibleValueSource", possibleValueSource.getName()));
warnedAboutUnexpectedValueField.add(possibleValueSource.getName());
}
output.setWarning(message);
}
}
if(fieldNames.size() == 1)
{
queryFilter.addCriteria(new QFilterCriteria(fieldNames.get(0), QCriteriaOperator.IN, input.getLabelList()));
}
else
{
String message = "Unexpected number of fields found for searching possibleValueSource by label (required: 1, found: " + fieldNames.size() + ")";
if(!warnedAboutUnexpectedNoOfFieldsToSearchByLabel.contains(possibleValueSource.getName()))
{
LOG.warn(message);
warnedAboutUnexpectedNoOfFieldsToSearchByLabel.add(possibleValueSource.getName());
}
output.setWarning(message);
}
}
else
{
String searchTerm = input.getSearchTerm();
@ -269,8 +388,8 @@ public class SearchPossibleValueSourceAction
queryFilter = input.getDefaultQueryFilter();
}
// todo - skip & limit as params
queryFilter.setLimit(250);
queryFilter.setLimit(input.getLimit());
queryFilter.setSkip(input.getSkip());
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
@ -288,7 +407,7 @@ public class SearchPossibleValueSourceAction
fieldName = table.getPrimaryKeyField();
}
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
output.setResults(qPossibleValues);
@ -301,7 +420,7 @@ public class SearchPossibleValueSourceAction
**
*******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" })
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) throws QException
{
try
{
@ -314,11 +433,10 @@ public class SearchPossibleValueSourceAction
}
catch(Exception e)
{
// LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
String message = "Error searching custom possible value source [" + input.getPossibleValueSourceName() + "]";
LOG.warn(message, e);
throw (new QException(message, e));
}
throw new NotImplementedException("Not impleemnted");
// return (null);
}
}

View File

@ -23,7 +23,11 @@ package com.kingsrook.qqq.backend.core.instances;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -31,21 +35,27 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType.FileUploadAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
@ -54,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPer
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
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.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
@ -75,13 +86,20 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEd
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertLoadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareFileMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareFileUploadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareValueMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveValueMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
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;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -107,6 +125,8 @@ public class QInstanceEnricher
//////////////////////////////////////////////////////////////////////////////////////////////////
private static final Map<String, String> labelMappings = new LinkedHashMap<>();
private static ListingHash<Class<?>, QInstanceEnricherPluginInterface<?>> enricherPlugins = new ListingHash<>();
/*******************************************************************************
@ -168,6 +188,7 @@ public class QInstanceEnricher
}
enrichJoins();
enrichInstance();
//////////////////////////////////////////////////////////////////////////////
// if the instance DOES have 1 or more scheduler, but no schedulable types, //
@ -184,6 +205,16 @@ public class QInstanceEnricher
/***************************************************************************
**
***************************************************************************/
private void enrichInstance()
{
runPlugins(QInstance.class, qInstance, qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
@ -248,6 +279,14 @@ public class QInstanceEnricher
}
}
}
///////////////////////////////////////////
// run plugins on joins if there are any //
///////////////////////////////////////////
for(QJoinMetaData join : qInstance.getJoins().values())
{
runPlugins(QJoinMetaData.class, join, qInstance);
}
}
catch(Exception e)
{
@ -263,6 +302,7 @@ public class QInstanceEnricher
private void enrichWidget(QWidgetMetaDataInterface widgetMetaData)
{
enrichPermissionRules(widgetMetaData);
runPlugins(QWidgetMetaDataInterface.class, widgetMetaData, qInstance);
}
@ -273,6 +313,7 @@ public class QInstanceEnricher
private void enrichBackend(QBackendMetaData qBackendMetaData)
{
qBackendMetaData.enrich();
runPlugins(QBackendMetaData.class, qBackendMetaData, qInstance);
}
@ -327,6 +368,7 @@ public class QInstanceEnricher
enrichPermissionRules(table);
enrichAuditRules(table);
runPlugins(QTableMetaData.class, table, qInstance);
}
@ -417,6 +459,7 @@ public class QInstanceEnricher
}
enrichPermissionRules(process);
runPlugins(QProcessMetaData.class, process, qInstance);
}
@ -538,6 +581,8 @@ public class QInstanceEnricher
field.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
}
}
runPlugins(QFieldMetaData.class, field, qInstance);
}
@ -609,6 +654,7 @@ public class QInstanceEnricher
ensureAppSectionMembersAreAppChildren(app);
enrichPermissionRules(app);
runPlugins(QAppMetaData.class, app, qInstance);
}
@ -756,6 +802,7 @@ public class QInstanceEnricher
}
enrichPermissionRules(report);
runPlugins(QReportMetaData.class, report, qInstance);
}
@ -847,7 +894,7 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
public void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
{
Map<String, Serializable> values = new HashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
@ -859,6 +906,7 @@ public class QInstanceEnricher
values
)
.withName(processName)
.withIcon(new QIcon().withName("library_add"))
.withLabel(table.getLabel() + " Bulk Insert")
.withTableName(table.getName())
.withIsHidden(true)
@ -889,18 +937,76 @@ public class QInstanceEnricher
.map(QFieldMetaData::getLabel)
.collect(Collectors.joining(", "));
QBackendStepMetaData prepareFileUploadStep = new QBackendStepMetaData()
.withName("prepareFileUpload")
.withCode(new QCodeReference(BulkInsertPrepareFileUploadStep.class));
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
.withName("upload")
.withLabel("Upload File")
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withLabel(table.getLabel() + " File").withIsRequired(true))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("previewText", "file upload instructions")
.withValue("text", "Upload a CSV file with the following columns:\n" + fieldsForHelpText))
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB)
.withFieldAdornment(FileUploadAdornment.newFieldAdornment()
.withValue(FileUploadAdornment.formatDragAndDrop())
.withValue(FileUploadAdornment.widthFull()))
.withLabel(table.getLabel() + " File")
.withIsRequired(true))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
process.addStep(0, uploadScreen);
process.getFrontendStep("review").setRecordListFields(editableFields);
QBackendStepMetaData prepareFileMappingStep = new QBackendStepMetaData()
.withName("prepareFileMapping")
.withCode(new QCodeReference(BulkInsertPrepareFileMappingStep.class));
QFrontendStepMetaData fileMappingScreen = new QFrontendStepMetaData()
.withName("fileMapping")
.withLabel("File Mapping")
.withBackStepName("prepareFileUpload")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
.withFormField(new QFieldMetaData("hasHeaderRow", QFieldType.BOOLEAN))
.withFormField(new QFieldMetaData("layout", QFieldType.STRING)); // is actually PVS, but, this field is only added to help support helpContent, so :shrug:
QBackendStepMetaData receiveFileMappingStep = new QBackendStepMetaData()
.withName("receiveFileMapping")
.withCode(new QCodeReference(BulkInsertReceiveFileMappingStep.class));
QBackendStepMetaData prepareValueMappingStep = new QBackendStepMetaData()
.withName("prepareValueMapping")
.withCode(new QCodeReference(BulkInsertPrepareValueMappingStep.class));
QFrontendStepMetaData valueMappingScreen = new QFrontendStepMetaData()
.withName("valueMapping")
.withLabel("Value Mapping")
.withBackStepName("prepareFileMapping")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_VALUE_MAPPING_FORM));
QBackendStepMetaData receiveValueMappingStep = new QBackendStepMetaData()
.withName("receiveValueMapping")
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
int i = 0;
process.addStep(i++, prepareFileUploadStep);
process.addStep(i++, uploadScreen);
process.addStep(i++, prepareFileMappingStep);
process.addStep(i++, fileMappingScreen);
process.addStep(i++, receiveFileMappingStep);
process.addStep(i++, prepareValueMappingStep);
process.addStep(i++, valueMappingScreen);
process.addStep(i++, receiveValueMappingStep);
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
//////////////////////////////////////////////////////////////////////////////////////////
// put the bulk-load profile form (e.g., for saving it) on the review & result screens) //
//////////////////////////////////////////////////////////////////////////////////////////
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW)
.withBackStepName("prepareFileMapping")
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_RESULT)
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
qInstance.addProcess(process);
}
@ -1295,6 +1401,137 @@ public class QInstanceEnricher
}
}
if(possibleValueSource.getIdType() == null)
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
if(table != null)
{
String primaryKeyField = table.getPrimaryKeyField();
QFieldMetaData primaryKeyFieldMetaData = table.getFields().get(primaryKeyField);
if(primaryKeyFieldMetaData != null)
{
possibleValueSource.setIdType(primaryKeyFieldMetaData.getType());
}
}
}
}
else if(QPossibleValueSourceType.ENUM.equals(possibleValueSource.getType()))
{
if(possibleValueSource.getIdType() == null)
{
if(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()))
{
Object id = possibleValueSource.getEnumValues().get(0).getId();
try
{
possibleValueSource.setIdType(QFieldType.fromClass(id.getClass()));
}
catch(Exception e)
{
LOG.warn("Error enriching possible value source with idType based on first enum value", e, logPair("possibleValueSource", possibleValueSource.getName()), logPair("id", id));
}
}
}
}
else if(QPossibleValueSourceType.CUSTOM.equals(possibleValueSource.getType()))
{
if(possibleValueSource.getIdType() == null)
{
try
{
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
Method getPossibleValueMethod = customPossibleValueProvider.getClass().getDeclaredMethod("getPossibleValue", Serializable.class);
Type returnType = getPossibleValueMethod.getGenericReturnType();
Type idType = ((ParameterizedType) returnType).getActualTypeArguments()[0];
if(idType instanceof Class<?> c)
{
possibleValueSource.setIdType(QFieldType.fromClass(c));
}
}
catch(Exception e)
{
LOG.warn("Error enriching possible value source with idType based on first custom value", e, logPair("possibleValueSource", possibleValueSource.getName()));
}
}
}
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
public static void addEnricherPlugin(QInstanceEnricherPluginInterface<?> plugin)
{
Optional<Method> enrichMethod = Arrays.stream(plugin.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals("enrich")
&& m.getParameterCount() == 2
&& !m.getParameterTypes()[0].equals(Object.class)
&& m.getParameterTypes()[1].equals(QInstance.class)
).findFirst();
if(enrichMethod.isPresent())
{
Class<?> parameterType = enrichMethod.get().getParameterTypes()[0];
enricherPlugins.add(parameterType, plugin);
}
else
{
LOG.warn("Could not find enrich method on enricher plugin [" + plugin.getClass().getName() + "] (to infer type being enriched) - this plugin will not be used.");
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void removeAllEnricherPlugins()
{
enricherPlugins.clear();
}
/***************************************************************************
** 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));
}
}
/*******************************************************************************
**
*******************************************************************************/
private <T> void runPlugins(Class<T> c, T t, QInstance qInstance)
{
for(QInstanceEnricherPluginInterface<?> plugin : CollectionUtils.nonNullList(enricherPlugins.get(c)))
{
@SuppressWarnings("unchecked")
QInstanceEnricherPluginInterface<T> castedPlugin = (QInstanceEnricherPluginInterface<T>) plugin;
castedPlugin.enrich(t, qInstance);
}
}

View File

@ -30,7 +30,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@ -111,7 +110,7 @@ public class QInstanceHelpContentManager
}
else
{
LOG.info("Discarding help content with key that does not contain name:value format", logPair("key", key), logPair("id", record.getValue("id")));
LOG.info("Discarding help content with key-part that does not contain name:value format", logPair("key", key), logPair("part", part), logPair("id", record.getValue("id")));
}
}
@ -150,19 +149,19 @@ public class QInstanceHelpContentManager
///////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(tableName))
{
processHelpContentForTable(key, tableName, sectionName, fieldName, slotName, roles, helpContent);
processHelpContentForTable(qInstance, key, tableName, sectionName, fieldName, slotName, roles, helpContent);
}
else if(StringUtils.hasContent(processName))
{
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
processHelpContentForProcess(qInstance, key, processName, fieldName, stepName, roles, helpContent);
}
else if(StringUtils.hasContent(widgetName))
{
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
processHelpContentForWidget(qInstance, key, widgetName, slotName, roles, helpContent);
}
else if(nameValuePairs.containsKey("instanceLevel"))
{
processHelpContentForInstance(key, slotName, roles, helpContent);
processHelpContentForInstance(qInstance, key, slotName, roles, helpContent);
}
}
catch(Exception e)
@ -176,9 +175,9 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForTable(QInstance qInstance, String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
LOG.info("Unrecognized table in help content", logPair("key", key));
@ -246,9 +245,30 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForProcess(QInstance qInstance, String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(processName.startsWith("*") && processName.length() > 1)
{
boolean anyMatched = false;
String subName = processName.substring(1);
for(QProcessMetaData process : qInstance.getProcesses().values())
{
if(process.getName().endsWith(subName))
{
anyMatched = true;
processHelpContentForProcess(qInstance, key, process.getName(), fieldName, stepName, roles, helpContent);
}
}
if(!anyMatched)
{
LOG.info("Wildcard process name did not match any processes in help content", logPair("key", key));
}
return;
}
QProcessMetaData process = qInstance.getProcess(processName);
if(process == null)
{
LOG.info("Unrecognized process in help content", logPair("key", key));
@ -306,9 +326,9 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForWidget(String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForWidget(QInstance qInstance, String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
QWidgetMetaDataInterface widget = qInstance.getWidget(widgetName);
if(!StringUtils.hasContent(slotName))
{
LOG.info("Missing slot name in help content", logPair("key", key));
@ -335,7 +355,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForInstance(String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForInstance(QInstance qInstance, String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
if(!StringUtils.hasContent(slotName))
{
@ -345,11 +365,11 @@ public class QInstanceHelpContentManager
{
if(helpContent != null)
{
QContext.getQInstance().withHelpContent(slotName, helpContent);
qInstance.withHelpContent(slotName, helpContent);
}
else
{
QContext.getQInstance().removeHelpContent(slotName, roles);
qInstance.removeHelpContent(slotName, roles);
}
}
}

View File

@ -37,7 +37,9 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
@ -108,12 +110,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantSetting;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
import org.apache.commons.lang.BooleanUtils;
import org.quartz.CronExpression;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -136,6 +142,8 @@ public class QInstanceValidator
private static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> validatorPlugins = new ListingHash<>();
private JoinGraph joinGraph = null;
private List<String> errors = new ArrayList<>();
@ -163,8 +171,7 @@ 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. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
JoinGraph joinGraph = null;
long start = System.currentTimeMillis();
long start = System.currentTimeMillis();
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -173,7 +180,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();
joinGraph = qInstanceEnricher.getJoinGraph();
this.joinGraph = qInstanceEnricher.getJoinGraph();
}
catch(Exception e)
{
@ -373,8 +380,8 @@ public class QInstanceValidator
assertCondition(join.getType() != null, "Missing type for join: " + joinName);
assertCondition(CollectionUtils.nullSafeHasContents(join.getJoinOns()), "Missing joinOns for join: " + joinName);
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " join " + joinName + " is not a defined table in this instance.");
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " join " + joinName + " is not a defined table in this instance.");
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " in join " + joinName + " is not a defined table in this instance.");
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " in join " + joinName + " is not a defined table in this instance.");
for(JoinOn joinOn : CollectionUtils.nonNullList(join.getJoinOns()))
{
@ -543,6 +550,60 @@ public class QInstanceValidator
{
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
///////////////////////
// validate variants //
///////////////////////
BackendVariantsConfig backendVariantsConfig = backend.getBackendVariantsConfig();
if(BooleanUtils.isTrue(backend.getUsesVariants()))
{
if(assertCondition(backendVariantsConfig != null, "Missing backendVariantsConfig in backend [" + backendName + "] which is marked as usesVariants"))
{
assertCondition(StringUtils.hasContent(backendVariantsConfig.getVariantTypeKey()), "Missing variantTypeKey in backendVariantsConfig in [" + backendName + "]");
String optionsTableName = backendVariantsConfig.getOptionsTableName();
QTableMetaData optionsTable = qInstance.getTable(optionsTableName);
if(assertCondition(StringUtils.hasContent(optionsTableName), "Missing optionsTableName in backendVariantsConfig in [" + backendName + "]"))
{
if(assertCondition(optionsTable != null, "Unrecognized optionsTableName [" + optionsTableName + "] in backendVariantsConfig in [" + backendName + "]"))
{
QQueryFilter optionsFilter = backendVariantsConfig.getOptionsFilter();
if(optionsFilter != null)
{
validateQueryFilter(qInstance, "optionsFilter in backendVariantsConfig in backend [" + backendName + "]: ", optionsTable, optionsFilter, null);
}
}
}
Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap = backendVariantsConfig.getBackendSettingSourceFieldNameMap();
if(assertCondition(CollectionUtils.nullSafeHasContents(backendSettingSourceFieldNameMap), "Missing or empty backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]"))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// only validate field names in the backendSettingSourceFieldNameMap if there is NOT a variantRecordSupplier //
// (the idea being, that the supplier might be building a record with fieldNames that aren't in the table... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(optionsTable != null && backendVariantsConfig.getVariantRecordLookupFunction() == null)
{
for(Map.Entry<BackendVariantSetting, String> entry : backendSettingSourceFieldNameMap.entrySet())
{
assertCondition(optionsTable.getFields().containsKey(entry.getValue()), "Unrecognized fieldName [" + entry.getValue() + "] in backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]");
}
}
}
if(backendVariantsConfig.getVariantRecordLookupFunction() != null)
{
validateSimpleCodeReference("VariantRecordSupplier in backendVariantsConfig in backend [" + backendName + "]: ", backendVariantsConfig.getVariantRecordLookupFunction(), UnsafeFunction.class, Function.class);
}
}
}
else
{
assertCondition(backendVariantsConfig == null, "Should not have a backendVariantsConfig in backend [" + backendName + "] which is not marked as usesVariants");
}
///////////////////////////////////////////
// let the backend do its own validation //
///////////////////////////////////////////
backend.performValidation(this);
runPlugins(QBackendMetaData.class, backend, qInstance);
@ -577,7 +638,7 @@ public class QInstanceValidator
private void validateAuthentication(QInstance qInstance)
{
QAuthenticationMetaData authentication = qInstance.getAuthentication();
if(authentication != null)
if(assertCondition(authentication != null, "Authentication MetaData must be defined."))
{
if(authentication.getCustomizer() != null)
{
@ -780,7 +841,7 @@ public class QInstanceValidator
{
if(assertCondition(StringUtils.hasContent(association.getName()), "missing a name for an Association on table " + table.getName()))
{
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
boolean recognizedTable = false;
if(assertCondition(StringUtils.hasContent(association.getAssociatedTableName()), "missing associatedTableName" + messageSuffix))
{
@ -988,7 +1049,15 @@ public class QInstanceValidator
@SuppressWarnings("unchecked")
Class<FieldBehavior<?>> behaviorClass = (Class<FieldBehavior<?>>) fieldBehavior.getClass();
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
List<String> behaviorErrors = fieldBehavior.validateBehaviorConfiguration(table, field);
if(behaviorErrors != null)
{
String prefixMinusTrailingSpace = prefix.replaceFirst(" *$", "");
for(String behaviorError : behaviorErrors)
{
errors.add(prefixMinusTrailingSpace + ": " + behaviorClass.getSimpleName() + ": " + behaviorError);
}
}
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
{
@ -1348,7 +1417,7 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getExpectedType() != null)
{
assertObjectCanBeCasted(prefix, tableCustomizer.getExpectedType(), customizerInstance);
assertObjectCanBeCasted(prefix, customizerInstance, tableCustomizer.getExpectedType());
}
}
}
@ -1360,18 +1429,31 @@ public class QInstanceValidator
/*******************************************************************************
** Make sure that a given object can be casted to an expected type.
*******************************************************************************/
private <T> T assertObjectCanBeCasted(String errorPrefix, Class<T> expectedType, Object object)
private void assertObjectCanBeCasted(String errorPrefix, Object object, Class<?>... anyOfExpectedClasses)
{
T castedObject = null;
try
for(Class<?> expectedClass : anyOfExpectedClasses)
{
castedObject = expectedType.cast(object);
try
{
expectedClass.cast(object);
return;
}
catch(ClassCastException e)
{
/////////////////////////////////////
// try next type (if there is one) //
/////////////////////////////////////
}
}
catch(ClassCastException e)
if(anyOfExpectedClasses.length == 1)
{
errors.add(errorPrefix + "CodeReference is not of the expected type: " + expectedType);
errors.add(errorPrefix + "CodeReference is not of the expected type: " + anyOfExpectedClasses[0]);
}
else
{
errors.add(errorPrefix + "CodeReference is not any of the expected types: " + Arrays.stream(anyOfExpectedClasses).map(c -> c.getName()).collect(Collectors.joining(", ")));
}
return castedObject;
}
@ -1608,12 +1690,12 @@ public class QInstanceValidator
for(QFieldMetaData field : process.getInputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName());
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName() + " ");
}
for(QFieldMetaData field : process.getOutputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName());
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName() + " ");
}
if(process.getCancelStep() != null)
@ -1824,7 +1906,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
public void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
{
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
{
@ -1868,7 +1950,8 @@ public class QInstanceValidator
{
if(fieldName.contains("."))
{
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
String tableNameBeforeDot = fieldName.substring(0, fieldName.lastIndexOf("."));
if(CollectionUtils.nullSafeHasContents(queryJoins))
{
@ -1892,11 +1975,32 @@ public class QInstanceValidator
}
else
{
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())
// {
// }
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);
}
}
}
@ -2000,6 +2104,11 @@ public class QInstanceValidator
}
}
if(widget.getValidatorPlugin() != null)
{
widget.getValidatorPlugin().validate(widget, qInstance, this);
}
runPlugins(QWidgetMetaDataInterface.class, widget, qInstance);
}
);
@ -2099,6 +2208,8 @@ public class QInstanceValidator
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
assertCondition(possibleValueSource.getIdType() != null, "possibleValueSource " + name + " is missing its idType.");
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
}
@ -2108,7 +2219,8 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?> expectedClass)
@SafeVarargs
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
{
if(!preAssertionsForCodeReference(codeReference, prefix))
{
@ -2136,7 +2248,7 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////
if(classInstance != null)
{
assertObjectCanBeCasted(prefix, expectedClass, classInstance);
assertObjectCanBeCasted(prefix, classInstance, anyOfExpectedClasses);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.plugins;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Interface for additional / optional enrichment to be done on q instance members.
** Some may be provided by QQQ - others can be defined by applications.
*******************************************************************************/
public interface QInstanceEnricherPluginInterface<T>
{
/*******************************************************************************
**
*******************************************************************************/
void enrich(T object, QInstance qInstance);
}

View File

@ -326,6 +326,20 @@ public class AuditSingleInput implements Serializable
/*******************************************************************************
** Fluent setter for details
*******************************************************************************/
public AuditSingleInput withDetailMessages(List<String> details)
{
for(String detail : details)
{
addDetail(message);
}
return (this);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -40,6 +40,8 @@ public class ProcessState implements Serializable
private Map<String, Serializable> values = new HashMap<>();
private List<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty();
private Optional<String> backStepName = Optional.empty();
private boolean isStepBack = false;
private ProcessMetaDataAdjustment processMetaDataAdjustment = null;
@ -122,6 +124,39 @@ public class ProcessState implements Serializable
/*******************************************************************************
** Getter for backStepName
**
*******************************************************************************/
public Optional<String> getBackStepName()
{
return backStepName;
}
/*******************************************************************************
** Setter for backStepName
**
*******************************************************************************/
public void setBackStepName(String backStepName)
{
this.backStepName = Optional.of(backStepName);
}
/*******************************************************************************
** clear out the value of backStepName (set the Optional to empty)
**
*******************************************************************************/
public void clearBackStepName()
{
this.backStepName = Optional.empty();
}
/*******************************************************************************
** Getter for stepList
**
@ -176,4 +211,35 @@ public class ProcessState implements Serializable
}
/*******************************************************************************
** Getter for isStepBack
*******************************************************************************/
public boolean getIsStepBack()
{
return (this.isStepBack);
}
/*******************************************************************************
** Setter for isStepBack
*******************************************************************************/
public void setIsStepBack(boolean isStepBack)
{
this.isStepBack = isStepBack;
}
/*******************************************************************************
** Fluent setter for isStepBack
*******************************************************************************/
public ProcessState withIsStepBack(boolean isStepBack)
{
this.isStepBack = isStepBack;
return (this);
}
}

View File

@ -53,6 +53,7 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
//////////////////////////////////////////////////////////////////////////
private ArrayList<Serializable> primaryKeys;
private ArrayList<String> bulletsOfText;
/*******************************************************************************
@ -497,4 +498,35 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
return (this);
}
/*******************************************************************************
** Getter for bulletsOfText
*******************************************************************************/
public ArrayList<String> getBulletsOfText()
{
return (this.bulletsOfText);
}
/*******************************************************************************
** Setter for bulletsOfText
*******************************************************************************/
public void setBulletsOfText(ArrayList<String> bulletsOfText)
{
this.bulletsOfText = bulletsOfText;
}
/*******************************************************************************
** Fluent setter for bulletsOfText
*******************************************************************************/
public ProcessSummaryLine withBulletsOfText(ArrayList<String> bulletsOfText)
{
this.bulletsOfText = bulletsOfText;
return (this);
}
}

View File

@ -25,19 +25,28 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
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.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;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerMessage;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -46,6 +55,8 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class RunBackendStepInput extends AbstractActionInput
{
private static final QLogger LOG = QLogger.getLogger(RunBackendStepInput.class);
private ProcessState processState;
private String processName;
private String tableName;
@ -55,12 +66,13 @@ public class RunBackendStepInput extends AbstractActionInput
private RunProcessInput.FrontendStepBehavior frontendStepBehavior;
private Instant basepullLastRunTime;
private ProcessTracerInterface processTracer;
////////////////////////////////////////////////////////////////////////////
// note - new fields should generally be added in method: cloneFieldsInto //
////////////////////////////////////////////////////////////////////////////
/*******************************************************************************
**
*******************************************************************************/
@ -96,6 +108,7 @@ public class RunBackendStepInput extends AbstractActionInput
target.setAsyncJobCallback(getAsyncJobCallback());
target.setFrontendStepBehavior(getFrontendStepBehavior());
target.setValues(getValues());
target.setProcessTracer(getProcessTracer().orElse(null));
}
@ -238,6 +251,26 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
** Getter for records converted to entities of a given type.
**
*******************************************************************************/
public <E extends QRecordEntity> List<E> getRecordsAsEntities(Class<E> entityClass) throws QException
{
List<E> rs = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////
// note - important to call getRecords here, which is overwritten in subclasses! //
///////////////////////////////////////////////////////////////////////////////////
for(QRecord record : getRecords())
{
rs.add(QRecordEntity.fromQRecord(entityClass, record));
}
return (rs);
}
/*******************************************************************************
** Setter for records
**
@ -419,6 +452,17 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
** Accessor for processState's isStepBack attribute
**
*******************************************************************************/
public boolean getIsStepBack()
{
return processState.getIsStepBack();
}
/*******************************************************************************
** Accessor for processState - protected, because we generally want to access
** its members through wrapper methods, we think
@ -524,4 +568,64 @@ public class RunBackendStepInput extends AbstractActionInput
return (this);
}
/*******************************************************************************
** Setter for processTracer
*******************************************************************************/
public void setProcessTracer(ProcessTracerInterface processTracer)
{
this.processTracer = processTracer;
}
/*******************************************************************************
** Fluent setter for processTracer
*******************************************************************************/
public RunBackendStepInput withProcessTracer(ProcessTracerInterface processTracer)
{
this.processTracer = processTracer;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public Optional<ProcessTracerInterface> getProcessTracer()
{
return Optional.ofNullable(processTracer);
}
/***************************************************************************
**
***************************************************************************/
public void traceMessage(ProcessTracerMessage message)
{
if(processTracer != null && message != null)
{
try
{
processTracer.handleMessage(this, message);
}
catch(Exception e)
{
LOG.warn("Error tracing message", e, logPair("message", message));
}
}
}
/***************************************************************************
**
***************************************************************************/
public QProcessMetaData getProcess()
{
return (QContext.getQInstance().getProcess(getProcessName()));
}
}

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
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.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -258,7 +259,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/*******************************************************************************
**
** add a record to the step output, e.g., for going through to the next step.
*******************************************************************************/
public void addRecord(QRecord record)
{
@ -271,6 +272,16 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/***************************************************************************
** add a RecordEntity to the step output, e.g., for going through to the next step.
***************************************************************************/
public void addRecordEntity(QRecordEntity recordEntity)
{
addRecord(recordEntity.toQRecord());
}
/*******************************************************************************
** Getter for auditInputList
*******************************************************************************/

View File

@ -49,6 +49,7 @@ public class RunProcessInput extends AbstractActionInput
private ProcessState processState;
private FrontendStepBehavior frontendStepBehavior = FrontendStepBehavior.BREAK;
private String startAfterStep;
private String startAtStep;
private String processUUID;
private AsyncJobCallback asyncJobCallback;
@ -451,4 +452,35 @@ public class RunProcessInput extends AbstractActionInput
{
return asyncJobCallback;
}
/*******************************************************************************
** Getter for startAtStep
*******************************************************************************/
public String getStartAtStep()
{
return (this.startAtStep);
}
/*******************************************************************************
** Setter for startAtStep
*******************************************************************************/
public void setStartAtStep(String startAtStep)
{
this.startAtStep = startAtStep;
}
/*******************************************************************************
** Fluent setter for startAtStep
*******************************************************************************/
public RunProcessInput withStartAtStep(String startAtStep)
{
this.startAtStep = startAtStep;
return (this);
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.actions.tables.query;
/*******************************************************************************
**
*******************************************************************************/
public enum CriteriaOption implements CriteriaOptionInterface
{
CASE_INSENSITIVE;
}

View File

@ -0,0 +1,30 @@
/*
* 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.actions.tables.query;
/*******************************************************************************
**
*******************************************************************************/
public interface CriteriaOptionInterface
{
}

View File

@ -26,8 +26,10 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
@ -45,7 +47,7 @@ public class QFilterCriteria implements Serializable, Cloneable, QMetaDataObject
{
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
private String fieldName;
private String fieldName;
private QCriteriaOperator operator;
private List<Serializable> values;
@ -54,6 +56,8 @@ public class QFilterCriteria implements Serializable, Cloneable, QMetaDataObject
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String otherFieldName;
private Set<CriteriaOptionInterface> options = null;
/*******************************************************************************
@ -70,6 +74,13 @@ public class QFilterCriteria implements Serializable, Cloneable, QMetaDataObject
clone.values = new ArrayList<>();
clone.values.addAll(values);
}
if(options != null)
{
clone.options = new HashSet<>();
clone.options.addAll(options);
}
return clone;
}
catch(CloneNotSupportedException e)
@ -386,4 +397,78 @@ public class QFilterCriteria implements Serializable, Cloneable, QMetaDataObject
return Objects.hash(fieldName, operator, values, otherFieldName);
}
/*******************************************************************************
** Getter for options
*******************************************************************************/
public Set<CriteriaOptionInterface> getOptions()
{
return (this.options);
}
/*******************************************************************************
** Setter for options
*******************************************************************************/
public void setOptions(Set<CriteriaOptionInterface> options)
{
this.options = options;
}
/*******************************************************************************
** Fluent setter for options
*******************************************************************************/
public QFilterCriteria withOptions(Set<CriteriaOptionInterface> options)
{
this.options = options;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public QFilterCriteria withOption(CriteriaOptionInterface option)
{
if(options == null)
{
options = new HashSet<>();
}
options.add(option);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public QFilterCriteria withoutOption(CriteriaOptionInterface option)
{
if(options != null)
{
options.remove(option);
}
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasOption(CriteriaOptionInterface option)
{
if(options == null)
{
return (false);
}
return (options.contains(option));
}
}

View File

@ -854,4 +854,20 @@ public class QQueryFilter implements Serializable, Cloneable, QMetaDataObject
}
/***************************************************************************
**
***************************************************************************/
public void applyCriteriaOptionToAllCriteria(CriteriaOptionInterface criteriaOption)
{
for(QFilterCriteria criteria : CollectionUtils.nonNullList(this.criteria))
{
criteria.withOption(criteriaOption);
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
{
subFilter.applyCriteriaOptionToAllCriteria(criteriaOption);
}
}
}

View File

@ -22,13 +22,14 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.storage;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
/*******************************************************************************
** Input for Storage actions.
*******************************************************************************/
public class StorageInput extends AbstractTableActionInput
public class StorageInput extends AbstractTableActionInput implements Serializable
{
private String reference;
private String contentType;

View File

@ -38,9 +38,10 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
private QQueryFilter defaultQueryFilter;
private String searchTerm;
private List<Serializable> idList;
private List<String> labelList;
private Integer skip = 0;
private Integer limit = 100;
private Integer limit = 250;
@ -281,4 +282,35 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
this.limit = limit;
return (this);
}
/*******************************************************************************
** Getter for labelList
*******************************************************************************/
public List<String> getLabelList()
{
return (this.labelList);
}
/*******************************************************************************
** Setter for labelList
*******************************************************************************/
public void setLabelList(List<String> labelList)
{
this.labelList = labelList;
}
/*******************************************************************************
** Fluent setter for labelList
*******************************************************************************/
public SearchPossibleValueSourceInput withLabelList(List<String> labelList)
{
this.labelList = labelList;
return (this);
}
}

View File

@ -35,6 +35,7 @@ public class SearchPossibleValueSourceOutput extends AbstractActionOutput
{
private List<QPossibleValue<?>> results = new ArrayList<>();
private String warning;
/*******************************************************************************
@ -88,4 +89,35 @@ public class SearchPossibleValueSourceOutput extends AbstractActionOutput
return (this);
}
/*******************************************************************************
** Getter for warning
*******************************************************************************/
public String getWarning()
{
return (this.warning);
}
/*******************************************************************************
** Setter for warning
*******************************************************************************/
public void setWarning(String warning)
{
this.warning = warning;
}
/*******************************************************************************
** Fluent setter for warning
*******************************************************************************/
public SearchPossibleValueSourceOutput withWarning(String warning)
{
this.warning = warning;
return (this);
}
}

View File

@ -51,6 +51,7 @@ public class ChildRecordListData extends QWidgetData
private boolean canAddChildRecord = false;
private Map<String, Serializable> defaultValuesForNewChildRecords;
private Set<String> disabledFieldsForNewChildRecords;
private Map<String, String> defaultValuesForNewChildRecordsFromParentFields;
@ -523,6 +524,37 @@ public class ChildRecordListData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for defaultValuesForNewChildRecordsFromParentFields
*******************************************************************************/
public Map<String, String> getDefaultValuesForNewChildRecordsFromParentFields()
{
return (this.defaultValuesForNewChildRecordsFromParentFields);
}
/*******************************************************************************
** Setter for defaultValuesForNewChildRecordsFromParentFields
*******************************************************************************/
public void setDefaultValuesForNewChildRecordsFromParentFields(Map<String, String> defaultValuesForNewChildRecordsFromParentFields)
{
this.defaultValuesForNewChildRecordsFromParentFields = defaultValuesForNewChildRecordsFromParentFields;
}
/*******************************************************************************
** Fluent setter for defaultValuesForNewChildRecordsFromParentFields
*******************************************************************************/
public ChildRecordListData withDefaultValuesForNewChildRecordsFromParentFields(Map<String, String> defaultValuesForNewChildRecordsFromParentFields)
{
this.defaultValuesForNewChildRecordsFromParentFields = defaultValuesForNewChildRecordsFromParentFields;
return (this);
}
}

View File

@ -34,8 +34,12 @@ public class FilterAndColumnsSetupData extends QWidgetData
private String tableName;
private Boolean allowVariables = false;
private Boolean hideColumns = false;
private Boolean hidePreview = false;
private List<String> filterDefaultFieldNames;
private String filterFieldName = "queryFilterJson";
private String columnFieldName = "columnsJson";
/*******************************************************************************
@ -193,4 +197,97 @@ public class FilterAndColumnsSetupData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for hidePreview
*******************************************************************************/
public Boolean getHidePreview()
{
return (this.hidePreview);
}
/*******************************************************************************
** Setter for hidePreview
*******************************************************************************/
public void setHidePreview(Boolean hidePreview)
{
this.hidePreview = hidePreview;
}
/*******************************************************************************
** Fluent setter for hidePreview
*******************************************************************************/
public FilterAndColumnsSetupData withHidePreview(Boolean hidePreview)
{
this.hidePreview = hidePreview;
return (this);
}
/*******************************************************************************
** Getter for filterFieldName
*******************************************************************************/
public String getFilterFieldName()
{
return (this.filterFieldName);
}
/*******************************************************************************
** Setter for filterFieldName
*******************************************************************************/
public void setFilterFieldName(String filterFieldName)
{
this.filterFieldName = filterFieldName;
}
/*******************************************************************************
** Fluent setter for filterFieldName
*******************************************************************************/
public FilterAndColumnsSetupData withFilterFieldName(String filterFieldName)
{
this.filterFieldName = filterFieldName;
return (this);
}
/*******************************************************************************
** Getter for columnFieldName
*******************************************************************************/
public String getColumnFieldName()
{
return (this.columnFieldName);
}
/*******************************************************************************
** Setter for columnFieldName
*******************************************************************************/
public void setColumnFieldName(String columnFieldName)
{
this.columnFieldName = columnFieldName;
}
/*******************************************************************************
** Fluent setter for columnFieldName
*******************************************************************************/
public FilterAndColumnsSetupData withColumnFieldName(String columnFieldName)
{
this.columnFieldName = columnFieldName;
return (this);
}
}

View File

@ -89,6 +89,11 @@ public @interface QField
*******************************************************************************/
int maxLength() default Integer.MAX_VALUE;
/*******************************************************************************
**
*******************************************************************************/
int gridColumns() default -1;
/*******************************************************************************
**
*******************************************************************************/

View File

@ -154,7 +154,7 @@ public class QRecord implements Serializable
return (null);
}
Map<String, V> clone = new LinkedHashMap<>();
Map<String, V> clone = new LinkedHashMap<>(map.size());
for(Map.Entry<String, V> entry : map.entrySet())
{
Serializable value = entry.getValue();
@ -246,6 +246,24 @@ public class QRecord implements Serializable
}
/***************************************************************************
** copy all values from 'joinedRecord' into this record's values map,
** prefixing field names with joinTableNam + "."
***************************************************************************/
public void addJoinedRecordValues(String joinTableName, QRecord joinedRecord)
{
if(joinedRecord == null)
{
return;
}
for(Map.Entry<String, Serializable> entry : joinedRecord.getValues().entrySet())
{
setValue(joinTableName + "." + entry.getKey(), entry.getValue());
}
}
/*******************************************************************************
**

View File

@ -41,11 +41,14 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.ListingHash;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -61,6 +64,11 @@ public abstract class QRecordEntity
private Map<String, Serializable> originalRecordValues;
////////////////////////////////////////////////////////////////////////////////
// map of entity class names to QTableMetaData objects that they helped build //
////////////////////////////////////////////////////////////////////////////////
private static Map<String, QTableMetaData> tableReferences = new HashMap<>();
/*******************************************************************************
@ -95,6 +103,19 @@ public abstract class QRecordEntity
/***************************************************************************
** register a mapping between an entity class and a table that it is associated with.
***************************************************************************/
public static void registerTable(Class<? extends QRecordEntity> entityClass, QTableMetaData table)
{
if(entityClass != null && table != null)
{
tableReferences.put(entityClass.getName(), table);
}
}
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord
**
@ -176,7 +197,10 @@ public abstract class QRecordEntity
/*******************************************************************************
** Convert this entity to a QRecord.
** Convert this entity to a QRecord. ALL fields in the entity will be set
** in the QRecord. Note that, if you're using this for an input to the UpdateAction,
** that this could cause values to be set to null, e.g., if you constructed
** a entity from scratch, and didn't set all values in it!!
**
*******************************************************************************/
public QRecord toQRecord() throws QRuntimeException
@ -190,25 +214,7 @@ public abstract class QRecordEntity
qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
@SuppressWarnings("unchecked")
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
/////////////////////////////////////////////////////////////////////////////////
// do this so an empty list in the entity becomes an empty list in the QRecord //
/////////////////////////////////////////////////////////////////////////////////
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
}
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
{
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
}
}
toQRecordProcessAssociations(qRecord, (entity) -> entity.toQRecord());
return (qRecord);
}
@ -220,15 +226,65 @@ public abstract class QRecordEntity
/***************************************************************************
*
***************************************************************************/
private void toQRecordProcessAssociations(QRecord outputRecord, Function<QRecordEntity, QRecord> toRecordFunction) throws Exception
{
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
@SuppressWarnings("unchecked")
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
outputRecord.withAssociatedRecords(associationName, new ArrayList<>());
for(QRecordEntity associatedEntity : associatedEntities)
{
outputRecord.withAssociatedRecord(associationName, toRecordFunction.apply(associatedEntity));
}
}
}
}
/*******************************************************************************
**
** Overload of toQRecordOnlyChangedFields that preserves original behavior of
** that method, which is, to NOT includePrimaryKey
*******************************************************************************/
@Deprecated(since = "includePrimaryKey param was added")
public QRecord toQRecordOnlyChangedFields()
{
return toQRecordOnlyChangedFields(false);
}
/*******************************************************************************
** Useful for the use-case of:
** - fetch a QRecord (e.g., QueryAction or GetAction)
** - build a QRecordEntity out of it
** - change a field (or two) in it
** - want to pass it into an UpdateAction, and want to see only the fields that
** you know you changed get passed in to UpdateAction (e.g., PATCH semantics).
**
** But also - per the includePrimaryKey param, include the primaryKey in the
** records (e.g., to tell the Update which records to update).
**
** Also, useful for:
** - construct new entity, calling setters to populate some fields
** - pass that entity into
*******************************************************************************/
public QRecord toQRecordOnlyChangedFields(boolean includePrimaryKey)
{
try
{
QRecord qRecord = new QRecord();
String primaryKeyFieldName = ObjectUtils.tryElse(() -> tableReferences.get(getClass().getName()).getPrimaryKeyField(), null);
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
{
Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this);
@ -238,31 +294,16 @@ public abstract class QRecordEntity
originalValue = originalRecordValues.get(qRecordEntityField.getFieldName());
}
if(!Objects.equals(thisValue, originalValue))
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this value and the original value don't match - OR - this is the table's primary key field - then put the value in the record. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!Objects.equals(thisValue, originalValue) || (includePrimaryKey && Objects.equals(primaryKeyFieldName, qRecordEntityField.getFieldName())))
{
qRecord.setValue(qRecordEntityField.getFieldName(), thisValue);
}
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
@SuppressWarnings("unchecked")
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
/////////////////////////////////////////////////////////////////////////////////
// do this so an empty list in the entity becomes an empty list in the QRecord //
/////////////////////////////////////////////////////////////////////////////////
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
}
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
{
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
}
}
toQRecordProcessAssociations(qRecord, (entity) -> entity.toQRecordOnlyChangedFields(includePrimaryKey));
return (qRecord);
}
@ -488,15 +529,16 @@ public abstract class QRecordEntity
{
// todo - more types!!
return (returnType.equals(String.class)
|| returnType.equals(Integer.class)
|| returnType.equals(int.class)
|| returnType.equals(Boolean.class)
|| returnType.equals(boolean.class)
|| returnType.equals(BigDecimal.class)
|| returnType.equals(Instant.class)
|| returnType.equals(LocalDate.class)
|| returnType.equals(LocalTime.class)
|| returnType.equals(byte[].class));
|| returnType.equals(Integer.class)
|| returnType.equals(Long.class)
|| returnType.equals(int.class)
|| returnType.equals(Boolean.class)
|| returnType.equals(boolean.class)
|| returnType.equals(BigDecimal.class)
|| returnType.equals(Instant.class)
|| returnType.equals(LocalDate.class)
|| returnType.equals(LocalTime.class)
|| returnType.equals(byte[].class));
/////////////////////////////////////////////
// note - this list has implications upon: //
// - QFieldType.fromClass //

View File

@ -0,0 +1,201 @@
/*
* 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.data;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;
/*******************************************************************************
** Extension on QRecord, intended to be used where you've got records from
** multiple tables, and you want to combine them into a single "wide" joined
** record - but to do so without copying or modifying any of the individual
** records.
**
** e.g., given:
** - Order (id, orderNo, orderDate) (main table)
** - LineItem (id, sku, quantity)
** - Extrinsic (id, key, value)
**
** If set up in here as:
** - new QRecordWithJoinedRecords(order)
** .withJoinedRecordValues(lineItem)
** .withJoinedRecordValues(extrinsic)
**
** Then we'd have the appearance of values in the object like:
** - id, orderNo, orderDate, lineItem.id, lineItem.sku, lineItem.quantity, extrinsic.id, extrinsic.key, extrinsic.value
**
** Which, by the by, is how a query that returns joined records looks, and, is
** what BackendQueryFilterUtils can use to do filter.
**
** This is done without copying or mutating any of the records (which, if you just use
** QRecord.withJoinedRecordValues, then those values are copied into the main record)
** - because this object is just storing references to the input records.
**
** Note that this implies that, values changed in this record (e.g, calls to setValue)
** WILL impact the underlying records!
*******************************************************************************/
public class QRecordWithJoinedRecords extends QRecord
{
private QRecord mainRecord;
private Map<String, QRecord> components = new LinkedHashMap<>();
/***************************************************************************
**
***************************************************************************/
public QRecordWithJoinedRecords(QRecord mainRecord)
{
this.mainRecord = mainRecord;
}
/*************************************************************************
**
***************************************************************************/
@Override
public void addJoinedRecordValues(String joinTableName, QRecord joinedRecord)
{
components.put(joinTableName, joinedRecord);
}
/*************************************************************************
**
***************************************************************************/
public QRecordWithJoinedRecords withJoinedRecordValues(QRecord record, String joinTableName)
{
addJoinedRecordValues(joinTableName, record);
return (this);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Serializable getValue(String fieldName)
{
return performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) -> record.getValue(f)));
}
/***************************************************************************
*
***************************************************************************/
@Override
public void setValue(String fieldName, Object value)
{
performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) ->
{
record.setValue(f, value);
return (null);
}));
}
/***************************************************************************
*
***************************************************************************/
@Override
public void setValue(String fieldName, Serializable value)
{
performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) ->
{
record.setValue(f, value);
return (null);
}));
}
/***************************************************************************
**
***************************************************************************/
@Override
public void removeValue(String fieldName)
{
performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) ->
{
record.removeValue(f);
return (null);
}));
}
/***************************************************************************
** avoid having this same block in all the functions that call it...
** given a fieldName, which may be a joinTable.fieldName, apply the function
** to the right entity.
***************************************************************************/
private Serializable performFunctionOnRecordBasedOnFieldName(String fieldName, BiFunction<QRecord, String, Serializable> functionToPerform)
{
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
QRecord component = components.get(parts[0]);
if(component != null)
{
return functionToPerform.apply(component, parts[1]);
}
else
{
return null;
}
}
else
{
return functionToPerform.apply(mainRecord, fieldName);
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Serializable> getValues()
{
Map<String, Serializable> rs = new LinkedHashMap<>(mainRecord.getValues());
for(Map.Entry<String, QRecord> componentEntry : components.entrySet())
{
String joinTableName = componentEntry.getKey();
QRecord componentRecord = componentEntry.getValue();
for(Map.Entry<String, Serializable> entry : componentRecord.getValues().entrySet())
{
rs.put(joinTableName + "." + entry.getKey(), entry.getValue());
}
}
return (rs);
}
}

View File

@ -0,0 +1,47 @@
/*
* 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;
import com.kingsrook.qqq.backend.core.logging.QLogger;
/*******************************************************************************
** for use-cases where a metaDataProducer directly adds its objects to the
** qInstance, then this empty object can be returned.
*******************************************************************************/
public class EmptyMetaDataProducerOutput implements MetaDataProducerOutput
{
private static final QLogger LOG = QLogger.getLogger(EmptyMetaDataProducerOutput.class);
/***************************************************************************
**
***************************************************************************/
@Override
public void addSelfToInstance(QInstance instance)
{
/////////////////////////////////
// noop - this output is empty //
/////////////////////////////////
LOG.trace("empty meta data producer has nothing to add.");
}
}

View File

@ -29,5 +29,40 @@ 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

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@ -40,12 +41,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.producers.ChildJoinFromRecordEntityGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.producers.PossibleValueSourceOfEnumGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.PossibleValueSourceOfTableGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.RecordEntityToTableGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -82,13 +86,30 @@ public class MetaDataProducerHelper
comparatorValuesByType.put(QAppMetaData.class, 23);
}
private static MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer = null;
/*******************************************************************************
** Recursively find all classes in the given package, that implement MetaDataProducerInterface
** run them, and add their output to the given qInstance.
** run them, and add their output to the given qInstance - using the provided
** tableMetaDataCustomizer to help with all RecordEntity's that
** are configured to make tables.
**
** Note - they'll be sorted by the sortOrder they provide.
*******************************************************************************/
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName) throws QException
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName, MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer) throws QException
{
MetaDataProducerHelper.tableMetaDataCustomizer = tableMetaDataCustomizer;
processAllMetaDataProducersInPackage(instance, packageName);
MetaDataProducerHelper.tableMetaDataCustomizer = null;
}
/***************************************************************************
**
***************************************************************************/
public static List<MetaDataProducerInterface<?>> findProducers(String packageName) throws QException
{
List<Class<?>> classesInPackage;
try
@ -116,20 +137,27 @@ public class MetaDataProducerHelper
continue;
}
/////////////////////////////////////////////////////////////////////
// handle classes which are themselves MetaDataProducerInterface's //
/////////////////////////////////////////////////////////////////////
if(MetaDataProducerInterface.class.isAssignableFrom(aClass))
{
CollectionUtils.addIfNotNull(producers, processMetaDataProducer(aClass));
}
/////////////////////////////////////////////////////////////////////////
// handle classes that have the @QMetaDataProducingEntity annotation - //
// record entities that should produce meta-data //
/////////////////////////////////////////////////////////////////////////
if(aClass.isAnnotationPresent(QMetaDataProducingEntity.class))
{
QMetaDataProducingEntity qMetaDataProducingEntity = aClass.getAnnotation(QMetaDataProducingEntity.class);
if(qMetaDataProducingEntity.producePossibleValueSource())
{
producers.addAll(processMetaDataProducingEntity(aClass));
}
producers.addAll(processMetaDataProducingEntity(aClass));
}
//////////////////////////////////////////////////////////////////
// handle classes with the @QMetaDataProducingPossibleValueEnum //
// enums that are PVS's //
//////////////////////////////////////////////////////////////////
if(aClass.isAnnotationPresent(QMetaDataProducingPossibleValueEnum.class))
{
QMetaDataProducingPossibleValueEnum qMetaDataProducingPossibleValueEnum = aClass.getAnnotation(QMetaDataProducingPossibleValueEnum.class);
@ -164,6 +192,20 @@ 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 //
///////////////////////////////////////////////////////////////////////////
@ -197,17 +239,19 @@ public class MetaDataProducerHelper
**
***************************************************************************/
@SuppressWarnings("unchecked")
private static <T extends PossibleValueEnum<T>> MetaDataProducerInterface<?> processMetaDataProducingPossibleValueEnum(Class<?> aClass)
private static <T extends Serializable & PossibleValueEnum<T>> MetaDataProducerInterface<?> processMetaDataProducingPossibleValueEnum(Class<?> sourceClass)
{
String warningPrefix = "Found a class annotated as @" + QMetaDataProducingPossibleValueEnum.class.getSimpleName();
if(!PossibleValueEnum.class.isAssignableFrom(aClass))
if(!PossibleValueEnum.class.isAssignableFrom(sourceClass))
{
LOG.warn(warningPrefix + ", but which is not a " + PossibleValueEnum.class.getSimpleName() + ", so it will not be used.", logPair("class", aClass.getSimpleName()));
LOG.warn(warningPrefix + ", but which is not a " + PossibleValueEnum.class.getSimpleName() + ", so it will not be used.", logPair("class", sourceClass.getSimpleName()));
return null;
}
PossibleValueEnum<?>[] values = (PossibleValueEnum<?>[]) aClass.getEnumConstants();
return (new PossibleValueSourceOfEnumGenericMetaDataProducer<T>(aClass.getSimpleName(), (PossibleValueEnum<T>[]) values));
PossibleValueEnum<?>[] values = (PossibleValueEnum<?>[]) sourceClass.getEnumConstants();
PossibleValueSourceOfEnumGenericMetaDataProducer<T> producer = new PossibleValueSourceOfEnumGenericMetaDataProducer<>(sourceClass.getSimpleName(), (PossibleValueEnum<T>[]) values);
producer.setSourceClass(sourceClass);
return producer;
}
@ -215,41 +259,90 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static List<MetaDataProducerInterface<?>> processMetaDataProducingEntity(Class<?> aClass) throws Exception
private static List<MetaDataProducerInterface<?>> processMetaDataProducingEntity(Class<?> sourceClass) throws Exception
{
List<MetaDataProducerInterface<?>> rs = new ArrayList<>();
String warningPrefix = "Found a class annotated as @" + QMetaDataProducingEntity.class.getSimpleName();
if(!QRecordEntity.class.isAssignableFrom(aClass))
QMetaDataProducingEntity qMetaDataProducingEntity = sourceClass.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))
{
LOG.warn(warningPrefix + ", but which is not a " + QRecordEntity.class.getSimpleName() + ", so it will not be used.", logPair("class", aClass.getSimpleName()));
LOG.warn(warningPrefix + ", but which is not a " + QRecordEntity.class.getSimpleName() + ", so it will not be used.", logPair("class", sourceClass.getSimpleName()));
return (rs);
}
Field tableNameField = aClass.getDeclaredField("TABLE_NAME");
@SuppressWarnings("unchecked") // safe per the check above.
Class<? extends QRecordEntity> recordEntityClass = (Class<? extends QRecordEntity>) sourceClass;
////////////////////////////////////////////////
// get TABLE_NAME static field from the class //
////////////////////////////////////////////////
Field tableNameField = recordEntityClass.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", aClass.getSimpleName()));
LOG.warn(warningPrefix + ", but whose TABLE_NAME field is not a String, so it will not be used.", logPair("class", recordEntityClass.getSimpleName()));
return (rs);
}
String tableNameValue = (String) tableNameField.get(null);
rs.add(new PossibleValueSourceOfTableGenericMetaDataProducer(tableNameValue));
//////////////////////////////////////////
// add table producer, if so configured //
//////////////////////////////////////////
if(qMetaDataProducingEntity.produceTableMetaData())
{
try
{
Class<? extends MetaDataCustomizerInterface<?>> genericMetaProductionCustomizer = (Class<? extends MetaDataCustomizerInterface<?>>) qMetaDataProducingEntity.tableMetaDataCustomizer();
Class<? extends MetaDataCustomizerInterface<QTableMetaData>> tableMetaDataProductionCustomizer = null;
if(!genericMetaProductionCustomizer.equals(MetaDataCustomizerInterface.NoopMetaDataCustomizer.class))
{
tableMetaDataProductionCustomizer = (Class<? extends MetaDataCustomizerInterface<QTableMetaData>>) genericMetaProductionCustomizer;
}
RecordEntityToTableGenericMetaDataProducer producer = new RecordEntityToTableGenericMetaDataProducer(tableNameValue, recordEntityClass, tableMetaDataProductionCustomizer);
producer.setSourceClass(recordEntityClass);
if(tableMetaDataCustomizer != null)
{
producer.addRecordEntityTableMetaDataProductionCustomizer(tableMetaDataCustomizer);
}
rs.add(producer);
}
catch(Exception e)
{
throw new QException("Error processing table meta data producer for entity class: " + recordEntityClass.getName(), e);
}
}
////////////////////////////////////////
// add PVS producer, if so configured //
////////////////////////////////////////
if(qMetaDataProducingEntity.producePossibleValueSource())
{
PossibleValueSourceOfTableGenericMetaDataProducer producer = new PossibleValueSourceOfTableGenericMetaDataProducer(tableNameValue);
producer.setSourceClass(recordEntityClass);
rs.add(producer);
}
//////////////////////////
// process child tables //
//////////////////////////
QMetaDataProducingEntity qMetaDataProducingEntity = aClass.getAnnotation(QMetaDataProducingEntity.class);
for(ChildTable childTable : qMetaDataProducingEntity.childTables())
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
if(childTable.childJoin().enabled())
{
CollectionUtils.addIfNotNull(rs, processChildJoin(aClass, childTable));
CollectionUtils.addIfNotNull(rs, processChildJoin(recordEntityClass, childTable));
if(childTable.childRecordListWidget().enabled())
{
CollectionUtils.addIfNotNull(rs, processChildRecordListWidget(aClass, childTable));
CollectionUtils.addIfNotNull(rs, processChildRecordListWidget(recordEntityClass, childTable));
}
}
else
@ -259,7 +352,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", aClass.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", recordEntityClass.getSimpleName()), logPair("childEntityClass", childEntityClass.getSimpleName()));
}
}
}
@ -272,14 +365,16 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processChildRecordListWidget(Class<?> aClass, ChildTable childTable) throws Exception
private static MetaDataProducerInterface<?> processChildRecordListWidget(Class<? extends QRecordEntity> sourceClass, ChildTable childTable) throws Exception
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
String parentTableName = getTableNameStaticFieldValue(aClass);
String parentTableName = getTableNameStaticFieldValue(sourceClass);
String childTableName = getTableNameStaticFieldValue(childEntityClass);
ChildRecordListWidget childRecordListWidget = childTable.childRecordListWidget();
return (new ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, childRecordListWidget));
ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer producer = new ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, childRecordListWidget);
producer.setSourceClass(sourceClass);
return producer;
}
@ -309,20 +404,22 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processChildJoin(Class<?> aClass, ChildTable childTable) throws Exception
private static MetaDataProducerInterface<?> processChildJoin(Class<? extends QRecordEntity> entityClass, ChildTable childTable) throws Exception
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
String parentTableName = getTableNameStaticFieldValue(aClass);
String parentTableName = getTableNameStaticFieldValue(entityClass);
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 [" + aClass.getSimpleName() + "]");
LOG.warn("Could not find field in [" + childEntityClass.getSimpleName() + "] with possibleValueSource referencing table [" + entityClass.getSimpleName() + "]");
return (null);
}
return (new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName));
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName);
producer.setSourceClass(entityClass);
return producer;
}
@ -330,18 +427,20 @@ public class MetaDataProducerHelper
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processMetaDataProducer(Class<?> aClass) throws Exception
private static MetaDataProducerInterface<?> processMetaDataProducer(Class<?> sourceCClass) throws Exception
{
for(Constructor<?> constructor : aClass.getConstructors())
for(Constructor<?> constructor : sourceCClass.getConstructors())
{
if(constructor.getParameterCount() == 0)
{
Object o = constructor.newInstance();
return (MetaDataProducerInterface<?>) o;
MetaDataProducerInterface<?> producer = (MetaDataProducerInterface<?>) o;
producer.setSourceClass(sourceCClass);
return producer;
}
}
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()));
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()));
return null;
}
@ -361,4 +460,35 @@ public class MetaDataProducerHelper
String tableNameValue = (String) tableNameField.get(null);
return (tableNameValue);
}
/*******************************************************************************
** Getter for tableMetaDataCustomizer
*******************************************************************************/
public MetaDataCustomizerInterface<QTableMetaData> getTableMetaDataCustomizer()
{
return (MetaDataProducerHelper.tableMetaDataCustomizer);
}
/*******************************************************************************
** Setter for tableMetaDataCustomizer
*******************************************************************************/
public void setTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
MetaDataProducerHelper.tableMetaDataCustomizer = tableMetaDataCustomizer;
}
/*******************************************************************************
** Fluent setter for tableMetaDataCustomizer
*******************************************************************************/
public void withTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
MetaDataProducerHelper.tableMetaDataCustomizer = tableMetaDataCustomizer;
}
}

View File

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

View File

@ -24,6 +24,7 @@ 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;
@ -31,10 +32,12 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
** Output object for a MetaDataProducer, which contains multiple meta-data
** objects.
*******************************************************************************/
public class MetaDataProducerMultiOutput implements MetaDataProducerOutput
public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, SourceQBitAware
{
private List<MetaDataProducerOutput> contents;
private String sourceQBitName;
/*******************************************************************************
@ -98,4 +101,48 @@ public class MetaDataProducerMultiOutput implements MetaDataProducerOutput
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

@ -26,8 +26,13 @@ import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
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.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.serialization.QBackendMetaDataDeserializer;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.model.metadata.variants.LegacyBackendVariantSetting;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -45,21 +50,18 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
private Set<Capability> enabledCapabilities = new HashSet<>();
private Set<Capability> disabledCapabilities = new HashSet<>();
private Boolean usesVariants = false;
private String variantOptionsTableIdField;
private String variantOptionsTableNameField;
private String variantOptionsTableTypeField;
private String variantOptionsTableTypeValue;
private String variantOptionsTableUsernameField;
private String variantOptionsTablePasswordField;
private String variantOptionsTableApiKeyField;
private String variantOptionsTableClientIdField;
private String variantOptionsTableClientSecretField;
private String variantOptionsTableName;
private Boolean usesVariants = false;
private BackendVariantsConfig backendVariantsConfig;
// todo - at some point, we may want to apply this to secret properties on subclasses?
// @JsonFilter("secretsFilter")
@Deprecated(since = "Replaced by filter in backendVariantsConfig - but leaving as field to pair with ...TypeValue for building filter")
private String variantOptionsTableTypeField; // a field on which to filter the variant-options table, to limit which records in it are available as variants
@Deprecated(since = "Replaced by variantTypeKey and value in filter in backendVariantsConfig - but leaving as field to pair with ...TypeField for building filter")
private String variantOptionsTableTypeValue; // value for the type-field, to limit which records in it are available as variants; but also, the key in the session.backendVariants map!
/*******************************************************************************
@ -394,22 +396,15 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Getter for variantOptionsTableIdField
*******************************************************************************/
public String getVariantOptionsTableIdField()
{
return (this.variantOptionsTableIdField);
}
/*******************************************************************************
** Setter for variantOptionsTableIdField
*******************************************************************************/
@Deprecated(since = "backendVariantsConfig will infer this from the variant options table's primary key")
public void setVariantOptionsTableIdField(String variantOptionsTableIdField)
{
this.variantOptionsTableIdField = variantOptionsTableIdField;
/////////////////////////////////////////////////
// noop as we migrate to backendVariantsConfig //
/////////////////////////////////////////////////
}
@ -417,30 +412,24 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableIdField
*******************************************************************************/
@Deprecated(since = "backendVariantsConfig will infer this from the variant options table's primary key")
public QBackendMetaData withVariantOptionsTableIdField(String variantOptionsTableIdField)
{
this.variantOptionsTableIdField = variantOptionsTableIdField;
this.setVariantOptionsTableIdField(variantOptionsTableIdField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableNameField
*******************************************************************************/
public String getVariantOptionsTableNameField()
{
return (this.variantOptionsTableNameField);
}
/*******************************************************************************
** Setter for variantOptionsTableNameField
*******************************************************************************/
@Deprecated(since = "backendVariantsConfig will infer this from the variant options table's recordLabel")
public void setVariantOptionsTableNameField(String variantOptionsTableNameField)
{
this.variantOptionsTableNameField = variantOptionsTableNameField;
/////////////////////////////////////////////////
// noop as we migrate to backendVariantsConfig //
/////////////////////////////////////////////////
}
@ -448,30 +437,26 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableNameField
*******************************************************************************/
@Deprecated(since = "backendVariantsConfig will infer this from the variant options table's recordLabel")
public QBackendMetaData withVariantOptionsTableNameField(String variantOptionsTableNameField)
{
this.variantOptionsTableNameField = variantOptionsTableNameField;
this.setVariantOptionsTableNameField(variantOptionsTableNameField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableTypeField
*******************************************************************************/
public String getVariantOptionsTableTypeField()
{
return (this.variantOptionsTableTypeField);
}
/*******************************************************************************
** Setter for variantOptionsTableTypeField
*******************************************************************************/
@Deprecated(since = "Replaced by fieldName in filter in backendVariantsConfig - but leaving as field to pair with ...TypeValue for building filter")
public void setVariantOptionsTableTypeField(String variantOptionsTableTypeField)
{
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
if(this.variantOptionsTableTypeValue != null)
{
this.getOrWithNewBackendVariantsConfig().setOptionsFilter(new QQueryFilter(new QFilterCriteria(variantOptionsTableTypeField, QCriteriaOperator.EQUALS, variantOptionsTableTypeValue)));
}
}
@ -479,30 +464,28 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableTypeField
*******************************************************************************/
@Deprecated(since = "Replaced by fieldName in filter in backendVariantsConfig - but leaving as field to pair with ...TypeValue for building filter")
public QBackendMetaData withVariantOptionsTableTypeField(String variantOptionsTableTypeField)
{
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
this.setVariantOptionsTableTypeField(variantOptionsTableTypeField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableTypeValue
*******************************************************************************/
public String getVariantOptionsTableTypeValue()
{
return (this.variantOptionsTableTypeValue);
}
/*******************************************************************************
** Setter for variantOptionsTableTypeValue
*******************************************************************************/
@Deprecated(since = "Replaced by variantTypeKey and value in filter in backendVariantsConfig - but leaving as field to pair with ...TypeField for building filter")
public void setVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
{
this.getOrWithNewBackendVariantsConfig().setVariantTypeKey(variantOptionsTableTypeValue);
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
if(this.variantOptionsTableTypeField != null)
{
this.getOrWithNewBackendVariantsConfig().setOptionsFilter(new QQueryFilter(new QFilterCriteria(variantOptionsTableTypeField, QCriteriaOperator.EQUALS, variantOptionsTableTypeValue)));
}
}
@ -510,30 +493,22 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableTypeValue
*******************************************************************************/
@Deprecated(since = "Replaced by variantTypeKey and value in filter in backendVariantsConfig - but leaving as field to pair with ...TypeField for building filter")
public QBackendMetaData withVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
{
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
this.setVariantOptionsTableTypeValue(variantOptionsTableTypeValue);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableUsernameField
*******************************************************************************/
public String getVariantOptionsTableUsernameField()
{
return (this.variantOptionsTableUsernameField);
}
/*******************************************************************************
** Setter for variantOptionsTableUsernameField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public void setVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
{
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
this.getOrWithNewBackendVariantsConfig().withBackendSettingSourceFieldName(LegacyBackendVariantSetting.USERNAME, variantOptionsTableUsernameField);
}
@ -541,30 +516,22 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableUsernameField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public QBackendMetaData withVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
{
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
this.setVariantOptionsTableUsernameField(variantOptionsTableUsernameField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTablePasswordField
*******************************************************************************/
public String getVariantOptionsTablePasswordField()
{
return (this.variantOptionsTablePasswordField);
}
/*******************************************************************************
** Setter for variantOptionsTablePasswordField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public void setVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
{
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
this.getOrWithNewBackendVariantsConfig().withBackendSettingSourceFieldName(LegacyBackendVariantSetting.PASSWORD, variantOptionsTablePasswordField);
}
@ -572,30 +539,22 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTablePasswordField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public QBackendMetaData withVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
{
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
this.setVariantOptionsTablePasswordField(variantOptionsTablePasswordField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableApiKeyField
*******************************************************************************/
public String getVariantOptionsTableApiKeyField()
{
return (this.variantOptionsTableApiKeyField);
}
/*******************************************************************************
** Setter for variantOptionsTableApiKeyField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public void setVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
{
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
this.getOrWithNewBackendVariantsConfig().withBackendSettingSourceFieldName(LegacyBackendVariantSetting.API_KEY, variantOptionsTableApiKeyField);
}
@ -603,30 +562,22 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableApiKeyField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public QBackendMetaData withVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
{
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
this.setVariantOptionsTableApiKeyField(variantOptionsTableApiKeyField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableName
*******************************************************************************/
public String getVariantOptionsTableName()
{
return (this.variantOptionsTableName);
}
/*******************************************************************************
** Setter for variantOptionsTableName
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.tableName")
public void setVariantOptionsTableName(String variantOptionsTableName)
{
this.variantOptionsTableName = variantOptionsTableName;
this.getOrWithNewBackendVariantsConfig().withOptionsTableName(variantOptionsTableName);
}
@ -634,9 +585,10 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableName
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.tableName")
public QBackendMetaData withVariantOptionsTableName(String variantOptionsTableName)
{
this.variantOptionsTableName = variantOptionsTableName;
this.setVariantOptionsTableName(variantOptionsTableName);
return (this);
}
@ -651,22 +603,15 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
qInstance.addBackend(this);
}
/*******************************************************************************
** Getter for variantOptionsTableClientIdField
*******************************************************************************/
public String getVariantOptionsTableClientIdField()
{
return (this.variantOptionsTableClientIdField);
}
/*******************************************************************************
** Setter for variantOptionsTableClientIdField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public void setVariantOptionsTableClientIdField(String variantOptionsTableClientIdField)
{
this.variantOptionsTableClientIdField = variantOptionsTableClientIdField;
this.getOrWithNewBackendVariantsConfig().withBackendSettingSourceFieldName(LegacyBackendVariantSetting.CLIENT_ID, variantOptionsTableClientIdField);
}
@ -674,30 +619,22 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableClientIdField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public QBackendMetaData withVariantOptionsTableClientIdField(String variantOptionsTableClientIdField)
{
this.variantOptionsTableClientIdField = variantOptionsTableClientIdField;
this.setVariantOptionsTableClientIdField(variantOptionsTableClientIdField);
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableClientSecretField
*******************************************************************************/
public String getVariantOptionsTableClientSecretField()
{
return (this.variantOptionsTableClientSecretField);
}
/*******************************************************************************
** Setter for variantOptionsTableClientSecretField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public void setVariantOptionsTableClientSecretField(String variantOptionsTableClientSecretField)
{
this.variantOptionsTableClientSecretField = variantOptionsTableClientSecretField;
this.getOrWithNewBackendVariantsConfig().withBackendSettingSourceFieldName(LegacyBackendVariantSetting.CLIENT_SECRET, variantOptionsTableClientSecretField);
}
@ -705,11 +642,55 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Fluent setter for variantOptionsTableClientSecretField
*******************************************************************************/
@Deprecated(since = "Replaced by backendVariantsConfig.backendSettingSourceFieldNameMap")
public QBackendMetaData withVariantOptionsTableClientSecretField(String variantOptionsTableClientSecretField)
{
this.variantOptionsTableClientSecretField = variantOptionsTableClientSecretField;
this.setVariantOptionsTableClientSecretField(variantOptionsTableClientSecretField);
return (this);
}
/*******************************************************************************
** Getter for backendVariantsConfig
*******************************************************************************/
public BackendVariantsConfig getBackendVariantsConfig()
{
return (this.backendVariantsConfig);
}
/*******************************************************************************
** Setter for backendVariantsConfig
*******************************************************************************/
public void setBackendVariantsConfig(BackendVariantsConfig backendVariantsConfig)
{
this.backendVariantsConfig = backendVariantsConfig;
}
/*******************************************************************************
** Fluent setter for backendVariantsConfig
*******************************************************************************/
public QBackendMetaData withBackendVariantsConfig(BackendVariantsConfig backendVariantsConfig)
{
this.backendVariantsConfig = backendVariantsConfig;
return (this);
}
/***************************************************************************
**
***************************************************************************/
private BackendVariantsConfig getOrWithNewBackendVariantsConfig()
{
if(backendVariantsConfig == null)
{
setBackendVariantsConfig(new BackendVariantsConfig());
}
return backendVariantsConfig;
}
}

View File

@ -56,6 +56,7 @@ 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;
@ -89,6 +90,7 @@ 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<>();
@ -1489,6 +1491,7 @@ public class QInstance
}
/*******************************************************************************
** Getter for metaDataFilter
*******************************************************************************/
@ -1519,4 +1522,68 @@ 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

@ -30,4 +30,5 @@ public enum AuditLevel
NONE,
RECORD,
FIELD
// idea: only audit changes to fields, e.g., on edit. though, is that a different dimension than this?
}

View File

@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
@ -69,6 +70,7 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
protected Map<String, Serializable> defaultValues = new LinkedHashMap<>();
protected QInstanceValidatorPluginInterface<QWidgetMetaDataInterface> validatorPlugin;
/*******************************************************************************
@ -764,4 +766,35 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
/*******************************************************************************
** Getter for validatorPlugin
*******************************************************************************/
public QInstanceValidatorPluginInterface<QWidgetMetaDataInterface> getValidatorPlugin()
{
return (this.validatorPlugin);
}
/*******************************************************************************
** Setter for validatorPlugin
*******************************************************************************/
public void setValidatorPlugin(QInstanceValidatorPluginInterface<QWidgetMetaDataInterface> validatorPlugin)
{
this.validatorPlugin = validatorPlugin;
}
/*******************************************************************************
** Fluent setter for validatorPlugin
*******************************************************************************/
public QWidgetMetaData withValidatorPlugin(QInstanceValidatorPluginInterface<QWidgetMetaDataInterface> validatorPlugin)
{
this.validatorPlugin = validatorPlugin;
return (this);
}
}

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
@ -277,5 +278,13 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules, T
qInstance.addWidget(this);
}
/***************************************************************************
** let the widget include an instance validator plugin
***************************************************************************/
default QInstanceValidatorPluginInterface<QWidgetMetaDataInterface> getValidatorPlugin()
{
return (null);
}
}

View File

@ -23,8 +23,12 @@ 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;
@ -41,20 +45,22 @@ public enum AdornmentType
RENDER_HTML,
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 TARGET = "target";
String TO_RECORD_FROM_TABLE = "toRecordFromTable";
String TO_RECORD_FROM_TABLE_DYNAMIC = "toRecordFromTableDynamic";
}
@ -71,6 +77,8 @@ 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" //
@ -78,6 +86,17 @@ 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"));
}
}
@ -167,4 +186,76 @@ public enum AdornmentType
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class FileUploadAdornment
{
public static String FORMAT = "format";
public static String WIDTH = "width";
/***************************************************************************
**
***************************************************************************/
public static FieldAdornment newFieldAdornment()
{
return (new FieldAdornment(AdornmentType.FILE_UPLOAD));
}
/***************************************************************************
**
***************************************************************************/
public static Pair<String, String> formatDragAndDrop()
{
return (Pair.of(FORMAT, "dragAndDrop"));
}
/***************************************************************************
**
***************************************************************************/
public static Pair<String, String> formatButton()
{
return (Pair.of(FORMAT, "button"));
}
/***************************************************************************
**
***************************************************************************/
public static Pair<String, String> widthFull()
{
return (Pair.of(WIDTH, "full"));
}
/***************************************************************************
**
***************************************************************************/
public static Pair<String, String> widthHalf()
{
return (Pair.of(WIDTH, "half"));
}
}
/*******************************************************************************
**
*******************************************************************************/
public interface TooltipValues
{
String STATIC_TEXT = "staticText";
String TOOLTIP_DYNAMIC = "tooltipDynamic";
}
}

View File

@ -177,7 +177,7 @@ public class FieldAdornment
** Fluent setter for values
**
*******************************************************************************/
public FieldAdornment withValue(Pair<String, Serializable> value)
public FieldAdornment withValue(Pair<String, ? extends Serializable> value)
{
return (withValue(value.getA(), value.getB()));
}

View File

@ -0,0 +1,86 @@
/*
* 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.fields;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Wrapper (record) that holds a QFieldMetaData and a QTableMetaData -
**
** With a factory method (`get()`) to go from the use-case of, a String that's
** "joinTable.fieldName" or "fieldName" to the pair.
**
** Note that the "joinTable" member here - could be the "mainTable" passed in
** to that `get()` method.
**
*******************************************************************************/
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/***************************************************************************
** given a table, and a field-name string (which should either be the name
** of a field on that table, or another tableName + "." + fieldName (from
** that table) - get back the pair of table & field metaData that the
** input string is talking about.
***************************************************************************/
public static FieldAndJoinTable get(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
String joinTableName = fieldName.replaceAll("\\..*", "");
String joinFieldName = fieldName.replaceAll(".*\\.", "");
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
if(joinTable == null)
{
throw (new QException("Unrecognized join table name: " + joinTableName));
}
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
}
else
{
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
}
}
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}

View File

@ -22,11 +22,45 @@
package com.kingsrook.qqq.backend.core.model.metadata.fields;
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.tables.QTableMetaData;
/*******************************************************************************
** Interface to mark a field behavior as one to be used during generating
** display values.
*******************************************************************************/
public interface FieldDisplayBehavior<T extends FieldDisplayBehavior<T>> extends FieldBehavior<T>
{
NoopFieldDisplayBehavior NOOP = new NoopFieldDisplayBehavior();
/***************************************************************************
**
***************************************************************************/
@Override
@SuppressWarnings("unchecked")
default T getDefault()
{
return (T) NOOP;
}
/***************************************************************************
** a default implementation for this behavior type, which does nothing.
***************************************************************************/
class NoopFieldDisplayBehavior implements FieldDisplayBehavior<NoopFieldDisplayBehavior>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.fields;
import java.util.List;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Display value formatter for fields which store a QQueryFilter as JSON.
*******************************************************************************/
public class FilterJsonFieldDisplayValueFormatter implements FieldDisplayBehavior<FilterJsonFieldDisplayValueFormatter>
{
private static Consumer<ObjectMapper> jsonMapperCustomizer = om -> om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
String queryFilterJson = record.getValueString(field.getName());
if(StringUtils.hasContent(queryFilterJson))
{
try
{
QQueryFilter qQueryFilter = JsonUtils.toObject(queryFilterJson, QQueryFilter.class, jsonMapperCustomizer);
int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size();
record.setDisplayValue(field.getName(), criteriaCount + " Filter" + StringUtils.plural(criteriaCount));
}
catch(Exception e)
{
record.setDisplayValue(field.getName(), "Invalid Filter...");
}
}
}
}
}

View File

@ -82,6 +82,7 @@ public class QFieldMetaData implements Cloneable, QMetaDataObject
private QQueryFilter possibleValueSourceFilter;
private QPossibleValueSource inlinePossibleValueSource;
private Integer gridColumns;
private Integer maxLength;
private Set<FieldBehavior<?>> behaviors;
@ -199,6 +200,7 @@ public class QFieldMetaData implements Cloneable, QMetaDataObject
setIsRequired(fieldAnnotation.isRequired());
setIsEditable(fieldAnnotation.isEditable());
setIsHidden(fieldAnnotation.isHidden());
setGridColumns(fieldAnnotation.gridColumns());
if(StringUtils.hasContent(fieldAnnotation.label()))
{
@ -1063,6 +1065,7 @@ public class QFieldMetaData implements Cloneable, QMetaDataObject
}
/*******************************************************************************
** Getter for inlinePossibleValueSource
*******************************************************************************/
@ -1093,4 +1096,34 @@ public class QFieldMetaData implements Cloneable, QMetaDataObject
}
/*******************************************************************************
** Getter for gridColumns
*******************************************************************************/
public Integer getGridColumns()
{
return (this.gridColumns);
}
/*******************************************************************************
** Setter for gridColumns
*******************************************************************************/
public void setGridColumns(Integer gridColumns)
{
this.gridColumns = gridColumns;
}
/*******************************************************************************
** Fluent setter for gridColumns
*******************************************************************************/
public QFieldMetaData withGridColumns(Integer gridColumns)
{
this.gridColumns = gridColumns;
return (this);
}
}

View File

@ -27,6 +27,7 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@ -100,6 +101,16 @@ public enum QFieldType
/***************************************************************************
**
***************************************************************************/
public String getMixedCaseLabel()
{
return StringUtils.allCapsToMixedCase(name());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,477 @@
/*
* 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.fields;
import java.math.BigDecimal;
import java.util.ArrayList;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
/*******************************************************************************
** Validate the min & max value for numeric fields.
**
** For each min & max, there are 4 possible settings:
** - value - the number that is compared.
** - allowEqualTo - defaults to true. controls if < (>) or ≤ (≥)
** - behavior - defaults to ERROR. optionally can be "CLIP" instead.
** - clipAmount - if clipping, and not allowing equalTo, how much off the limit
** value should be added or subtracted. Defaults to 1.
**
** Convenient `withMin()` and `withMax()` methods exist for setting all 4
** properties for each of min or max. Else, fluent-setters are recommended.
*******************************************************************************/
public class ValueRangeBehavior implements FieldBehavior<ValueRangeBehavior>
{
/***************************************************************************
**
***************************************************************************/
public enum Behavior
{
ERROR,
CLIP
}
private Number minValue;
private boolean minAllowEqualTo = true;
private Behavior minBehavior = Behavior.ERROR;
private BigDecimal minClipAmount = BigDecimal.ONE;
private Number maxValue;
private boolean maxAllowEqualTo = true;
private Behavior maxBehavior = Behavior.ERROR;
private BigDecimal maxClipAmount = BigDecimal.ONE;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ValueRangeBehavior()
{
}
/***************************************************************************
**
***************************************************************************/
@Override
public ValueRangeBehavior getDefault()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
BigDecimal minLimitBigDecimal = minValue == null ? null : new BigDecimal(minValue.toString());
String minLimitString = minValue == null ? null : minValue.toString();
BigDecimal maxLimitBigDecimal = maxValue == null ? null : new BigDecimal(maxValue.toString());
String maxLimitString = maxValue == null ? null : maxValue.toString();
for(QRecord record : recordList)
{
BigDecimal recordValue = record.getValueBigDecimal(field.getName());
if(recordValue != null)
{
if(minLimitBigDecimal != null)
{
int compare = recordValue.compareTo(minLimitBigDecimal);
if(compare < 0 || (compare == 0 && !minAllowEqualTo))
{
if(this.minBehavior == Behavior.ERROR)
{
String operator = minAllowEqualTo ? "" : "greater than ";
record.addError(new BadInputStatusMessage("The value for " + field.getLabel() + " is too small (minimum allowed value is " + operator + minLimitString + ")"));
}
else if(this.minBehavior == Behavior.CLIP)
{
if(minAllowEqualTo)
{
record.setValue(field.getName(), minLimitBigDecimal);
}
else
{
record.setValue(field.getName(), minLimitBigDecimal.add(minClipAmount));
}
}
}
}
if(maxLimitBigDecimal != null)
{
int compare = recordValue.compareTo(maxLimitBigDecimal);
if(compare > 0 || (compare == 0 && !maxAllowEqualTo))
{
if(this.maxBehavior == Behavior.ERROR)
{
String operator = maxAllowEqualTo ? "" : "less than ";
record.addError(new BadInputStatusMessage("The value for " + field.getLabel() + " is too large (maximum allowed value is " + operator + maxLimitString + ")"));
}
else if(this.maxBehavior == Behavior.CLIP)
{
if(maxAllowEqualTo)
{
record.setValue(field.getName(), maxLimitBigDecimal);
}
else
{
record.setValue(field.getName(), maxLimitBigDecimal.subtract(maxClipAmount));
}
}
}
}
}
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean allowMultipleBehaviorsOfThisType()
{
return (false);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<String> validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData)
{
List<String> errors = new ArrayList<>();
if(minValue == null && maxValue == null)
{
errors.add("Either minValue or maxValue (or both) must be set.");
}
if(minValue != null && maxValue != null && new BigDecimal(minValue.toString()).compareTo(new BigDecimal(maxValue.toString())) > 0)
{
errors.add("minValue must be >= maxValue.");
}
if(fieldMetaData != null && fieldMetaData.getType() != null && !fieldMetaData.getType().isNumeric())
{
errors.add("can only be applied to a numeric type field.");
}
return (errors);
}
/***************************************************************************
**
***************************************************************************/
public ValueRangeBehavior withMin(Number value, boolean allowEqualTo, Behavior behavior, BigDecimal clipAmount)
{
setMinValue(value);
setMinAllowEqualTo(allowEqualTo);
setMinBehavior(behavior);
setMinClipAmount(clipAmount);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public ValueRangeBehavior withMax(Number value, boolean allowEqualTo, Behavior behavior, BigDecimal clipAmount)
{
setMaxValue(value);
setMaxAllowEqualTo(allowEqualTo);
setMaxBehavior(behavior);
setMaxClipAmount(clipAmount);
return (this);
}
/*******************************************************************************
** Getter for minValue
*******************************************************************************/
public Number getMinValue()
{
return (this.minValue);
}
/*******************************************************************************
** Setter for minValue
*******************************************************************************/
public void setMinValue(Number minValue)
{
this.minValue = minValue;
}
/*******************************************************************************
** Fluent setter for minValue
*******************************************************************************/
public ValueRangeBehavior withMinValue(Number minValue)
{
this.minValue = minValue;
return (this);
}
/*******************************************************************************
** Getter for maxValue
*******************************************************************************/
public Number getMaxValue()
{
return (this.maxValue);
}
/*******************************************************************************
** Setter for maxValue
*******************************************************************************/
public void setMaxValue(Number maxValue)
{
this.maxValue = maxValue;
}
/*******************************************************************************
** Fluent setter for maxValue
*******************************************************************************/
public ValueRangeBehavior withMaxValue(Number maxValue)
{
this.maxValue = maxValue;
return (this);
}
/*******************************************************************************
** Getter for minAllowEqualTo
*******************************************************************************/
public boolean getMinAllowEqualTo()
{
return (this.minAllowEqualTo);
}
/*******************************************************************************
** Setter for minAllowEqualTo
*******************************************************************************/
public void setMinAllowEqualTo(boolean minAllowEqualTo)
{
this.minAllowEqualTo = minAllowEqualTo;
}
/*******************************************************************************
** Fluent setter for minAllowEqualTo
*******************************************************************************/
public ValueRangeBehavior withMinAllowEqualTo(boolean minAllowEqualTo)
{
this.minAllowEqualTo = minAllowEqualTo;
return (this);
}
/*******************************************************************************
** Getter for maxAllowEqualTo
*******************************************************************************/
public boolean getMaxAllowEqualTo()
{
return (this.maxAllowEqualTo);
}
/*******************************************************************************
** Setter for maxAllowEqualTo
*******************************************************************************/
public void setMaxAllowEqualTo(boolean maxAllowEqualTo)
{
this.maxAllowEqualTo = maxAllowEqualTo;
}
/*******************************************************************************
** Fluent setter for maxAllowEqualTo
*******************************************************************************/
public ValueRangeBehavior withMaxAllowEqualTo(boolean maxAllowEqualTo)
{
this.maxAllowEqualTo = maxAllowEqualTo;
return (this);
}
/*******************************************************************************
** Getter for minBehavior
*******************************************************************************/
public Behavior getMinBehavior()
{
return (this.minBehavior);
}
/*******************************************************************************
** Setter for minBehavior
*******************************************************************************/
public void setMinBehavior(Behavior minBehavior)
{
this.minBehavior = minBehavior;
}
/*******************************************************************************
** Fluent setter for minBehavior
*******************************************************************************/
public ValueRangeBehavior withMinBehavior(Behavior minBehavior)
{
this.minBehavior = minBehavior;
return (this);
}
/*******************************************************************************
** Getter for maxBehavior
*******************************************************************************/
public Behavior getMaxBehavior()
{
return (this.maxBehavior);
}
/*******************************************************************************
** Setter for maxBehavior
*******************************************************************************/
public void setMaxBehavior(Behavior maxBehavior)
{
this.maxBehavior = maxBehavior;
}
/*******************************************************************************
** Fluent setter for maxBehavior
*******************************************************************************/
public ValueRangeBehavior withMaxBehavior(Behavior maxBehavior)
{
this.maxBehavior = maxBehavior;
return (this);
}
/*******************************************************************************
** Getter for minClipAmount
*******************************************************************************/
public BigDecimal getMinClipAmount()
{
return (this.minClipAmount);
}
/*******************************************************************************
** Setter for minClipAmount
*******************************************************************************/
public void setMinClipAmount(BigDecimal minClipAmount)
{
this.minClipAmount = minClipAmount;
}
/*******************************************************************************
** Fluent setter for minClipAmount
*******************************************************************************/
public ValueRangeBehavior withMinClipAmount(BigDecimal minClipAmount)
{
this.minClipAmount = minClipAmount;
return (this);
}
/*******************************************************************************
** Getter for maxClipAmount
*******************************************************************************/
public BigDecimal getMaxClipAmount()
{
return (this.maxClipAmount);
}
/*******************************************************************************
** Setter for maxClipAmount
*******************************************************************************/
public void setMaxClipAmount(BigDecimal maxClipAmount)
{
this.maxClipAmount = maxClipAmount;
}
/*******************************************************************************
** Fluent setter for maxClipAmount
*******************************************************************************/
public ValueRangeBehavior withMaxClipAmount(BigDecimal maxClipAmount)
{
this.maxClipAmount = maxClipAmount;
return (this);
}
}

View File

@ -43,7 +43,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*
*******************************************************************************/
@JsonInclude(Include.NON_NULL)
public class QFrontendFieldMetaData
public class QFrontendFieldMetaData implements Serializable
{
private String name;
private String label;
@ -51,6 +51,7 @@ public class QFrontendFieldMetaData
private boolean isRequired;
private boolean isEditable;
private boolean isHeavy;
private Integer gridColumns;
private String possibleValueSourceName;
private String displayFormat;
private Serializable defaultValue;
@ -66,7 +67,6 @@ public class QFrontendFieldMetaData
//////////////////////////////////////////////////////////////////////////////////
/*******************************************************************************
** Constructor
*******************************************************************************/
@ -78,6 +78,7 @@ public class QFrontendFieldMetaData
this.isRequired = fieldMetaData.getIsRequired();
this.isEditable = fieldMetaData.getIsEditable();
this.isHeavy = fieldMetaData.getIsHeavy();
this.gridColumns = fieldMetaData.getGridColumns();
this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName();
this.displayFormat = fieldMetaData.getDisplayFormat();
this.adornments = fieldMetaData.getAdornments();
@ -166,6 +167,17 @@ public class QFrontendFieldMetaData
/*******************************************************************************
** Getter for gridColumns
**
*******************************************************************************/
public Integer getGridColumns()
{
return gridColumns;
}
/*******************************************************************************
** Getter for displayFormat
**

View File

@ -86,7 +86,6 @@ public class QFrontendTableMetaData
//////////////////////////////////////////////////////////////////////////////////
/*******************************************************************************
**
*******************************************************************************/
@ -170,7 +169,7 @@ public class QFrontendTableMetaData
if(backend != null && backend.getUsesVariants())
{
usesVariants = true;
variantTableLabel = QContext.getQInstance().getTable(backend.getVariantOptionsTableName()).getLabel();
variantTableLabel = QContext.getQInstance().getTable(backend.getBackendVariantsConfig().getOptionsTableName()).getLabel();
}
this.helpContents = tableMetaData.getHelpContent();

View File

@ -216,11 +216,16 @@ public class SendSESAction
{
LOG.warn("More than one FROM value was found, will send using the first one found [" + partyList.get(0).getAddress() + "].");
}
Party fromParty = partyList.get(0);
if(fromParty.getAddress() == null)
{
throw (new QException("Cannot send SES message because a FROM address was not provided."));
}
/////////////////////////////
// return the from address //
/////////////////////////////
return (partyList.get(0).getAddress());
return (getFullEmailAddress(fromParty));
}
@ -267,15 +272,15 @@ public class SendSESAction
{
if(EmailPartyRole.CC.equals(party.getRole()))
{
ccList.add(party.getAddress());
ccList.add(getFullEmailAddress(party));
}
else if(EmailPartyRole.BCC.equals(party.getRole()))
{
bccList.add(party.getAddress());
bccList.add(getFullEmailAddress(party));
}
else if(party.getRole() == null || PartyRole.Default.DEFAULT.equals(party.getRole()) || EmailPartyRole.TO.equals(party.getRole()))
{
toList.add(party.getAddress());
toList.add(getFullEmailAddress(party));
}
else
{
@ -332,4 +337,22 @@ public class SendSESAction
return amazonSES;
}
/*******************************************************************************
**
*******************************************************************************/
private String getFullEmailAddress(Party party)
{
if(party.getLabel() != null)
{
return (party.getLabel() + " <" + party.getAddress() + ">");
}
/////////////////////////////
// return the from address //
/////////////////////////////
return (party.getAddress());
}
}

View File

@ -22,11 +22,14 @@
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.io.Serializable;
/*******************************************************************************
** Interface to be implemented by enums which can be used as a PossibleValueSource.
**
*******************************************************************************/
public interface PossibleValueEnum<T>
public interface PossibleValueEnum<T extends Serializable>
{
/*******************************************************************************
**

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.io.Serializable;
import java.util.Objects;
@ -30,7 +31,7 @@ import java.util.Objects;
**
** Type parameter `T` is the type of the id (often Integer, maybe String)
*******************************************************************************/
public class QPossibleValue<T>
public class QPossibleValue<T extends Serializable>
{
private final T id;
private final String label;

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -31,6 +32,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import net.sf.saxon.trans.SaxonErrorCode;
/*******************************************************************************
@ -45,6 +48,8 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
private String label;
private QPossibleValueSourceType type;
private QFieldType idType;
private String valueFormat = PVSValueFormatAndFields.LABEL_ONLY.getFormat();
private List<String> valueFields = PVSValueFormatAndFields.LABEL_ONLY.getFields();
private String valueFormatIfNotFound = null;
@ -100,7 +105,7 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
** Create a new possible value source, for an enum, with default settings.
** e.g., type=ENUM; name from param values from the param; LABEL_ONLY format
*******************************************************************************/
public static <I, T extends PossibleValueEnum<I>> QPossibleValueSource newForEnum(String name, T[] values)
public static <I extends Serializable, T extends PossibleValueEnum<I>> QPossibleValueSource newForEnum(String name, T[] values)
{
return new QPossibleValueSource()
.withName(name)
@ -556,7 +561,7 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
** myPossibleValueSource.withValuesFromEnum(MyEnum.values()));
**
*******************************************************************************/
public <I, T extends PossibleValueEnum<I>> QPossibleValueSource withValuesFromEnum(T[] values)
public <I extends Serializable, T extends PossibleValueEnum<I>> QPossibleValueSource withValuesFromEnum(T[] values)
{
Set<I> usedIds = new HashSet<>();
List<I> duplicatedIds = new ArrayList<>();
@ -679,4 +684,35 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
return (this);
}
/*******************************************************************************
** Getter for idType
*******************************************************************************/
public QFieldType getIdType()
{
return (this.idType);
}
/*******************************************************************************
** Setter for idType
*******************************************************************************/
public void setIdType(QFieldType idType)
{
this.idType = idType;
}
/*******************************************************************************
** Fluent setter for idType
*******************************************************************************/
public QPossibleValueSource withIdType(QFieldType idType)
{
this.idType = idType;
return (this);
}
}

View File

@ -29,6 +29,9 @@ public enum QComponentType
{
HELP_TEXT,
BULK_EDIT_FORM,
BULK_LOAD_FILE_MAPPING_FORM,
BULK_LOAD_VALUE_MAPPING_FORM,
BULK_LOAD_PROFILE_FORM,
VALIDATION_REVIEW_SCREEN,
EDIT_FORM,
VIEW_FORM,

View File

@ -48,6 +48,7 @@ public class QFrontendStepMetaData extends QStepMetaData
private Map<String, QFieldMetaData> formFieldMap;
private String format;
private String backStepName;
private List<QHelpContent> helpContents;
@ -436,4 +437,35 @@ public class QFrontendStepMetaData extends QStepMetaData
}
/*******************************************************************************
** Getter for backStepName
*******************************************************************************/
public String getBackStepName()
{
return (this.backStepName);
}
/*******************************************************************************
** Setter for backStepName
*******************************************************************************/
public void setBackStepName(String backStepName)
{
this.backStepName = backStepName;
}
/*******************************************************************************
** Fluent setter for backStepName
*******************************************************************************/
public QFrontendStepMetaData withBackStepName(String backStepName)
{
this.backStepName = backStepName;
return (this);
}
}

View File

@ -31,11 +31,13 @@ import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.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;
@ -45,11 +47,14 @@ 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
public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface, SourceQBitAware
{
private String name;
private String label;
private String tableName;
private String name;
private String label;
private String tableName;
private String sourceQBitName;
private boolean isHidden = false;
private BasepullConfiguration basepullConfiguration;
private QPermissionRules permissionRules;
@ -70,6 +75,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private VariantRunStrategy variantRunStrategy;
private String variantBackend;
private QCodeReference processTracerCodeReference;
private Map<String, QSupplementalProcessMetaData> supplementalMetaData;
@ -877,4 +884,69 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return (this);
}
/*******************************************************************************
** Getter for processTracerCodeReference
*******************************************************************************/
public QCodeReference getProcessTracerCodeReference()
{
return (this.processTracerCodeReference);
}
/*******************************************************************************
** Setter for processTracerCodeReference
*******************************************************************************/
public void setProcessTracerCodeReference(QCodeReference processTracerCodeReference)
{
this.processTracerCodeReference = processTracerCodeReference;
}
/*******************************************************************************
** Fluent setter for processTracerCodeReference
*******************************************************************************/
public QProcessMetaData withProcessTracerCodeReference(QCodeReference processTracerCodeReference)
{
this.processTracerCodeReference = processTracerCodeReference;
return (this);
}
/*******************************************************************************
** 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,6 +62,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
private String parentTableName; // e.g., order
private String foreignKeyFieldName; // e.g., orderId
private Class<?> sourceClass;
/***************************************************************************
@ -102,4 +103,37 @@ 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

@ -38,14 +38,14 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** produce a QJoinMetaData, based on a QRecordEntity and a ChildTable sub-annotation.
**
** e.g., Orders & LineItems - on the Order entity
** <code>
<code>
@QMetaDataProducingEntity( childTables = { @ChildTable(
childTableEntityClass = LineItem.class,
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines"))
childTableEntityClass = LineItem.class,
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines"))
})
public class Order extends QRecordEntity
** </code>
</code>
**
*******************************************************************************/
public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implements MetaDataProducerInterface<QWidgetMetaData>
@ -53,18 +53,29 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
private String childTableName; // e.g., lineItem
private String parentTableName; // e.g., order
private MetaDataCustomizerInterface<QWidgetMetaData> widgetMetaDataProductionCustomizer = null;
private ChildRecordListWidget childRecordListWidget;
private Class<?> sourceClass;
/***************************************************************************
**
***************************************************************************/
public ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, ChildRecordListWidget childRecordListWidget)
public ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, ChildRecordListWidget childRecordListWidget) throws Exception
{
this.childTableName = childTableName;
this.parentTableName = parentTableName;
this.childRecordListWidget = childRecordListWidget;
Class<? extends MetaDataCustomizerInterface<?>> genericMetaProductionCustomizer = (Class<? extends MetaDataCustomizerInterface<?>>) childRecordListWidget.widgetMetaDataCustomizer();
if(!genericMetaProductionCustomizer.equals(MetaDataCustomizerInterface.NoopMetaDataCustomizer.class))
{
Class<? extends MetaDataCustomizerInterface<QWidgetMetaData>> widgetMetaProductionCustomizerClass = (Class<? extends MetaDataCustomizerInterface<QWidgetMetaData>>) genericMetaProductionCustomizer;
this.widgetMetaDataProductionCustomizer = widgetMetaProductionCustomizerClass.getConstructor().newInstance();
}
}
@ -94,7 +105,44 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
widget.withDefaultValue("maxRows", childRecordListWidget.maxRows());
}
if(this.widgetMetaDataProductionCustomizer != null)
{
widget = this.widgetMetaDataProductionCustomizer.customizeMetaData(qInstance, widget);
}
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

@ -0,0 +1,58 @@
/*
* 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.producers;
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.TopLevelMetaDataInterface;
/*******************************************************************************
** Interface to be implemented by classes that are designed to help customize
** meta-data objects as they're being produced, e.g., such as a table produced
** via the QMetaDataProducingEntity, or maybe tables loaded by a qbit??
*******************************************************************************/
public interface MetaDataCustomizerInterface<T extends TopLevelMetaDataInterface>
{
/***************************************************************************
**
***************************************************************************/
T customizeMetaData(QInstance qInstance, T metaData) throws QException;
/***************************************************************************
** noop version of this interface - used as default value in annotation
**
***************************************************************************/
class NoopMetaDataCustomizer<T extends TopLevelMetaDataInterface> implements MetaDataCustomizerInterface<T>
{
/***************************************************************************
**
***************************************************************************/
@Override
public T customizeMetaData(QInstance qInstance, T metaData) throws QException
{
return (metaData);
}
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import java.io.Serializable;
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.possiblevalues.PossibleValueEnum;
@ -34,11 +35,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
** based on a PossibleValueEnum
**
***************************************************************************/
public class PossibleValueSourceOfEnumGenericMetaDataProducer<T extends PossibleValueEnum<T>> implements MetaDataProducerInterface<QPossibleValueSource>
public class PossibleValueSourceOfEnumGenericMetaDataProducer<T extends Serializable & PossibleValueEnum<T>> implements MetaDataProducerInterface<QPossibleValueSource>
{
private final String name;
private final PossibleValueEnum<T>[] values;
private Class<?> sourceClass;
/*******************************************************************************
@ -61,4 +66,37 @@ public class PossibleValueSourceOfEnumGenericMetaDataProducer<T extends Possible
{
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,6 +37,7 @@ public class PossibleValueSourceOfTableGenericMetaDataProducer implements MetaDa
{
private final String tableName;
private Class<?> sourceClass;
/*******************************************************************************
@ -58,4 +59,38 @@ 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

@ -0,0 +1,191 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/***************************************************************************
** Generic meta-data-producer, which should be instantiated (e.g., by
** MetaDataProducerHelper), to produce a QPossibleValueSource meta-data
** based on a QRecordEntity class (which has corresponding QTableMetaData).
**
***************************************************************************/
public class RecordEntityToTableGenericMetaDataProducer implements MetaDataProducerInterface<QTableMetaData>
{
private final String tableName;
private final Class<? extends QRecordEntity> entityClass;
private final List<MetaDataCustomizerInterface<QTableMetaData>> metaDataCustomizers = new ArrayList<>();
private static MetaDataCustomizerInterface<QTableMetaData> defaultMetaDataCustomizer = null;
private Class<?> sourceClass;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RecordEntityToTableGenericMetaDataProducer(String tableName, Class<? extends QRecordEntity> entityClass, Class<? extends MetaDataCustomizerInterface<QTableMetaData>> metaDataProductionCustomizerClass) throws QException
{
this.tableName = tableName;
this.entityClass = entityClass;
if(metaDataProductionCustomizerClass != null)
{
metaDataCustomizers.add(getMetaDataProductionCustomizer(metaDataProductionCustomizerClass));
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public QTableMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData qTableMetaData = new QTableMetaData();
qTableMetaData.setName(tableName);
qTableMetaData.setRecordLabelFormat("%s");
qTableMetaData.withFieldsFromEntity(entityClass);
////////////////////////////////////////////////////////////////////
// use the productionCustomizers to fill in more of the meta data //
////////////////////////////////////////////////////////////////////
for(MetaDataCustomizerInterface<QTableMetaData> metaDataMetaDataCustomizer : metaDataCustomizers)
{
qTableMetaData = metaDataMetaDataCustomizer.customizeMetaData(qInstance, qTableMetaData);
}
///////////////////////////////////////////////////////////////////////////////////
// now if there's a default customizer, call it too - for generic, common things //
// you might want on all of your tables, or defaults if not set otherwise //
///////////////////////////////////////////////////////////////////////////////////
if(defaultMetaDataCustomizer != null)
{
qTableMetaData = defaultMetaDataCustomizer.customizeMetaData(qInstance, qTableMetaData);
}
/////////////////////////////////////////////////////////////////////////
// use primary key as record label field, if it hasn't been set so far //
// todo - does this belong in the enricher?? //
/////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(qTableMetaData.getRecordLabelFields()) && StringUtils.hasContent(qTableMetaData.getPrimaryKeyField()))
{
qTableMetaData.setRecordLabelFields(List.of(qTableMetaData.getPrimaryKeyField()));
}
return qTableMetaData;
}
/***************************************************************************
**
***************************************************************************/
private MetaDataCustomizerInterface<QTableMetaData> getMetaDataProductionCustomizer(Class<? extends MetaDataCustomizerInterface<QTableMetaData>> metaDataCustomizerClass) throws QException
{
try
{
return metaDataCustomizerClass.getConstructor().newInstance();
}
catch(Exception e)
{
throw (new QException("Error constructing table metadata production customizer class [" + metaDataCustomizerClass + "]: ", e));
}
}
/***************************************************************************
**
***************************************************************************/
public void addRecordEntityTableMetaDataProductionCustomizer(MetaDataCustomizerInterface<QTableMetaData> metaDataMetaDataCustomizer)
{
metaDataCustomizers.add(metaDataMetaDataCustomizer);
}
/*******************************************************************************
** Getter for defaultMetaDataCustomizer
*******************************************************************************/
public static MetaDataCustomizerInterface<QTableMetaData> getDefaultMetaDataCustomizer()
{
return (RecordEntityToTableGenericMetaDataProducer.defaultMetaDataCustomizer);
}
/*******************************************************************************
** Setter for defaultMetaDataCustomizer
*******************************************************************************/
public static void setDefaultMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> defaultMetaDataCustomizer)
{
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

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
/***************************************************************************
@ -43,4 +44,6 @@ public @interface ChildRecordListWidget
boolean canAddChildRecords() default false;
String manageAssociationName() default "";
Class<? extends MetaDataCustomizerInterface> widgetMetaDataCustomizer() default MetaDataCustomizerInterface.NoopMetaDataCustomizer.class;
}

View File

@ -26,6 +26,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
/*******************************************************************************
@ -41,8 +42,10 @@ import java.lang.annotation.Target;
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public @interface QMetaDataProducingEntity
{
boolean producePossibleValueSource() default true;
boolean produceTableMetaData() default false;
Class<? extends MetaDataCustomizerInterface> tableMetaDataCustomizer() default MetaDataCustomizerInterface.NoopMetaDataCustomizer.class;
boolean producePossibleValueSource() default false;
ChildTable[] childTables() default { };
}

View File

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

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

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

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

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

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

@ -0,0 +1,77 @@
/*
* 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,10 +50,12 @@ 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;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -61,25 +63,19 @@ 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
public class QTableMetaData implements QAppChildMetaData, Serializable, MetaDataWithPermissionRules, TopLevelMetaDataInterface, SourceQBitAware
{
private static final QLogger LOG = QLogger.getLogger(QTableMetaData.class);
private String name;
private String label;
// TODO: resolve confusion over:
// Is this name of what backend the table is stored in (yes)
// Or the "name" of the table WITHIN the backend (no)
// although that's how "backendName" is used in QFieldMetaData.
// Idea:
// rename "backendName" here to "backend"
// add "nameInBackend" (or similar) for the table name in the backend
// OR - add a whole "backendDetails" object, with different details per backend-type
private String backendName;
private String primaryKeyField;
private boolean isHidden = false;
private String sourceQBitName;
private Map<String, QFieldMetaData> fields;
private List<UniqueKey> uniqueKeys;
private List<Association> associations;
@ -184,6 +180,12 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// stash a reference from this entityClass to this table in the QRecordEntity class //
// (used within that class later, if it wants to know about a table that an Entity helped build) //
///////////////////////////////////////////////////////////////////////////////////////////////////
QRecordEntity.registerTable(entityClass, this);
return (this);
}
@ -714,6 +716,25 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
/*******************************************************************************
** Getter for sections
**
*******************************************************************************/
public QFieldSection getSection(String name)
{
for(QFieldSection qFieldSection : CollectionUtils.nonNullList(sections))
{
if(qFieldSection.getName().equals(name))
{
return (qFieldSection);
}
}
return (null);
}
/*******************************************************************************
** Setter for sections
**
@ -1038,7 +1059,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
{
for(Capability disabledCapability : disabledCapabilities)
{
withCapability(disabledCapability);
withoutCapability(disabledCapability);
}
return (this);
}
@ -1536,4 +1557,38 @@ 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

@ -0,0 +1,221 @@
/*
* 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.model.metadata.layout.QIcon;
/*******************************************************************************
** Factory class for creating "standard" qfield sections. e.g., if you want
** the same t1, t2, and t3 section on all your tables, use this class to
** produce them.
**
** You can change the default name & iconNames for those sections, but note,
** this is a static/utility style class, so those settings are static fields.
**
** The method customT2 is provided as not much of a shortcut over "doing it yourself",
** but to allow all sections for a table to be produced through calls to this factory,
** so they look more similar.
*******************************************************************************/
public class SectionFactory
{
private static String defaultT1name = "identity";
private static String defaultT1iconName = "badge";
private static String defaultT2name = "data";
private static String defaultT2iconName = "text_snippet";
private static String defaultT3name = "dates";
private static String defaultT3iconName = "calendar_month";
/*******************************************************************************
** private constructor, to enforce static usage, e.g., to make clear the fields
** are static fields.
**
*******************************************************************************/
private SectionFactory()
{
}
/***************************************************************************
**
***************************************************************************/
public static QFieldSection defaultT1(String... fieldNames)
{
return new QFieldSection(defaultT1name, new QIcon().withName(defaultT1iconName), Tier.T1, List.of(fieldNames));
}
/***************************************************************************
**
***************************************************************************/
public static QFieldSection defaultT2(String... fieldNames)
{
return new QFieldSection(defaultT2name, new QIcon().withName(defaultT2iconName), Tier.T2, List.of(fieldNames));
}
/***************************************************************************
**
***************************************************************************/
public static QFieldSection customT2(String name, QIcon icon, String... fieldNames)
{
return new QFieldSection(name, icon, Tier.T2, List.of(fieldNames));
}
/***************************************************************************
**
***************************************************************************/
public static QFieldSection defaultT3(String... fieldNames)
{
return new QFieldSection(defaultT3name, new QIcon().withName(defaultT3iconName), Tier.T3, List.of(fieldNames));
}
/*******************************************************************************
** Getter for defaultT1name
*******************************************************************************/
public static String getDefaultT1name()
{
return (SectionFactory.defaultT1name);
}
/*******************************************************************************
** Setter for defaultT1name
*******************************************************************************/
public static void setDefaultT1name(String defaultT1name)
{
SectionFactory.defaultT1name = defaultT1name;
}
/*******************************************************************************
** Getter for defaultT1iconName
*******************************************************************************/
public static String getDefaultT1iconName()
{
return (SectionFactory.defaultT1iconName);
}
/*******************************************************************************
** Setter for defaultT1iconName
*******************************************************************************/
public static void setDefaultT1iconName(String defaultT1iconName)
{
SectionFactory.defaultT1iconName = defaultT1iconName;
}
/*******************************************************************************
** Getter for defaultT2name
*******************************************************************************/
public static String getDefaultT2name()
{
return (SectionFactory.defaultT2name);
}
/*******************************************************************************
** Setter for defaultT2name
*******************************************************************************/
public static void setDefaultT2name(String defaultT2name)
{
SectionFactory.defaultT2name = defaultT2name;
}
/*******************************************************************************
** Getter for defaultT2iconName
*******************************************************************************/
public static String getDefaultT2iconName()
{
return (SectionFactory.defaultT2iconName);
}
/*******************************************************************************
** Setter for defaultT2iconName
*******************************************************************************/
public static void setDefaultT2iconName(String defaultT2iconName)
{
SectionFactory.defaultT2iconName = defaultT2iconName;
}
/*******************************************************************************
** Getter for defaultT3name
*******************************************************************************/
public static String getDefaultT3name()
{
return (SectionFactory.defaultT3name);
}
/*******************************************************************************
** Setter for defaultT3name
*******************************************************************************/
public static void setDefaultT3name(String defaultT3name)
{
SectionFactory.defaultT3name = defaultT3name;
}
/*******************************************************************************
** Getter for defaultT3iconName
*******************************************************************************/
public static String getDefaultT3iconName()
{
return (SectionFactory.defaultT3iconName);
}
/*******************************************************************************
** Setter for defaultT3iconName
*******************************************************************************/
public static void setDefaultT3iconName(String defaultT3iconName)
{
SectionFactory.defaultT3iconName = defaultT3iconName;
}
}

View File

@ -0,0 +1,89 @@
/*
* 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,17 +22,11 @@
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;
/*******************************************************************************
@ -51,22 +45,10 @@ public class TablesPossibleValueSourceMetaDataProvider
{
QPossibleValueSource possibleValueSource = new QPossibleValueSource()
.withName(NAME)
.withType(QPossibleValueSourceType.ENUM)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(TablesCustomPossibleValueProvider.class))
.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

@ -0,0 +1,31 @@
/*
* 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.variants;
/*******************************************************************************
** interface to be implemented by enums (presumably) that define the possible
** settings a particular backend type can get from a variant record.
*******************************************************************************/
public interface BackendVariantSetting
{
}

View File

@ -0,0 +1,225 @@
/*
* 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.variants;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Configs for how a backend that uses variants works. Specifically:
**
** - the variant "type key" - e.g., key for variants map in session.
** - what table supplies the variant options (optionsTableName
** - an optional filter to apply to that options table
** - a map of the settings that a backend gets from its variant table to the
** field names in that table that they come from. e.g., a backend may have a
** username attribute, whose value comes from a field named "theUser" in the
** variant options table.
** - an optional code reference to a variantRecordLookupFunction - to customize
** how the variant record is looked up (such as, adding joined or other custom
** fields).
*******************************************************************************/
public class BackendVariantsConfig
{
private String variantTypeKey;
private String optionsTableName;
private QQueryFilter optionsFilter;
private QCodeReference variantRecordLookupFunction;
private Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap;
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getOptionsTableName()
{
return (this.optionsTableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setOptionsTableName(String optionsTableName)
{
this.optionsTableName = optionsTableName;
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getOptionsFilter()
{
return (this.optionsFilter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setOptionsFilter(QQueryFilter optionsFilter)
{
this.optionsFilter = optionsFilter;
}
/*******************************************************************************
** Getter for backendSettingSourceFieldNameMap
*******************************************************************************/
public Map<BackendVariantSetting, String> getBackendSettingSourceFieldNameMap()
{
return (this.backendSettingSourceFieldNameMap);
}
/*******************************************************************************
** Setter for backendSettingSourceFieldNameMap
*******************************************************************************/
public void setBackendSettingSourceFieldNameMap(Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap)
{
this.backendSettingSourceFieldNameMap = backendSettingSourceFieldNameMap;
}
/*******************************************************************************
** Fluent setter for backendSettingSourceFieldNameMap
*******************************************************************************/
public BackendVariantsConfig withBackendSettingSourceFieldName(BackendVariantSetting backendVariantSetting, String sourceFieldName)
{
if(this.backendSettingSourceFieldNameMap == null)
{
this.backendSettingSourceFieldNameMap = new HashMap<>();
}
this.backendSettingSourceFieldNameMap.put(backendVariantSetting, sourceFieldName);
return (this);
}
/*******************************************************************************
** Fluent setter for backendSettingSourceFieldNameMap
*******************************************************************************/
public BackendVariantsConfig withBackendSettingSourceFieldNameMap(Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap)
{
this.backendSettingSourceFieldNameMap = backendSettingSourceFieldNameMap;
return (this);
}
/*******************************************************************************
** Getter for variantTypeKey
*******************************************************************************/
public String getVariantTypeKey()
{
return (this.variantTypeKey);
}
/*******************************************************************************
** Setter for variantTypeKey
*******************************************************************************/
public void setVariantTypeKey(String variantTypeKey)
{
this.variantTypeKey = variantTypeKey;
}
/*******************************************************************************
** Fluent setter for variantTypeKey
*******************************************************************************/
public BackendVariantsConfig withVariantTypeKey(String variantTypeKey)
{
this.variantTypeKey = variantTypeKey;
return (this);
}
/*******************************************************************************
** Fluent setter for optionsTableName
*******************************************************************************/
public BackendVariantsConfig withOptionsTableName(String optionsTableName)
{
this.optionsTableName = optionsTableName;
return (this);
}
/*******************************************************************************
** Fluent setter for optionsFilter
*******************************************************************************/
public BackendVariantsConfig withOptionsFilter(QQueryFilter optionsFilter)
{
this.optionsFilter = optionsFilter;
return (this);
}
/*******************************************************************************
** Getter for variantRecordLookupFunction
*******************************************************************************/
public QCodeReference getVariantRecordLookupFunction()
{
return (this.variantRecordLookupFunction);
}
/*******************************************************************************
** Setter for variantRecordLookupFunction
*******************************************************************************/
public void setVariantRecordLookupFunction(QCodeReference variantRecordLookupFunction)
{
this.variantRecordLookupFunction = variantRecordLookupFunction;
}
/*******************************************************************************
** Fluent setter for variantRecordLookupFunction
*******************************************************************************/
public BackendVariantsConfig withVariantRecordLookupFunction(QCodeReference variantRecordLookupFunction)
{
this.variantRecordLookupFunction = variantRecordLookupFunction;
return (this);
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.variants;
import java.io.Serializable;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
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.session.QSession;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
/*******************************************************************************
** Utility methods for backends working with Variants.
*******************************************************************************/
public class BackendVariantsUtil
{
/*******************************************************************************
** Get the variant id from the session for the backend.
*******************************************************************************/
public static Serializable getVariantId(QBackendMetaData backendMetaData) throws QException
{
QSession session = QContext.getQSession();
String variantTypeKey = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(variantTypeKey))
{
throw (new QException("Could not find Backend Variant information in session under key '" + variantTypeKey + "' for Backend '" + backendMetaData.getName() + "'"));
}
Serializable variantId = session.getBackendVariants().get(variantTypeKey);
return variantId;
}
/*******************************************************************************
** For backends that use variants, look up the variant record (in theory, based
** on an id in the session's backend variants map, then fetched from the backend's
** variant options table.
*******************************************************************************/
@SuppressWarnings("unchecked")
public static QRecord getVariantRecord(QBackendMetaData backendMetaData) throws QException
{
Serializable variantId = getVariantId(backendMetaData);
QRecord record;
if(backendMetaData.getBackendVariantsConfig().getVariantRecordLookupFunction() != null)
{
Object o = QCodeLoader.getAdHoc(Object.class, backendMetaData.getBackendVariantsConfig().getVariantRecordLookupFunction());
if(o instanceof UnsafeFunction<?,?,?> unsafeFunction)
{
record = ((UnsafeFunction<Serializable, QRecord, QException>) unsafeFunction).apply(variantId);
}
else if(o instanceof Function<?,?> function)
{
record = ((Function<Serializable, QRecord>) function).apply(variantId);
}
else
{
throw (new QException("Backend Variant's recordLookupFunction is not of any expected type (should have been caught by instance validation??)"));
}
}
else
{
GetInput getInput = new GetInput();
getInput.setShouldMaskPasswords(false);
getInput.setTableName(backendMetaData.getBackendVariantsConfig().getOptionsTableName());
getInput.setPrimaryKey(variantId);
GetOutput getOutput = new GetAction().execute(getInput);
record = getOutput.getRecord();
}
if(record == null)
{
throw (new QException("Could not find Backend Variant in table " + backendMetaData.getBackendVariantsConfig().getOptionsTableName() + " with id '" + variantId + "'"));
}
return record;
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.variants;
/*******************************************************************************
** temporary class, while we migrate from original way that variants were set up
** e.g., by calling 'variantOptionsTableUsernameField', to the new way, using
** the BackendVariantsConfig which uses a map of enum constants.
**
** so when those deprecated setters are removed, this enum can be too.
*****************************************************************************/
public enum LegacyBackendVariantSetting implements BackendVariantSetting
{
USERNAME,
PASSWORD,
API_KEY,
CLIENT_ID,
CLIENT_SECRET
}

View File

@ -0,0 +1,230 @@
/*
* 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.processes;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
** QRecord Entity for QQQProcess table - e.g., table that stores an id, name
** and the label for all processes in the QQQ application. Useful as a foreign
** key from other logging type tables.
*******************************************************************************/
public class QQQProcess extends QRecordEntity
{
public static final String TABLE_NAME = "qqqProcess";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String name;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String label;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public QQQProcess()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public QQQProcess(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public QQQProcess withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public QQQProcess withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public QQQProcess withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public QQQProcess withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for label
*******************************************************************************/
public String getLabel()
{
return (this.label);
}
/*******************************************************************************
** Setter for label
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
*******************************************************************************/
public QQQProcess withLabel(String label)
{
this.label = label;
return (this);
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
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.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Utility class for accessing QQQProcess records (well, just their ids at this time)
** Takes care of inserting upon a miss, and dealing with the cache table.
*******************************************************************************/
public class QQQProcessTableManager
{
private static final QLogger LOG = QLogger.getLogger(QQQProcessTableManager.class);
/*******************************************************************************
**
*******************************************************************************/
public static Integer getQQQProcessId(QInstance qInstance, String processName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQProcessesMetaDataProvider.QQQ_PROCESS_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", processName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
QProcessMetaData processMetaData = qInstance.getProcess(processName);
if(processMetaData == null)
{
LOG.info("No such process", logPair("processName", processName));
return (null);
}
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQProcess.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", processName).withValue("label", processMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord().getValueInteger("id");
}
}

View File

@ -0,0 +1,134 @@
/*
* 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.processes;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
/*******************************************************************************
** Provides meta data for the QQQProcess table, PVS, and a cache table.
*******************************************************************************/
public class QQQProcessesMetaDataProvider
{
public static final String QQQ_PROCESS_CACHE_TABLE_NAME = QQQProcess.TABLE_NAME + "Cache";
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String persistentBackendName, String cacheBackendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
instance.addTable(defineQQQProcess(persistentBackendName, backendDetailEnricher));
instance.addTable(defineQQQProcessCache(cacheBackendName, backendDetailEnricher));
instance.addPossibleValueSource(defineQQQProcessPossibleValueSource());
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineQQQProcess(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(QQQProcess.TABLE_NAME)
.withLabel("Process")
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withPrimaryKeyField("id")
.withUniqueKey(new UniqueKey("name"))
.withFieldsFromEntity(QQQProcess.class)
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineQQQProcessCache(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(QQQ_PROCESS_CACHE_TABLE_NAME)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withPrimaryKeyField("id")
.withUniqueKey(new UniqueKey("name"))
.withFieldsFromEntity(QQQProcess.class)
.withCacheOf(new CacheOf()
.withSourceTable(QQQProcess.TABLE_NAME)
.withUseCase(new CacheUseCase()
.withType(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY)
.withCacheSourceMisses(false)
.withCacheUniqueKey(new UniqueKey("name"))
.withSourceUniqueKey(new UniqueKey("name"))
.withDoCopySourcePrimaryKeyToCache(true)
)
);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource defineQQQProcessPossibleValueSource()
{
return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QQQProcess.TABLE_NAME)
.withTableName(QQQProcess.TABLE_NAME))
.withOrderByField("label");
}
}

View File

@ -0,0 +1,285 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
/*******************************************************************************
** Entity bean for the savedBulkLoadProfile table
*******************************************************************************/
public class SavedBulkLoadProfile extends QRecordEntity
{
public static final String TABLE_NAME = "savedBulkLoadProfile";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS, label = "Profile Name")
private String label;
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "Table", isRequired = true)
private String tableName;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID, label = "Owner")
private String userId;
@QField(label = "Mapping JSON")
private String mappingJson;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SavedBulkLoadProfile()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SavedBulkLoadProfile(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return id;
}
/*******************************************************************************
** Setter for id
**
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Getter for createDate
**
*******************************************************************************/
public Instant getCreateDate()
{
return createDate;
}
/*******************************************************************************
** Setter for createDate
**
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Getter for modifyDate
**
*******************************************************************************/
public Instant getModifyDate()
{
return modifyDate;
}
/*******************************************************************************
** Setter for modifyDate
**
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return label;
}
/*******************************************************************************
** Setter for label
**
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
**
*******************************************************************************/
public SavedBulkLoadProfile withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Setter for tableName
**
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
**
*******************************************************************************/
public SavedBulkLoadProfile withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for userId
**
*******************************************************************************/
public String getUserId()
{
return userId;
}
/*******************************************************************************
** Setter for userId
**
*******************************************************************************/
public void setUserId(String userId)
{
this.userId = userId;
}
/*******************************************************************************
** Fluent setter for userId
**
*******************************************************************************/
public SavedBulkLoadProfile withUserId(String userId)
{
this.userId = userId;
return (this);
}
/*******************************************************************************
** Getter for mappingJson
*******************************************************************************/
public String getMappingJson()
{
return (this.mappingJson);
}
/*******************************************************************************
** Setter for mappingJson
*******************************************************************************/
public void setMappingJson(String mappingJson)
{
this.mappingJson = mappingJson;
}
/*******************************************************************************
** Fluent setter for mappingJson
*******************************************************************************/
public SavedBulkLoadProfile withMappingJson(String mappingJson)
{
this.mappingJson = mappingJson;
return (this);
}
}

View File

@ -0,0 +1,141 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles;
import java.util.ArrayList;
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.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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
/*******************************************************************************
**
*******************************************************************************/
public class SavedBulkLoadProfileJsonFieldDisplayValueFormatter implements FieldDisplayBehavior<SavedBulkLoadProfileJsonFieldDisplayValueFormatter>
{
private static SavedBulkLoadProfileJsonFieldDisplayValueFormatter savedReportJsonFieldDisplayValueFormatter = null;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private SavedBulkLoadProfileJsonFieldDisplayValueFormatter()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static SavedBulkLoadProfileJsonFieldDisplayValueFormatter getInstance()
{
if(savedReportJsonFieldDisplayValueFormatter == null)
{
savedReportJsonFieldDisplayValueFormatter = new SavedBulkLoadProfileJsonFieldDisplayValueFormatter();
}
return (savedReportJsonFieldDisplayValueFormatter);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public SavedBulkLoadProfileJsonFieldDisplayValueFormatter getDefault()
{
return getInstance();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
if(field.getName().equals("mappingJson"))
{
String mappingJson = record.getValueString("mappingJson");
if(StringUtils.hasContent(mappingJson))
{
try
{
record.setDisplayValue("mappingJson", jsonToDisplayValue(mappingJson));
}
catch(Exception e)
{
record.setDisplayValue("mappingJson", "Invalid Mapping...");
}
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private String jsonToDisplayValue(String mappingJson)
{
JSONObject jsonObject = new JSONObject(mappingJson);
List<String> parts = new ArrayList<>();
if(jsonObject.has("fieldList"))
{
JSONArray fieldListArray = jsonObject.getJSONArray("fieldList");
parts.add(fieldListArray.length() + " field" + StringUtils.plural(fieldListArray.length()));
}
if(jsonObject.has("hasHeaderRow"))
{
boolean hasHeaderRow = jsonObject.getBoolean("hasHeaderRow");
parts.add((hasHeaderRow ? "With" : "Without") + " header row");
}
if(jsonObject.has("layout"))
{
String layout = jsonObject.getString("layout");
parts.add("Layout: " + StringUtils.allCapsToMixedCase(layout));
}
return StringUtils.join("; ", parts);
}
}

View File

@ -0,0 +1,168 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.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.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.processes.implementations.savedbulkloadprofiles.DeleteSavedBulkLoadProfileProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedbulkloadprofiles.QuerySavedBulkLoadProfileProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.savedbulkloadprofiles.StoreSavedBulkLoadProfileProcess;
/*******************************************************************************
**
*******************************************************************************/
public class SavedBulkLoadProfileMetaDataProvider
{
public static final String SHARED_SAVED_BULK_LOAD_PROFILE_JOIN_SAVED_BULK_LOAD_PROFILE = "sharedSavedBulkLoadProfileJoinSavedBulkLoadProfile";
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String recordTablesBackendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
instance.addTable(defineSavedBulkLoadProfileTable(recordTablesBackendName, backendDetailEnricher));
instance.addPossibleValueSource(QPossibleValueSource.newForTable(SavedBulkLoadProfile.TABLE_NAME));
/////////////////////////////////////
// todo - param to enable sharing? //
/////////////////////////////////////
instance.addTable(defineSharedSavedBulkLoadProfileTable(recordTablesBackendName, backendDetailEnricher));
instance.addJoin(defineSharedSavedBulkLoadProfileJoinSavedBulkLoadProfile());
if(instance.getPossibleValueSource(ShareScopePossibleValueMetaDataProducer.NAME) == null)
{
instance.addPossibleValueSource(new ShareScopePossibleValueMetaDataProducer().produce(new QInstance()));
}
////////////////////////////////////
// processes for working with 'em //
////////////////////////////////////
instance.add(StoreSavedBulkLoadProfileProcess.getProcessMetaData());
instance.add(QuerySavedBulkLoadProfileProcess.getProcessMetaData());
instance.add(DeleteSavedBulkLoadProfileProcess.getProcessMetaData());
}
/*******************************************************************************
**
*******************************************************************************/
private QJoinMetaData defineSharedSavedBulkLoadProfileJoinSavedBulkLoadProfile()
{
return (new QJoinMetaData()
.withName(SHARED_SAVED_BULK_LOAD_PROFILE_JOIN_SAVED_BULK_LOAD_PROFILE)
.withLeftTable(SharedSavedBulkLoadProfile.TABLE_NAME)
.withRightTable(SavedBulkLoadProfile.TABLE_NAME)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("savedBulkLoadProfileId", "id")));
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineSavedBulkLoadProfileTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(SavedBulkLoadProfile.TABLE_NAME)
.withLabel("Bulk Load Profile")
.withIcon(new QIcon().withName("drive_folder_upload"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withBackendName(backendName)
.withPrimaryKeyField("id")
.withFieldsFromEntity(SavedBulkLoadProfile.class)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
table.getField("mappingJson").withBehavior(SavedBulkLoadProfileJsonFieldDisplayValueFormatter.getInstance());
table.getField("mappingJson").setLabel("Mapping");
table.withShareableTableMetaData(new ShareableTableMetaData()
.withSharedRecordTableName(SharedSavedBulkLoadProfile.TABLE_NAME)
.withAssetIdFieldName("savedBulkLoadProfileId")
.withScopeFieldName("scope")
.withThisTableOwnerIdFieldName("userId")
.withAudienceType(new ShareableAudienceType().withName("user").withFieldName("userId")));
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineSharedSavedBulkLoadProfileTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(SharedSavedBulkLoadProfile.TABLE_NAME)
.withLabel("Shared Bulk Load Profile")
.withIcon(new QIcon().withName("share"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("savedBulkLoadProfileId")
.withBackendName(backendName)
.withUniqueKey(new UniqueKey("savedBulkLoadProfileId", "userId"))
.withPrimaryKeyField("id")
.withFieldsFromEntity(SharedSavedBulkLoadProfile.class)
// todo - security key
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "savedBulkLoadProfileId", "userId")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("scope")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
}

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