Adding table-cacheOf concept; ability to add a child record from child-list widget

This commit is contained in:
2022-12-05 10:24:15 -06:00
parent 3691ad87e5
commit 060da69afb
32 changed files with 1766 additions and 109 deletions

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -48,7 +49,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
protected String openTopLevelBulletList()
{
return ("""
<div style="padding: 1rem;">
<div style="padding-left: 2rem;">
<ul>""");
}
@ -101,7 +102,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
protected String bulletNameValue(String name, String value)
{
return ("<li><b>" + name + "</b> &nbsp; " + value + "</li>");
return ("<li><b>" + name + "</b> &nbsp; " + Objects.requireNonNullElse(value, "--") + "</li>");
}

View File

@ -22,9 +22,12 @@
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -42,11 +45,14 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListDat
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.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.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.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
@ -58,13 +64,64 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public static QWidgetMetaData defineWidgetFromJoin(QJoinMetaData join)
public static Builder widgetMetaDataBuilder(QJoinMetaData join)
{
return (new QWidgetMetaData()
return (new Builder(new QWidgetMetaData()
.withName(join.getName())
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withDefaultValue("joinName", join.getName()));
.withDefaultValue("joinName", join.getName())));
}
/*******************************************************************************
**
*******************************************************************************/
public static class Builder extends AbstractWidgetMetaDataBuilder
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Builder(QWidgetMetaData widgetMetaData)
{
super(widgetMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withName(String name)
{
widgetMetaData.setName(name);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withLabel(String label)
{
widgetMetaData.setLabel(label);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public AbstractWidgetMetaDataBuilder withCanAddChildRecord(boolean b)
{
widgetMetaData.withDefaultValue("canAddChildRecord", true);
return (this);
}
}
@ -119,7 +176,24 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
String tablePath = input.getInstance().getTablePath(input, table.getName());
String viewAllLink = tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset());
return (new RenderWidgetOutput(new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink)));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
widgetData.setCanAddChildRecord(true);
//////////////////////////////////////////////////////////
// new child records must have values from the join-ons //
//////////////////////////////////////////////////////////
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
for(JoinOn joinOn : join.getJoinOns())
{
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
}
return (new RenderWidgetOutput(widgetData));
}
}

View File

@ -22,9 +22,12 @@
package com.kingsrook.qqq.backend.core.actions.interfaces;
import java.util.HashSet;
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.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
@ -37,4 +40,34 @@ public interface GetInterface
**
*******************************************************************************/
GetOutput execute(GetInput getInput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default void validateInput(GetInput getInput) throws QException
{
if(getInput.getPrimaryKey() != null & getInput.getUniqueKey() != null)
{
throw new QException("A GetInput may not contain both a primary key [" + getInput.getPrimaryKey() + "] and unique key [" + getInput.getUniqueKey() + "]");
}
if(getInput.getUniqueKey() != null)
{
QTableMetaData table = getInput.getTable();
boolean foundMatch = false;
for(UniqueKey uniqueKey : table.getUniqueKeys())
{
if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet()))
{
foundMatch = true;
break;
}
}
if(!foundMatch)
{
throw new QException("Table [" + table.getName() + "] does not have a unique key defined on fields: " + getInput.getUniqueKey().keySet().stream().sorted().toList());
}
}
}
}

View File

@ -22,7 +22,11 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
@ -32,16 +36,26 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.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.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;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
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 org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
@ -65,7 +79,8 @@ public class GetAction
{
ActionHelper.validateSession(getInput);
postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
QTableMetaData table = getInput.getTable();
postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.getInput = getInput;
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
@ -79,22 +94,55 @@ public class GetAction
}
catch(IllegalStateException ise)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if a module doesn't implement Get directly - try to do a Get by a Query by the primary key //
// see below. //
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if a module doesn't implement Get directly - try to do a Get by a Query in the DefaultGetInterface (inner class) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
GetOutput getOutput;
if(getInterface != null)
if(getInterface == null)
{
getOutput = getInterface.execute(getInput);
}
else
{
getOutput = performGetViaQuery(getInput);
getInterface = new DefaultGetInterface();
}
getInterface.validateInput(getInput);
getOutput = getInterface.execute(getInput);
////////////////////////////
// handle cache use-cases //
////////////////////////////
if(table.getCacheOf() != null)
{
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////////////////////
// if the record wasn't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
if(recordFromSource != null)
{
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
InsertInput insertInput = new InsertInput(getInput.getInstance());
insertInput.setSession(getInput.getSession());
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////
// if the record was found, but it's too old, maybe re-fetch from cache source //
/////////////////////////////////////////////////////////////////////////////////
refreshCacheIfExpired(getInput, getOutput);
}
}
////////////////////////////////////////////////////////
// if the record is found, perform post-actions on it //
////////////////////////////////////////////////////////
if(getOutput.getRecord() != null)
{
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
@ -108,20 +156,162 @@ public class GetAction
/*******************************************************************************
**
*******************************************************************************/
private GetOutput performGetViaQuery(GetInput getInput) throws QException
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
{
QueryInput queryInput = new QueryInput(getInput.getInstance());
queryInput.setSession(getInput.getSession());
queryInput.setTableName(getInput.getTableName());
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey()))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
GetOutput getOutput = new GetOutput();
if(!queryOutput.getRecords().isEmpty())
QRecord cacheRecord = new QRecord(recordFromSource);
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
{
getOutput.setRecord(queryOutput.getRecords().get(0));
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
}
return (cacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
{
QTableMetaData table = getInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
QRecord cachedRecord = getOutput.getRecord();
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
if(recordFromSource != null)
{
///////////////////////////////////////////////////////////////////
// if the record was found in the source, update it in the cache //
///////////////////////////////////////////////////////////////////
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
UpdateInput updateInput = new UpdateInput(getInput.getInstance());
updateInput.setSession(getInput.getSession());
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
getOutput.setRecord(updateOutput.getRecords().get(0));
}
else
{
/////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source, then remove it from the cache //
/////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput(getInput.getInstance());
deleteInput.setSession(getInput.getSession());
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(getOutput.getRecord().getValue(table.getPrimaryKeyField())));
new DeleteAction().execute(deleteInput);
getOutput.setRecord(null);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord tryToGetFromCacheSource(GetInput getInput, GetOutput getOutput) throws QException
{
QRecord recordFromSource = null;
QTableMetaData table = getInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
{
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
break;
}
else
{
// todo!!
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
}
}
return (recordFromSource);
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
{
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput(getInput.getInstance());
sourceGetInput.setSession(getInput.getSession());
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
return (sourceGetOutput.getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
private static class DefaultGetInterface implements GetInterface
{
@Override
public GetOutput execute(GetInput getInput) throws QException
{
QueryInput queryInput = new QueryInput(getInput.getInstance());
queryInput.setSession(getInput.getSession());
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
// build filter using either pkey or unique key //
//////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
if(getInput.getPrimaryKey() != null)
{
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
}
else if(getInput.getUniqueKey() != null)
{
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
{
if(entry.getValue() == null)
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
}
else
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
}
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
}
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
GetOutput getOutput = new GetOutput();
if(!queryOutput.getRecords().isEmpty())
{
getOutput.setRecord(queryOutput.getRecords().get(0));
}
return (getOutput);
}
return (getOutput);
}

View File

@ -48,6 +48,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
@ -65,6 +67,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
@ -131,6 +135,7 @@ public class QInstanceValidator
validateApps(qInstance);
validatePossibleValueSources(qInstance);
validateQueuesAndProviders(qInstance);
validateJoins(qInstance);
validateUniqueTopLevelNames(qInstance);
}
@ -149,6 +154,43 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateJoins(QInstance qInstance)
{
qInstance.getJoins().forEach((joinName, join) ->
{
assertCondition(Objects.equals(joinName, join.getName()), "Inconsistent naming for join: " + joinName + "/" + join.getName() + ".");
assertCondition(StringUtils.hasContent(join.getLeftTable()), "Missing left-table name in join: " + joinName);
assertCondition(StringUtils.hasContent(join.getRightTable()), "Missing right-table name in join: " + joinName);
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.");
for(JoinOn joinOn : CollectionUtils.nonNullList(join.getJoinOns()))
{
assertCondition(StringUtils.hasContent(joinOn.getLeftField()), "Missing left-field name in a joinOn for join: " + joinName);
assertCondition(StringUtils.hasContent(joinOn.getRightField()), "Missing right-field name in a joinOn for join: " + joinName);
if(leftTableExists)
{
assertNoException(() -> qInstance.getTable(join.getLeftTable()).getField(joinOn.getLeftField()), "Left field name in joinOn " + joinName + " is not a defined field in table " + join.getLeftTable());
}
if(rightTableExists)
{
assertNoException(() -> qInstance.getTable(join.getRightTable()).getField(joinOn.getRightField()), "Right field name in joinOn " + joinName + " is not a defined field in table " + join.getRightTable());
}
}
});
}
/*******************************************************************************
** there can be some unexpected bad-times if you have a table and process, or
** table and app (etc) with the same name (e.g., in app tree building). So,
@ -305,14 +347,7 @@ public class QInstanceValidator
{
table.getFields().forEach((fieldName, field) ->
{
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
if(field.getPossibleValueSourceName() != null)
{
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
}
validateTableField(qInstance, tableName, fieldName, field);
});
}
@ -392,12 +427,82 @@ public class QInstanceValidator
{
validateAssociatedScripts(table);
}
//////////////////////
// validate cacheOf //
//////////////////////
if(table.getCacheOf() != null)
{
validateTableCacheOf(qInstance, table);
}
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QFieldMetaData field)
{
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
if(field.getPossibleValueSourceName() != null)
{
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
}
ValueTooLongBehavior behavior = field.getBehavior(qInstance, ValueTooLongBehavior.class);
if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH))
{
assertCondition(field.getMaxLength() != null, "Field " + fieldName + " in table " + tableName + " specifies a ValueTooLongBehavior, but not a maxLength.");
}
if(field.getMaxLength() != null)
{
assertCondition(field.getMaxLength() > 0, "Field " + fieldName + " in table " + tableName + " has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0.");
assertCondition(field.getType().isStringLike(), "Field " + fieldName + " in table " + tableName + " has maxLength, but is not of a supported type (" + field.getType() + ") - must be a string-like type.");
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTableCacheOf(QInstance qInstance, QTableMetaData table)
{
CacheOf cacheOf = table.getCacheOf();
String prefix = "Table " + table.getName() + " cacheOf ";
String sourceTableName = cacheOf.getSourceTable();
if(assertCondition(StringUtils.hasContent(sourceTableName), prefix + "is missing a sourceTable name"))
{
assertCondition(qInstance.getTable(sourceTableName) != null, prefix + "is referencing an unknown sourceTable: " + sourceTableName);
boolean hasExpirationSeconds = cacheOf.getExpirationSeconds() != null;
boolean hasCacheDateFieldName = StringUtils.hasContent(cacheOf.getCachedDateFieldName());
assertCondition(hasExpirationSeconds && hasCacheDateFieldName || (!hasExpirationSeconds && !hasCacheDateFieldName), prefix + "is missing either expirationSeconds or cachedDateFieldName (must either have both, or neither.)");
if(hasCacheDateFieldName)
{
assertNoException(() -> table.getField(cacheOf.getCachedDateFieldName()), prefix + "cachedDateFieldName " + cacheOf.getCachedDateFieldName() + " is an unrecognized field.");
}
if(assertCondition(CollectionUtils.nullSafeHasContents(cacheOf.getUseCases()), prefix + "does not have any useCases defined."))
{
for(CacheUseCase useCase : cacheOf.getUseCases())
{
assertCondition(useCase.getType() != null, prefix + "has a useCase without a type.");
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -36,7 +37,9 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
public class GetInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private Serializable primaryKey;
private Serializable primaryKey;
private Map<String, Serializable> uniqueKey;
private boolean shouldTranslatePossibleValues = false;
private boolean shouldGenerateDisplayValues = false;
@ -104,7 +107,41 @@ public class GetInput extends AbstractTableActionInput
this.primaryKey = primaryKey;
return (this);
}
/*******************************************************************************
** Getter for uniqueKey
**
*******************************************************************************/
public Map<String, Serializable> getUniqueKey()
{
return uniqueKey;
}
/*******************************************************************************
** Setter for uniqueKey
**
*******************************************************************************/
public void setUniqueKey(Map<String, Serializable> uniqueKey)
{
this.uniqueKey = uniqueKey;
}
/*******************************************************************************
** Fluent setter for uniqueKey
**
*******************************************************************************/
public GetInput withUniqueKey(Map<String, Serializable> uniqueKey)
{
this.uniqueKey = uniqueKey;
return (this);
}
/*******************************************************************************

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -39,6 +41,9 @@ public class ChildRecordListData implements QWidget
private String tablePath;
private String viewAllLink;
private boolean canAddChildRecord = true;
private Map<String, Serializable> defaultValuesForNewChildRecords;
/*******************************************************************************
@ -209,4 +214,73 @@ public class ChildRecordListData implements QWidget
{
this.viewAllLink = viewAllLink;
}
/*******************************************************************************
** Getter for canAddChildRecord
**
*******************************************************************************/
public boolean getCanAddChildRecord()
{
return canAddChildRecord;
}
/*******************************************************************************
** Setter for canAddChildRecord
**
*******************************************************************************/
public void setCanAddChildRecord(boolean canAddChildRecord)
{
this.canAddChildRecord = canAddChildRecord;
}
/*******************************************************************************
** Fluent setter for canAddChildRecord
**
*******************************************************************************/
public ChildRecordListData withCanAddChildRecord(boolean canAddChildRecord)
{
this.canAddChildRecord = canAddChildRecord;
return (this);
}
/*******************************************************************************
** Getter for defaultValuesForNewChildRecords
**
*******************************************************************************/
public Map<String, Serializable> getDefaultValuesForNewChildRecords()
{
return defaultValuesForNewChildRecords;
}
/*******************************************************************************
** Setter for defaultValuesForNewChildRecords
**
*******************************************************************************/
public void setDefaultValuesForNewChildRecords(Map<String, Serializable> defaultValuesForNewChildRecords)
{
this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords;
}
/*******************************************************************************
** Fluent setter for defaultValuesForNewChildRecords
**
*******************************************************************************/
public ChildRecordListData withDefaultValuesForNewChildRecords(Map<String, Serializable> defaultValuesForNewChildRecords)
{
this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords;
return (this);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
@ -416,6 +417,16 @@ public class QRecord implements Serializable
/*******************************************************************************
**
*******************************************************************************/
public Instant getValueInstant(String fieldName)
{
return (ValueUtils.getValueAsInstant(values.get(fieldName)));
}
/*******************************************************************************
** Getter for backendDetails
**

View File

@ -0,0 +1,54 @@
/*
* 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.model.metadata.dashboard;
/*******************************************************************************
**
*******************************************************************************/
public class AbstractWidgetMetaDataBuilder
{
protected QWidgetMetaData widgetMetaData;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AbstractWidgetMetaDataBuilder(QWidgetMetaData widgetMetaData)
{
this.widgetMetaData = widgetMetaData;
}
/*******************************************************************************
** Getter for widgetMetaData
**
*******************************************************************************/
public QWidgetMetaData getWidgetMetaData()
{
return widgetMetaData;
}
}

View File

@ -88,4 +88,14 @@ public enum QFieldType
throw (new QException("Unrecognized class [" + c + "]"));
}
/*******************************************************************************
**
*******************************************************************************/
public boolean isStringLike()
{
return this == QFieldType.STRING || this == QFieldType.TEXT || this == QFieldType.HTML || this == QFieldType.PASSWORD;
}
}

View File

@ -41,6 +41,7 @@ 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.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
/*******************************************************************************
@ -85,6 +86,8 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
private Set<Capability> enabledCapabilities = new HashSet<>();
private Set<Capability> disabledCapabilities = new HashSet<>();
private CacheOf cacheOf;
/*******************************************************************************
@ -1000,4 +1003,38 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
return (this);
}
/*******************************************************************************
** Getter for cacheOf
**
*******************************************************************************/
public CacheOf getCacheOf()
{
return cacheOf;
}
/*******************************************************************************
** Setter for cacheOf
**
*******************************************************************************/
public void setCacheOf(CacheOf cacheOf)
{
this.cacheOf = cacheOf;
}
/*******************************************************************************
** Fluent setter for cacheOf
**
*******************************************************************************/
public QTableMetaData withCacheOf(CacheOf cacheOf)
{
this.cacheOf = cacheOf;
return (this);
}
}

View File

@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@ -37,6 +39,38 @@ public class UniqueKey
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public UniqueKey()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public UniqueKey(List<String> fieldNames)
{
this.fieldNames = fieldNames;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public UniqueKey(String... fieldNames)
{
this.fieldNames = Arrays.stream(fieldNames).toList();
}
/*******************************************************************************
** Getter for fieldNames
**
@ -117,4 +151,21 @@ public class UniqueKey
this.fieldNames.add(fieldName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public String getDescription(QTableMetaData table)
{
List<String> fieldLabels = new ArrayList<>();
for(String fieldName : getFieldNames())
{
fieldLabels.add(table.getField(fieldName).getLabel());
}
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
}
}

View File

@ -0,0 +1,195 @@
/*
* 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.model.metadata.tables.cache;
import java.util.ArrayList;
import java.util.List;
/*******************************************************************************
** Meta-data, to assign to a table which is a "cache of" another table.
** e.g., a database table that's a "cache of" an api table - we'd have
** databaseTable.withCacheOf(sourceTable=apiTable)
*******************************************************************************/
public class CacheOf
{
private String sourceTable;
private Integer expirationSeconds;
private String cachedDateFieldName;
private List<CacheUseCase> useCases;
// private QCodeReference mapper;
/*******************************************************************************
** Getter for sourceTable
**
*******************************************************************************/
public String getSourceTable()
{
return sourceTable;
}
/*******************************************************************************
** Setter for sourceTable
**
*******************************************************************************/
public void setSourceTable(String sourceTable)
{
this.sourceTable = sourceTable;
}
/*******************************************************************************
** Fluent setter for sourceTable
**
*******************************************************************************/
public CacheOf withSourceTable(String sourceTable)
{
this.sourceTable = sourceTable;
return (this);
}
/*******************************************************************************
** Getter for expirationSeconds
**
*******************************************************************************/
public Integer getExpirationSeconds()
{
return expirationSeconds;
}
/*******************************************************************************
** Setter for expirationSeconds
**
*******************************************************************************/
public void setExpirationSeconds(Integer expirationSeconds)
{
this.expirationSeconds = expirationSeconds;
}
/*******************************************************************************
** Fluent setter for expirationSeconds
**
*******************************************************************************/
public CacheOf withExpirationSeconds(Integer expirationSeconds)
{
this.expirationSeconds = expirationSeconds;
return (this);
}
/*******************************************************************************
** Getter for cachedDateFieldName
**
*******************************************************************************/
public String getCachedDateFieldName()
{
return cachedDateFieldName;
}
/*******************************************************************************
** Setter for cachedDateFieldName
**
*******************************************************************************/
public void setCachedDateFieldName(String cachedDateFieldName)
{
this.cachedDateFieldName = cachedDateFieldName;
}
/*******************************************************************************
** Fluent setter for cachedDateFieldName
**
*******************************************************************************/
public CacheOf withCachedDateFieldName(String cachedDateFieldName)
{
this.cachedDateFieldName = cachedDateFieldName;
return (this);
}
/*******************************************************************************
** Getter for useCases
**
*******************************************************************************/
public List<CacheUseCase> getUseCases()
{
return useCases;
}
/*******************************************************************************
** Setter for useCases
**
*******************************************************************************/
public void setUseCases(List<CacheUseCase> useCases)
{
this.useCases = useCases;
}
/*******************************************************************************
** Fluent setter for useCases
**
*******************************************************************************/
public CacheOf withUseCases(List<CacheUseCase> useCases)
{
this.useCases = useCases;
return (this);
}
/*******************************************************************************
** Fluent setter for useCases
**
*******************************************************************************/
public CacheOf withUseCase(CacheUseCase useCase)
{
if(this.useCases == null)
{
this.useCases = new ArrayList<>();
}
this.useCases.add(useCase);
return (this);
}
}

View File

@ -0,0 +1,187 @@
/*
* 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.model.metadata.tables.cache;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
**
*******************************************************************************/
public class CacheUseCase
{
public enum Type
{
PRIMARY_KEY_TO_PRIMARY_KEY, // e.g., the primary key in the cache table equals the primary key in the source table.
UNIQUE_KEY_TO_PRIMARY_KEY, // e.g., a unique key in the cache table equals the primary key in the source table.
UNIQUE_KEY_TO_UNIQUE_KEY // e..g, a unique key in the cache table equals a unique key in the source table.
}
private Type type;
private boolean cacheSourceMisses = false; // whether or not, if a "miss" happens in the SOURCE, if that fact gets cached.
//////////////////////////
// for UNIQUE_KEY types //
//////////////////////////
private UniqueKey cacheUniqueKey;
private UniqueKey sourceUniqueKey;
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public Type getType()
{
return type;
}
/*******************************************************************************
** Setter for type
**
*******************************************************************************/
public void setType(Type type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
**
*******************************************************************************/
public CacheUseCase withType(Type type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for cacheSourceMisses
**
*******************************************************************************/
public boolean getCacheSourceMisses()
{
return cacheSourceMisses;
}
/*******************************************************************************
** Setter for cacheSourceMisses
**
*******************************************************************************/
public void setCacheSourceMisses(boolean cacheSourceMisses)
{
this.cacheSourceMisses = cacheSourceMisses;
}
/*******************************************************************************
** Fluent setter for cacheSourceMisses
**
*******************************************************************************/
public CacheUseCase withCacheSourceMisses(boolean cacheSourceMisses)
{
this.cacheSourceMisses = cacheSourceMisses;
return (this);
}
/*******************************************************************************
** Getter for cacheUniqueKey
**
*******************************************************************************/
public UniqueKey getCacheUniqueKey()
{
return cacheUniqueKey;
}
/*******************************************************************************
** Setter for cacheUniqueKey
**
*******************************************************************************/
public void setCacheUniqueKey(UniqueKey cacheUniqueKey)
{
this.cacheUniqueKey = cacheUniqueKey;
}
/*******************************************************************************
** Fluent setter for cacheUniqueKey
**
*******************************************************************************/
public CacheUseCase withCacheUniqueKey(UniqueKey cacheUniqueKey)
{
this.cacheUniqueKey = cacheUniqueKey;
return (this);
}
/*******************************************************************************
** Getter for sourceUniqueKey
**
*******************************************************************************/
public UniqueKey getSourceUniqueKey()
{
return sourceUniqueKey;
}
/*******************************************************************************
** Setter for sourceUniqueKey
**
*******************************************************************************/
public void setSourceUniqueKey(UniqueKey sourceUniqueKey)
{
this.sourceUniqueKey = sourceUniqueKey;
}
/*******************************************************************************
** Fluent setter for sourceUniqueKey
**
*******************************************************************************/
public CacheUseCase withSourceUniqueKey(UniqueKey sourceUniqueKey)
{
this.sourceUniqueKey = sourceUniqueKey;
return (this);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
/*******************************************************************************
**
*******************************************************************************/
public class MemoryTableBackendDetails extends QTableBackendDetails
{
private boolean cloneUponStore = false;
/*******************************************************************************
** Getter for cloneUponStore
**
*******************************************************************************/
public boolean getCloneUponStore()
{
return cloneUponStore;
}
/*******************************************************************************
** Setter for cloneUponStore
**
*******************************************************************************/
public void setCloneUponStore(boolean cloneUponStore)
{
this.cloneUponStore = cloneUponStore;
}
/*******************************************************************************
** Fluent setter for cloneUponStore
**
*******************************************************************************/
public MemoryTableBackendDetails withCloneUponStore(boolean cloneUponStore)
{
this.cloneUponStore = cloneUponStore;
return (this);
}
}

View File

@ -48,7 +48,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@ -176,16 +175,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
*******************************************************************************/
private Set<List<Serializable>> getExistingKeys(RunBackendStepInput runBackendStepInput, UniqueKey uniqueKey) throws QException
{
return (getExistingKeys(runBackendStepInput, uniqueKey.getFieldNames()));
}
/*******************************************************************************
**
*******************************************************************************/
private Set<List<Serializable>> getExistingKeys(RunBackendStepInput runBackendStepInput, List<String> ukFieldNames) throws QException
{
List<String> ukFieldNames = uniqueKey.getFieldNames();
Set<List<Serializable>> existingRecords = new HashSet<>();
if(ukFieldNames != null)
{
@ -276,7 +266,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
{
UniqueKey uniqueKey = entry.getKey();
ProcessSummaryLine ukErrorSummary = entry.getValue();
String ukErrorSuffix = " inserted, because they contain a duplicate key (" + getUkDescription(uniqueKey.getFieldNames()) + ")";
String ukErrorSuffix = " inserted, because they contain a duplicate key (" + uniqueKey.getDescription(table) + ")";
ukErrorSummary
.withSingularFutureMessage(tableLabel + " record will not be" + ukErrorSuffix)
@ -290,21 +280,4 @@ public class BulkInsertTransformStep extends AbstractTransformStep
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private String getUkDescription(List<String> ukFieldNames)
{
List<String> fieldLabels = new ArrayList<>();
for(String fieldName : ukFieldNames)
{
fieldLabels.add(table.getField(fieldName).getLabel());
}
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
}
}

View File

@ -405,8 +405,10 @@ public class GeneralProcessUtils
** get that record id.
**
*******************************************************************************/
public static Integer validateSingleSelectedId(RunBackendStepInput runBackendStepInput, String tableLabel) throws QException
public static Integer validateSingleSelectedId(RunBackendStepInput runBackendStepInput, String tableName) throws QException
{
String tableLabel = runBackendStepInput.getInstance().getTable(tableName).getLabel();
////////////////////////////////////////////////////
// Get the selected recordId and verify we only 1 //
////////////////////////////////////////////////////