diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java
index a80c095d..9dd03013 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java
@@ -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 ("""
-
+
""");
}
@@ -101,7 +102,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
protected String bulletNameValue(String name, String value)
{
- return ("- " + name + " " + value + "
");
+ return ("- " + name + " " + Objects.requireNonNullElse(value, "--") + "
");
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java
index a5507f42..c2918111 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java
@@ -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 defaultValuesForNewChildRecords = new HashMap<>();
+ for(JoinOn joinOn : join.getJoinOns())
+ {
+ defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
+ }
+ widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
+ }
+
+ return (new RenderWidgetOutput(widgetData));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
index 258cd856..aeff3162 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
@@ -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());
+ }
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
index da923b7b..f6162271 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
@@ -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 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);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index ee58535c..2a76e5b7 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -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.");
+ }
+ }
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
index e08e3f22..ccb3bb1f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
@@ -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 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 getUniqueKey()
+ {
+ return uniqueKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for uniqueKey
+ **
+ *******************************************************************************/
+ public void setUniqueKey(Map uniqueKey)
+ {
+ this.uniqueKey = uniqueKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for uniqueKey
+ **
+ *******************************************************************************/
+ public GetInput withUniqueKey(Map uniqueKey)
+ {
+ this.uniqueKey = uniqueKey;
+ return (this);
+ }
+
/*******************************************************************************
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java
index 75ddbf4d..86339e57 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java
@@ -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 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 getDefaultValuesForNewChildRecords()
+ {
+ return defaultValuesForNewChildRecords;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for defaultValuesForNewChildRecords
+ **
+ *******************************************************************************/
+ public void setDefaultValuesForNewChildRecords(Map defaultValuesForNewChildRecords)
+ {
+ this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for defaultValuesForNewChildRecords
+ **
+ *******************************************************************************/
+ public ChildRecordListData withDefaultValuesForNewChildRecords(Map defaultValuesForNewChildRecords)
+ {
+ this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
index 308f0b7c..e5fc5505 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java
@@ -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
**
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java
new file mode 100644
index 00000000..24b5a06b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java
@@ -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 .
+ */
+
+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;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java
index 41ffdcc4..10f6d567 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java
@@ -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;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
index 61a007cf..1530576c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
@@ -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 enabledCapabilities = new HashSet<>();
private Set 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);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java
index 698e7417..a2c1204c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java
@@ -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 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 fieldLabels = new ArrayList<>();
+
+ for(String fieldName : getFieldNames())
+ {
+ fieldLabels.add(table.getField(fieldName).getLabel());
+ }
+
+ return (StringUtils.joinWithCommasAndAnd(fieldLabels));
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java
new file mode 100644
index 00000000..c7359c85
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java
@@ -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 .
+ */
+
+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 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 getUseCases()
+ {
+ return useCases;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for useCases
+ **
+ *******************************************************************************/
+ public void setUseCases(List useCases)
+ {
+ this.useCases = useCases;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for useCases
+ **
+ *******************************************************************************/
+ public CacheOf withUseCases(List 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);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java
new file mode 100644
index 00000000..62def005
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java
@@ -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 .
+ */
+
+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);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java
new file mode 100644
index 00000000..c3726fb9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java
@@ -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 .
+ */
+
+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);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java
index ba7912d5..0f9c12ad 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java
@@ -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> getExistingKeys(RunBackendStepInput runBackendStepInput, UniqueKey uniqueKey) throws QException
{
- return (getExistingKeys(runBackendStepInput, uniqueKey.getFieldNames()));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private Set> getExistingKeys(RunBackendStepInput runBackendStepInput, List ukFieldNames) throws QException
- {
+ List ukFieldNames = uniqueKey.getFieldNames();
Set> 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 ukFieldNames)
- {
- List fieldLabels = new ArrayList<>();
-
- for(String fieldName : ukFieldNames)
- {
- fieldLabels.add(table.getField(fieldName).getLabel());
- }
-
- return (StringUtils.joinWithCommasAndAnd(fieldLabels));
- }
-
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
index 1ec145b1..9aa75a73 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
@@ -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 //
////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java
index 60ec3cb0..5775802c 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java
@@ -69,8 +69,9 @@ class ChildRecordListRendererTest
void testParentRecordNotFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
RenderWidgetInput input = new RenderWidgetInput(qInstance);
@@ -92,8 +93,9 @@ class ChildRecordListRendererTest
void testNoChildRecordsFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
@@ -122,8 +124,9 @@ class ChildRecordListRendererTest
void testChildRecordsFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java
index 3c927c8d..13a9009e 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java
@@ -117,8 +117,9 @@ class ParentWidgetRendererTest
void testNoChildRecordsFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
@@ -147,8 +148,9 @@ class ParentWidgetRendererTest
void testChildRecordsFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java
index 97fbbdd8..2f788cf9 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java
@@ -103,8 +103,9 @@ class ProcessWidgetRendererTest
void testNoChildRecordsFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
@@ -133,8 +134,9 @@ class ProcessWidgetRendererTest
void testChildRecordsFound() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
- QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
- .withLabel("Line Items");
+ QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
+ .withLabel("Line Items")
+ .getWidgetMetaData();
qInstance.addWidget(widget);
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
index dd576194..71575bd2 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
@@ -22,12 +22,28 @@
package com.kingsrook.qqq.backend.core.actions.tables;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
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.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.update.UpdateInput;
+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.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
@@ -37,6 +53,19 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
class GetActionTest
{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ @AfterEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ MemoryRecordStore.resetStatistics();
+ }
+
+
+
/*******************************************************************************
** At the core level, there isn't much that can be asserted, as it uses the
** mock implementation - just confirming that all of the "wiring" works.
@@ -55,4 +84,164 @@ class GetActionTest
assertNotNull(result);
assertNotNull(result.getRecord());
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testUniqueKeyCache() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+
+ /////////////////////////////////////
+ // insert rows in the source table //
+ /////////////////////////////////////
+ TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
+ new QRecord().withValue("id", 1).withValue("firstName", "George").withValue("lastName", "Washington").withValue("noOfShoes", 5),
+ new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Adams"),
+ new QRecord().withValue("id", 3).withValue("firstName", "Thomas").withValue("lastName", "Jefferson")
+ ));
+
+ /////////////////////////////////////////////////////////////////////////////
+ // get from the table which caches it - confirm they are (magically) found //
+ /////////////////////////////////////////////////////////////////////////////
+ {
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ assertNotNull(getOutput.getRecord());
+ assertNotNull(getOutput.getRecord().getValue("cachedDate"));
+ assertEquals(5, getOutput.getRecord().getValue("noOfShoes"));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // request a row that doesn't exist in cache or source, should miss both //
+ ///////////////////////////////////////////////////////////////////////////
+ {
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "John", "lastName", "McCain"));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ assertNull(getOutput.getRecord());
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // update the record in the source table - then re-get from cache table - shouldn't see new value. //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ {
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 6)));
+ new UpdateAction().execute(updateInput);
+
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ assertNotNull(getOutput.getRecord());
+ assertNotNull(getOutput.getRecord().getValue("cachedDate"));
+ assertEquals(5, getOutput.getRecord().getValue("noOfShoes"));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // delete the cached record; re-get, and we should see the updated value //
+ ///////////////////////////////////////////////////////////////////////////
+ {
+ DeleteInput deleteInput = new DeleteInput(qInstance);
+ deleteInput.setSession(new QSession());
+ deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "George")));
+ new DeleteAction().execute(deleteInput);
+
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ assertNotNull(getOutput.getRecord());
+ assertNotNull(getOutput.getRecord().getValue("cachedDate"));
+ assertEquals(6, getOutput.getRecord().getValue("noOfShoes"));
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // update the source record; see that it isn't updated in cache. //
+ ///////////////////////////////////////////////////////////////////
+ {
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 7)));
+ new UpdateAction().execute(updateInput);
+
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ assertNotNull(getOutput.getRecord());
+ assertNotNull(getOutput.getRecord().getValue("cachedDate"));
+ assertEquals(6, getOutput.getRecord().getValue("noOfShoes"));
+
+ ///////////////////////////////////////////////////////////////////////
+ // then artificially move back the cachedDate in the cache table. //
+ // then re-get from cache table, and we should see the updated value //
+ ///////////////////////////////////////////////////////////////////////
+ updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
+ new UpdateAction().execute(updateInput);
+
+ getOutput = new GetAction().execute(getInput);
+ assertEquals(7, getOutput.getRecord().getValue("noOfShoes"));
+ }
+
+ /////////////////////////////////////////////////
+ // should only be 1 cache record at this point //
+ /////////////////////////////////////////////////
+ assertEquals(1, TestUtils.queryTable(TestUtils.defineInstance(), TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE).size());
+
+ //////////////////////////////////////////////////////////////////////
+ // delete the source record - it will still be in the cache though. //
+ //////////////////////////////////////////////////////////////////////
+ {
+ DeleteInput deleteInput = new DeleteInput(qInstance);
+ deleteInput.setSession(new QSession());
+ deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ deleteInput.setPrimaryKeys(List.of(1));
+ new DeleteAction().execute(deleteInput);
+
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ assertNotNull(getOutput.getRecord());
+
+ ////////////////////////////////////////////////////////////////////
+ // then artificially move back the cachedDate in the cache table. //
+ // then re-get from cache table, and now it should go away //
+ ////////////////////////////////////////////////////////////////////
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(new QSession());
+ updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
+ new UpdateAction().execute(updateInput);
+
+ getInput = new GetInput(qInstance);
+ getInput.setSession(new QSession());
+ getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
+ getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
+ getOutput = new GetAction().execute(getInput);
+ assertNull(getOutput.getRecord());
+ }
+ }
+
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
index 84033870..11ae63ff 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
@@ -1180,8 +1180,7 @@ class QInstanceValidatorTest
void testValidUniqueKeys()
{
assertValidationSuccess((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
- .withUniqueKey(new UniqueKey().withFieldName("id"))
- .withUniqueKey(new UniqueKey().withFieldName("firstName").withFieldName("lastName")));
+ .withUniqueKey(new UniqueKey().withFieldName("id")));
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
index 749896ac..55fe4ace 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
@@ -86,15 +86,19 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
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.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.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
+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.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
@@ -133,6 +137,7 @@ public class TestUtils
public static final String PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport";
public static final String TABLE_NAME_PERSON_FILE = "personFile";
public static final String TABLE_NAME_PERSON_MEMORY = "personMemory";
+ public static final String TABLE_NAME_PERSON_MEMORY_CACHE = "personMemoryCache";
public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly";
public static final String TABLE_NAME_BASEPULL = "basepullTest";
public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport";
@@ -164,6 +169,7 @@ public class TestUtils
qInstance.addTable(defineTablePerson());
qInstance.addTable(definePersonFileTable());
qInstance.addTable(definePersonMemoryTable());
+ qInstance.addTable(definePersonMemoryCacheTable());
qInstance.addTable(defineTableIdAndNameOnly());
qInstance.addTable(defineTableShape());
qInstance.addTable(defineTableBasepull());
@@ -608,6 +614,7 @@ public class TestUtils
.withName(TABLE_NAME_PERSON_MEMORY)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
+ .withUniqueKey(new UniqueKey("firstName", "lastName"))
.withFields(TestUtils.defineTablePerson().getFields()))
.withField(standardQqqAutomationStatusField())
@@ -635,6 +642,36 @@ public class TestUtils
+ /*******************************************************************************
+ ** Define yet another version of the 'person' table, also in-memory, and as a
+ ** cache on the other in-memory one...
+ *******************************************************************************/
+ public static QTableMetaData definePersonMemoryCacheTable()
+ {
+ UniqueKey uniqueKey = new UniqueKey("firstName", "lastName");
+ return (new QTableMetaData()
+ .withName(TABLE_NAME_PERSON_MEMORY_CACHE)
+ .withBackendName(MEMORY_BACKEND_NAME)
+ .withBackendDetails(new MemoryTableBackendDetails()
+ .withCloneUponStore(true))
+ .withPrimaryKeyField("id")
+ .withUniqueKey(uniqueKey)
+ .withFields(TestUtils.defineTablePerson().getFields()))
+ .withField(new QFieldMetaData("cachedDate", QFieldType.DATE_TIME))
+ .withCacheOf(new CacheOf()
+ .withSourceTable(TABLE_NAME_PERSON_MEMORY)
+ .withCachedDateFieldName("cachedDate")
+ .withExpirationSeconds(60)
+ .withUseCase(new CacheUseCase()
+ .withType(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY)
+ .withSourceUniqueKey(uniqueKey)
+ .withCacheUniqueKey(uniqueKey)
+ .withCacheSourceMisses(false)
+ ));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
index 606e0da6..4996b8d2 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
@@ -57,9 +57,14 @@ import com.kingsrook.qqq.backend.core.utils.QLogger;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.module.api.exceptions.OAuthCredentialsException;
+import com.kingsrook.qqq.backend.module.api.exceptions.OAuthExpiredTokenException;
import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
+import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
@@ -69,6 +74,9 @@ import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -122,9 +130,11 @@ public class BaseAPIActionUtil
{
try
{
- String urlSuffix = buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey());
- String url = buildTableUrl(table);
- HttpGet request = new HttpGet(url + urlSuffix);
+ String urlSuffix = getInput.getPrimaryKey() != null
+ ? buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey())
+ : buildUrlSuffixForSingleRecordGet(getInput.getUniqueKey());
+ String url = buildTableUrl(table);
+ HttpGet request = new HttpGet(url + urlSuffix);
GetOutput rs = new GetOutput();
QHttpResponse response = makeRequest(table, request);
@@ -468,6 +478,8 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected void handleResponseError(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws QException
{
+ checkForOAuthExpiredToken(table, request, response);
+
int statusCode = response.getStatusCode();
String resultString = response.getContent();
String errorMessage = "HTTP " + request.getMethod() + " for table [" + table.getName() + "] failed with status " + statusCode + ": " + resultString;
@@ -486,6 +498,22 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected void checkForOAuthExpiredToken(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws OAuthExpiredTokenException
+ {
+ if(backendMetaData.getAuthorizationType().equals(AuthorizationType.OAUTH2))
+ {
+ if(response.getStatusCode().equals(HttpStatus.SC_UNAUTHORIZED)) // 401
+ {
+ throw (new OAuthExpiredTokenException("Expired token indicated by response: " + response));
+ }
+ }
+ }
+
+
+
/*******************************************************************************
** method to build up a query string based on a given QFilter object
**
@@ -511,13 +539,29 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String buildUrlSuffixForSingleRecordGet(Map uniqueKey) throws QException
+ {
+ QTableMetaData table = actionInput.getTable();
+ QQueryFilter filter = new QQueryFilter();
+ for(Map.Entry entry : uniqueKey.entrySet())
+ {
+ filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
+ }
+ return (buildQueryStringForGet(filter, 1, 0, table.getFields()));
+ }
+
+
+
/*******************************************************************************
** As part of making a request - set up its authorization header (not just
** strictly "Authorization", but whatever is needed for auth).
**
** Can be overridden if an API uses an authorization type we don't natively support.
*******************************************************************************/
- protected void setupAuthorizationInRequest(HttpRequestBase request)
+ protected void setupAuthorizationInRequest(HttpRequestBase request) throws QException
{
switch(backendMetaData.getAuthorizationType())
{
@@ -533,6 +577,10 @@ public class BaseAPIActionUtil
request.addHeader("API-Key", backendMetaData.getApiKey());
break;
+ case OAUTH2:
+ request.setHeader("Authorization", "Bearer " + getOAuth2Token());
+ break;
+
default:
throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType());
}
@@ -540,6 +588,66 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String getOAuth2Token() throws OAuthCredentialsException
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // check for the access token in the backend meta data. if it's not there, then issue a request for a token. //
+ // this is not generally meant to be put in the meta data by the app programmer - rather, we're just using //
+ // it as a "cheap & easy" way to "cache" the token within our process's memory... //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ String accessToken = ValueUtils.getValueAsString(backendMetaData.getCustomValue("accessToken"));
+
+ if(!StringUtils.hasContent(accessToken))
+ {
+ String fullURL = backendMetaData.getBaseUrl() + "oauth/token";
+ String postBody = "grant_type=client_credentials&client_id=" + backendMetaData.getClientId() + "&client_secret=" + backendMetaData.getClientSecret();
+
+ LOG.info(session, "Fetching OAuth2 token from " + fullURL);
+
+ try(CloseableHttpClient client = HttpClients.custom().setConnectionManager(new PoolingHttpClientConnectionManager()).build())
+ {
+ HttpPost request = new HttpPost(fullURL);
+ request.setEntity(new StringEntity(postBody));
+ request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
+
+ HttpResponse response = client.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ HttpEntity entity = response.getEntity();
+ String resultString = EntityUtils.toString(entity);
+ if(statusCode != HttpStatus.SC_OK)
+ {
+ throw (new OAuthCredentialsException("Did not receive successful response when requesting oauth token [" + statusCode + "]: " + resultString));
+ }
+
+ JSONObject resultJSON = new JSONObject(resultString);
+ accessToken = (resultJSON.getString("access_token"));
+ LOG.debug(session, "Fetched access token: " + accessToken);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // stash the access token in the backendMetaData, from which it will be used for future requests //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ backendMetaData.withCustomValue("accessToken", accessToken);
+ }
+ catch(OAuthCredentialsException oce)
+ {
+ throw (oce);
+ }
+ catch(Exception e)
+ {
+ String errorMessage = "Error getting OAuth Token";
+ LOG.warn(session, errorMessage, e);
+ throw (new OAuthCredentialsException(errorMessage, e));
+ }
+ }
+
+ return (accessToken);
+ }
+
+
+
/*******************************************************************************
** As part of making a request - set up its content-type header.
*******************************************************************************/
@@ -727,8 +835,9 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException
{
- int sleepMillis = getInitialRateLimitBackoffMillis();
- int rateLimitsCaught = 0;
+ int sleepMillis = getInitialRateLimitBackoffMillis();
+ int rateLimitsCaught = 0;
+ boolean caughtAnOAuthExpiredToken = false;
while(true)
{
@@ -768,6 +877,25 @@ public class BaseAPIActionUtil
return (qResponse);
}
}
+ catch(OAuthCredentialsException oce)
+ {
+ LOG.error(session, "OAuth Credential failure for [" + table.getName() + "]");
+ throw (oce);
+ }
+ catch(OAuthExpiredTokenException oete)
+ {
+ if(!caughtAnOAuthExpiredToken)
+ {
+ LOG.info(session, "OAuth Expired token for [" + table.getName() + "] - retrying");
+ backendMetaData.withCustomValue("accessToken", null);
+ caughtAnOAuthExpiredToken = true;
+ }
+ else
+ {
+ LOG.info(session, "OAuth Expired token for [" + table.getName() + "] even after a retry. Giving up.");
+ throw (oete);
+ }
+ }
catch(RateLimitException rle)
{
rateLimitsCaught++;
@@ -781,6 +909,13 @@ public class BaseAPIActionUtil
SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS);
sleepMillis *= 2;
}
+ catch(QException qe)
+ {
+ ///////////////////////////////////////////////////////////////
+ // re-throw exceptions that QQQ or application code produced //
+ ///////////////////////////////////////////////////////////////
+ throw (qe);
+ }
catch(Exception e)
{
String message = "An unknown error occurred trying to make an HTTP request to [" + request.getURI() + "] on table [" + table.getName() + "].";
@@ -906,7 +1041,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected int getInitialRateLimitBackoffMillis()
{
- return (0);
+ return (500);
}
@@ -916,7 +1051,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected int getMaxAllowedRateLimitErrors()
{
- return (0);
+ return (3);
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java
new file mode 100644
index 00000000..a418e1bf
--- /dev/null
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java
@@ -0,0 +1,52 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.module.api.exceptions;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+
+
+/*******************************************************************************
+ ** Exception to be thrown during OAuth Token generation.
+ **
+ *******************************************************************************/
+public class OAuthCredentialsException extends QException
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public OAuthCredentialsException(String message)
+ {
+ super(message);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public OAuthCredentialsException(String errorMessage, Exception e)
+ {
+ super(errorMessage, e);
+ }
+}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java
new file mode 100644
index 00000000..6c68e98f
--- /dev/null
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java
@@ -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 .
+ */
+
+package com.kingsrook.qqq.backend.module.api.exceptions;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+
+
+/*******************************************************************************
+ ** Exception to be thrown by a request that uses OAuth, if the current token
+ ** is expired. Generally should signal that the token needs refreshed, and the
+ ** request should be tried again.
+ **
+ *******************************************************************************/
+public class OAuthExpiredTokenException extends QException
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public OAuthExpiredTokenException(String message)
+ {
+ super(message);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public OAuthExpiredTokenException(String errorMessage, Exception e)
+ {
+ super(errorMessage, e);
+ }
+}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java
index f1852f53..92a4aaf7 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java
@@ -22,10 +22,13 @@
package com.kingsrook.qqq.backend.module.api.exceptions;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+
+
/*******************************************************************************
**
*******************************************************************************/
-public class RateLimitException extends Exception
+public class RateLimitException extends QException
{
/*******************************************************************************
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java
index fbfde3b9..0c542373 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java
@@ -30,5 +30,6 @@ public enum AuthorizationType
API_KEY_HEADER,
BASIC_AUTH_API_KEY,
BASIC_AUTH_USERNAME_PASSWORD,
+ OAUTH2,
}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java
index 773b61f8..310dc704 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java
@@ -39,6 +39,8 @@ public class APIBackendMetaData extends QBackendMetaData
{
private String baseUrl;
private String apiKey;
+ private String clientId;
+ private String clientSecret;
private String username;
private String password;
@@ -156,6 +158,74 @@ public class APIBackendMetaData extends QBackendMetaData
+ /*******************************************************************************
+ ** Getter for clientId
+ **
+ *******************************************************************************/
+ public String getClientId()
+ {
+ return clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientId
+ **
+ *******************************************************************************/
+ public void setClientId(String clientId)
+ {
+ this.clientId = clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for clientId
+ **
+ *******************************************************************************/
+ public APIBackendMetaData withClientId(String clientId)
+ {
+ this.clientId = clientId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for clientSecret
+ **
+ *******************************************************************************/
+ public String getClientSecret()
+ {
+ return clientSecret;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientSecret
+ **
+ *******************************************************************************/
+ public void setClientSecret(String clientSecret)
+ {
+ this.clientSecret = clientSecret;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for clientSecret
+ **
+ *******************************************************************************/
+ public APIBackendMetaData withClientSecret(String clientSecret)
+ {
+ this.clientSecret = clientSecret;
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for username
**
@@ -391,4 +461,16 @@ public class APIBackendMetaData extends QBackendMetaData
{
qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName());
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean requiresPrimaryKeyOnTables()
+ {
+ return (false);
+ }
+
}
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
index b0585069..bbe66c2d 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
@@ -414,7 +414,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
case IS_BLANK:
{
clause += " IS NULL";
- if(isString(field.getType()))
+ if(field.getType().isStringLike())
{
clause += " OR " + column + " = ''";
}
@@ -424,7 +424,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
case IS_NOT_BLANK:
{
clause += " IS NOT NULL";
- if(isString(field.getType()))
+ if(field.getType().isStringLike())
{
clause += " AND " + column + " != ''";
}
@@ -479,16 +479,6 @@ public abstract class AbstractRDBMSAction implements QActionInterface
- /*******************************************************************************
- **
- *******************************************************************************/
- private static boolean isString(QFieldType fieldType)
- {
- return fieldType == QFieldType.STRING || fieldType == QFieldType.TEXT || fieldType == QFieldType.HTML || fieldType == QFieldType.PASSWORD;
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
index 2d1916d2..409b8774 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
@@ -136,7 +136,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
// execute the query - iterate over results //
//////////////////////////////////////////////
QueryOutput queryOutput = new QueryOutput(queryInput);
- System.out.println(sql);
+ // System.out.println(sql);
PreparedStatement statement = createStatement(connection, sql.toString(), queryInput);
QueryManager.executeStatement(statement, ((ResultSet resultSet) ->
{
diff --git a/qqq-dev-tools/bin/createTableToEntityFields.groovy b/qqq-dev-tools/bin/createTableToRecordEntity.groovy
similarity index 95%
rename from qqq-dev-tools/bin/createTableToEntityFields.groovy
rename to qqq-dev-tools/bin/createTableToRecordEntity.groovy
index 91af3078..a2afb2eb 100755
--- a/qqq-dev-tools/bin/createTableToEntityFields.groovy
+++ b/qqq-dev-tools/bin/createTableToRecordEntity.groovy
@@ -1,14 +1,20 @@
#!/usr/bin/env groovy
/*******************************************************************************
- ** Script to convert a list of columnNames from a CREATE TABLE statement
- ** to fields for a QRecordEntity (on stdout)
+ ** Script to convert a CREATE TABLE statement to fields for a QRecordEntity
*******************************************************************************/
+if (args.length < 1)
+{
+ System.out.println("Usage: ${this.class.getSimpleName()} EntityClassName [writeWholeClass] [writeTableMetaData]")
+ System.exit(1);
+}
+
String className = args[0]
boolean writeWholeClass = args.length > 1 ? args[1] : false;
boolean writeTableMetaData = args.length > 2 ? args[2] : false;
+println("Please paste in a CREATE TABLE statement (then a newline and an end-of-file (CTRL-D))")
def reader = new BufferedReader(new InputStreamReader(System.in))
String line
String allFieldNames = ""
@@ -27,7 +33,6 @@ if(writeWholeClass)
public class %s extends QRecordEntity
{
public static final String TABLE_NAME = "%s";
-
""".formatted(className, className, classNameLcFirst));
}