mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Adding table-cacheOf concept; ability to add a child record from child-list widget
This commit is contained in:
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.Charset;
|
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.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||||
@ -48,7 +49,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
|||||||
protected String openTopLevelBulletList()
|
protected String openTopLevelBulletList()
|
||||||
{
|
{
|
||||||
return ("""
|
return ("""
|
||||||
<div style="padding: 1rem;">
|
<div style="padding-left: 2rem;">
|
||||||
<ul>""");
|
<ul>""");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
protected String bulletNameValue(String name, String value)
|
protected String bulletNameValue(String name, String value)
|
||||||
{
|
{
|
||||||
return ("<li><b>" + name + "</b> " + value + "</li>");
|
return ("<li><b>" + name + "</b> " + Objects.requireNonNullElse(value, "--") + "</li>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,9 +22,12 @@
|
|||||||
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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.GetAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
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.dashboard.widgets.WidgetType;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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.code.QCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.AbstractWidgetMetaDataBuilder;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
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.joins.QJoinMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
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.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())
|
.withName(join.getName())
|
||||||
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null))
|
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null))
|
||||||
.withType(WidgetType.CHILD_RECORD_LIST.getType())
|
.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 tablePath = input.getInstance().getTablePath(input, table.getName());
|
||||||
String viewAllLink = tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset());
|
String viewAllLink = tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset());
|
||||||
|
|
||||||
return (new RenderWidgetOutput(new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink)));
|
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink);
|
||||||
|
|
||||||
|
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
|
||||||
|
{
|
||||||
|
widgetData.setCanAddChildRecord(true);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// new child records must have values from the join-ons //
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||||
|
for(JoinOn joinOn : join.getJoinOns())
|
||||||
|
{
|
||||||
|
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
|
||||||
|
}
|
||||||
|
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new RenderWidgetOutput(widgetData));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,12 @@
|
|||||||
package com.kingsrook.qqq.backend.core.actions.interfaces;
|
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.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
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.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;
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,11 @@
|
|||||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
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.QPossibleValueTranslator;
|
||||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
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.GetInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
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.QCriteriaOperator;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
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.QQueryFilter;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
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.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.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.QBackendModuleDispatcher;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
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);
|
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;
|
this.getInput = getInput;
|
||||||
|
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
@ -79,22 +94,55 @@ public class GetAction
|
|||||||
}
|
}
|
||||||
catch(IllegalStateException ise)
|
catch(IllegalStateException ise)
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if a module doesn't implement Get directly - try to do a Get by a Query by the primary key //
|
// if a module doesn't implement Get directly - try to do a Get by a Query in the DefaultGetInterface (inner class) //
|
||||||
// see below. //
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GetOutput getOutput;
|
GetOutput getOutput;
|
||||||
if(getInterface != null)
|
if(getInterface == null)
|
||||||
{
|
{
|
||||||
|
getInterface = new DefaultGetInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
getInterface.validateInput(getInput);
|
||||||
getOutput = getInterface.execute(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
|
else
|
||||||
{
|
{
|
||||||
getOutput = performGetViaQuery(getInput);
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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)
|
if(getOutput.getRecord() != null)
|
||||||
{
|
{
|
||||||
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
|
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
|
||||||
@ -108,12 +156,153 @@ public class GetAction
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private GetOutput performGetViaQuery(GetInput getInput) throws QException
|
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
|
||||||
|
{
|
||||||
|
QRecord cacheRecord = new QRecord(recordFromSource);
|
||||||
|
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
|
||||||
|
{
|
||||||
|
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 queryInput = new QueryInput(getInput.getInstance());
|
||||||
queryInput.setSession(getInput.getSession());
|
queryInput.setSession(getInput.getSession());
|
||||||
queryInput.setTableName(getInput.getTableName());
|
queryInput.setTableName(getInput.getTableName());
|
||||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey()))));
|
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
// build filter using either pkey or unique key //
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
QQueryFilter filter = new QQueryFilter();
|
||||||
|
if(getInput.getPrimaryKey() != null)
|
||||||
|
{
|
||||||
|
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
|
||||||
|
}
|
||||||
|
else if(getInput.getUniqueKey() != null)
|
||||||
|
{
|
||||||
|
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
|
||||||
|
{
|
||||||
|
if(entry.getValue() == null)
|
||||||
|
{
|
||||||
|
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
|
||||||
|
}
|
||||||
|
|
||||||
|
queryInput.setFilter(filter);
|
||||||
|
|
||||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
|
|
||||||
GetOutput getOutput = new GetOutput();
|
GetOutput getOutput = new GetOutput();
|
||||||
@ -123,6 +312,7 @@ public class GetAction
|
|||||||
}
|
}
|
||||||
return (getOutput);
|
return (getOutput);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.QCodeType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
|
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.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.QAppChildMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
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.AutomationStatusTracking;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
|
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.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.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -131,6 +135,7 @@ public class QInstanceValidator
|
|||||||
validateApps(qInstance);
|
validateApps(qInstance);
|
||||||
validatePossibleValueSources(qInstance);
|
validatePossibleValueSources(qInstance);
|
||||||
validateQueuesAndProviders(qInstance);
|
validateQueuesAndProviders(qInstance);
|
||||||
|
validateJoins(qInstance);
|
||||||
|
|
||||||
validateUniqueTopLevelNames(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
|
** 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,
|
** 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) ->
|
table.getFields().forEach((fieldName, field) ->
|
||||||
{
|
{
|
||||||
assertCondition(Objects.equals(fieldName, field.getName()),
|
validateTableField(qInstance, tableName, fieldName, field);
|
||||||
"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 + ".");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,12 +427,82 @@ public class QInstanceValidator
|
|||||||
{
|
{
|
||||||
validateAssociatedScripts(table);
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
|
|||||||
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
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
|
public class GetInput extends AbstractTableActionInput
|
||||||
{
|
{
|
||||||
private QBackendTransaction transaction;
|
private QBackendTransaction transaction;
|
||||||
|
|
||||||
private Serializable primaryKey;
|
private Serializable primaryKey;
|
||||||
|
private Map<String, Serializable> uniqueKey;
|
||||||
|
|
||||||
private boolean shouldTranslatePossibleValues = false;
|
private boolean shouldTranslatePossibleValues = false;
|
||||||
private boolean shouldGenerateDisplayValues = false;
|
private boolean shouldGenerateDisplayValues = false;
|
||||||
@ -107,6 +110,40 @@ public class GetInput extends AbstractTableActionInput
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for uniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Map<String, Serializable> getUniqueKey()
|
||||||
|
{
|
||||||
|
return uniqueKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for uniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setUniqueKey(Map<String, Serializable> uniqueKey)
|
||||||
|
{
|
||||||
|
this.uniqueKey = uniqueKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for uniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public GetInput withUniqueKey(Map<String, Serializable> uniqueKey)
|
||||||
|
{
|
||||||
|
this.uniqueKey = uniqueKey;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for shouldTranslatePossibleValues
|
** Getter for shouldTranslatePossibleValues
|
||||||
**
|
**
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
|
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.actions.tables.query.QueryOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
|
||||||
@ -39,6 +41,9 @@ public class ChildRecordListData implements QWidget
|
|||||||
private String tablePath;
|
private String tablePath;
|
||||||
private String viewAllLink;
|
private String viewAllLink;
|
||||||
|
|
||||||
|
private boolean canAddChildRecord = true;
|
||||||
|
private Map<String, Serializable> defaultValuesForNewChildRecords;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -209,4 +214,73 @@ public class ChildRecordListData implements QWidget
|
|||||||
{
|
{
|
||||||
this.viewAllLink = viewAllLink;
|
this.viewAllLink = viewAllLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for canAddChildRecord
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean getCanAddChildRecord()
|
||||||
|
{
|
||||||
|
return canAddChildRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for canAddChildRecord
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setCanAddChildRecord(boolean canAddChildRecord)
|
||||||
|
{
|
||||||
|
this.canAddChildRecord = canAddChildRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for canAddChildRecord
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ChildRecordListData withCanAddChildRecord(boolean canAddChildRecord)
|
||||||
|
{
|
||||||
|
this.canAddChildRecord = canAddChildRecord;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for defaultValuesForNewChildRecords
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Map<String, Serializable> getDefaultValuesForNewChildRecords()
|
||||||
|
{
|
||||||
|
return defaultValuesForNewChildRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for defaultValuesForNewChildRecords
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setDefaultValuesForNewChildRecords(Map<String, Serializable> defaultValuesForNewChildRecords)
|
||||||
|
{
|
||||||
|
this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for defaultValuesForNewChildRecords
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ChildRecordListData withDefaultValuesForNewChildRecords(Map<String, Serializable> defaultValuesForNewChildRecords)
|
||||||
|
{
|
||||||
|
this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.data;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.ArrayList;
|
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
|
** Getter for backendDetails
|
||||||
**
|
**
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class AbstractWidgetMetaDataBuilder
|
||||||
|
{
|
||||||
|
protected QWidgetMetaData widgetMetaData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public AbstractWidgetMetaDataBuilder(QWidgetMetaData widgetMetaData)
|
||||||
|
{
|
||||||
|
this.widgetMetaData = widgetMetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for widgetMetaData
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QWidgetMetaData getWidgetMetaData()
|
||||||
|
{
|
||||||
|
return widgetMetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -88,4 +88,14 @@ public enum QFieldType
|
|||||||
|
|
||||||
throw (new QException("Unrecognized class [" + c + "]"));
|
throw (new QException("Unrecognized class [" + c + "]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean isStringLike()
|
||||||
|
{
|
||||||
|
return this == QFieldType.STRING || this == QFieldType.TEXT || this == QFieldType.HTML || this == QFieldType.PASSWORD;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.QAppChildMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
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.automation.QTableAutomationDetails;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -85,6 +86,8 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
|
|||||||
private Set<Capability> enabledCapabilities = new HashSet<>();
|
private Set<Capability> enabledCapabilities = new HashSet<>();
|
||||||
private Set<Capability> disabledCapabilities = new HashSet<>();
|
private Set<Capability> disabledCapabilities = new HashSet<>();
|
||||||
|
|
||||||
|
private CacheOf cacheOf;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -1000,4 +1003,38 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
|
|||||||
return (this);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables;
|
|||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -37,6 +39,38 @@ public class UniqueKey
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public UniqueKey()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public UniqueKey(List<String> fieldNames)
|
||||||
|
{
|
||||||
|
this.fieldNames = fieldNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public UniqueKey(String... fieldNames)
|
||||||
|
{
|
||||||
|
this.fieldNames = Arrays.stream(fieldNames).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for fieldNames
|
** Getter for fieldNames
|
||||||
**
|
**
|
||||||
@ -117,4 +151,21 @@ public class UniqueKey
|
|||||||
this.fieldNames.add(fieldName);
|
this.fieldNames.add(fieldName);
|
||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getDescription(QTableMetaData table)
|
||||||
|
{
|
||||||
|
List<String> fieldLabels = new ArrayList<>();
|
||||||
|
|
||||||
|
for(String fieldName : getFieldNames())
|
||||||
|
{
|
||||||
|
fieldLabels.add(table.getField(fieldName).getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.tables.cache;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Meta-data, to assign to a table which is a "cache of" another table.
|
||||||
|
** e.g., a database table that's a "cache of" an api table - we'd have
|
||||||
|
** databaseTable.withCacheOf(sourceTable=apiTable)
|
||||||
|
*******************************************************************************/
|
||||||
|
public class CacheOf
|
||||||
|
{
|
||||||
|
private String sourceTable;
|
||||||
|
private Integer expirationSeconds;
|
||||||
|
private String cachedDateFieldName;
|
||||||
|
private List<CacheUseCase> useCases;
|
||||||
|
|
||||||
|
// private QCodeReference mapper;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for sourceTable
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getSourceTable()
|
||||||
|
{
|
||||||
|
return sourceTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for sourceTable
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setSourceTable(String sourceTable)
|
||||||
|
{
|
||||||
|
this.sourceTable = sourceTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for sourceTable
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheOf withSourceTable(String sourceTable)
|
||||||
|
{
|
||||||
|
this.sourceTable = sourceTable;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for expirationSeconds
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Integer getExpirationSeconds()
|
||||||
|
{
|
||||||
|
return expirationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for expirationSeconds
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setExpirationSeconds(Integer expirationSeconds)
|
||||||
|
{
|
||||||
|
this.expirationSeconds = expirationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for expirationSeconds
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheOf withExpirationSeconds(Integer expirationSeconds)
|
||||||
|
{
|
||||||
|
this.expirationSeconds = expirationSeconds;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for cachedDateFieldName
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getCachedDateFieldName()
|
||||||
|
{
|
||||||
|
return cachedDateFieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for cachedDateFieldName
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setCachedDateFieldName(String cachedDateFieldName)
|
||||||
|
{
|
||||||
|
this.cachedDateFieldName = cachedDateFieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for cachedDateFieldName
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheOf withCachedDateFieldName(String cachedDateFieldName)
|
||||||
|
{
|
||||||
|
this.cachedDateFieldName = cachedDateFieldName;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for useCases
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<CacheUseCase> getUseCases()
|
||||||
|
{
|
||||||
|
return useCases;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for useCases
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setUseCases(List<CacheUseCase> useCases)
|
||||||
|
{
|
||||||
|
this.useCases = useCases;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for useCases
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheOf withUseCases(List<CacheUseCase> useCases)
|
||||||
|
{
|
||||||
|
this.useCases = useCases;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for useCases
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheOf withUseCase(CacheUseCase useCase)
|
||||||
|
{
|
||||||
|
if(this.useCases == null)
|
||||||
|
{
|
||||||
|
this.useCases = new ArrayList<>();
|
||||||
|
}
|
||||||
|
this.useCases.add(useCase);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.tables.cache;
|
||||||
|
|
||||||
|
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class CacheUseCase
|
||||||
|
{
|
||||||
|
public enum Type
|
||||||
|
{
|
||||||
|
PRIMARY_KEY_TO_PRIMARY_KEY, // e.g., the primary key in the cache table equals the primary key in the source table.
|
||||||
|
UNIQUE_KEY_TO_PRIMARY_KEY, // e.g., a unique key in the cache table equals the primary key in the source table.
|
||||||
|
UNIQUE_KEY_TO_UNIQUE_KEY // e..g, a unique key in the cache table equals a unique key in the source table.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private boolean cacheSourceMisses = false; // whether or not, if a "miss" happens in the SOURCE, if that fact gets cached.
|
||||||
|
|
||||||
|
//////////////////////////
|
||||||
|
// for UNIQUE_KEY types //
|
||||||
|
//////////////////////////
|
||||||
|
private UniqueKey cacheUniqueKey;
|
||||||
|
private UniqueKey sourceUniqueKey;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Type getType()
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setType(Type type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheUseCase withType(Type type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for cacheSourceMisses
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean getCacheSourceMisses()
|
||||||
|
{
|
||||||
|
return cacheSourceMisses;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for cacheSourceMisses
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setCacheSourceMisses(boolean cacheSourceMisses)
|
||||||
|
{
|
||||||
|
this.cacheSourceMisses = cacheSourceMisses;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for cacheSourceMisses
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheUseCase withCacheSourceMisses(boolean cacheSourceMisses)
|
||||||
|
{
|
||||||
|
this.cacheSourceMisses = cacheSourceMisses;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for cacheUniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public UniqueKey getCacheUniqueKey()
|
||||||
|
{
|
||||||
|
return cacheUniqueKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for cacheUniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setCacheUniqueKey(UniqueKey cacheUniqueKey)
|
||||||
|
{
|
||||||
|
this.cacheUniqueKey = cacheUniqueKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for cacheUniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheUseCase withCacheUniqueKey(UniqueKey cacheUniqueKey)
|
||||||
|
{
|
||||||
|
this.cacheUniqueKey = cacheUniqueKey;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for sourceUniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public UniqueKey getSourceUniqueKey()
|
||||||
|
{
|
||||||
|
return sourceUniqueKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for sourceUniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setSourceUniqueKey(UniqueKey sourceUniqueKey)
|
||||||
|
{
|
||||||
|
this.sourceUniqueKey = sourceUniqueKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for sourceUniqueKey
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public CacheUseCase withSourceUniqueKey(UniqueKey sourceUniqueKey)
|
||||||
|
{
|
||||||
|
this.sourceUniqueKey = sourceUniqueKey;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
|
||||||
|
|
||||||
|
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class MemoryTableBackendDetails extends QTableBackendDetails
|
||||||
|
{
|
||||||
|
private boolean cloneUponStore = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for cloneUponStore
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean getCloneUponStore()
|
||||||
|
{
|
||||||
|
return cloneUponStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for cloneUponStore
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setCloneUponStore(boolean cloneUponStore)
|
||||||
|
{
|
||||||
|
this.cloneUponStore = cloneUponStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for cloneUponStore
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public MemoryTableBackendDetails withCloneUponStore(boolean cloneUponStore)
|
||||||
|
{
|
||||||
|
this.cloneUponStore = cloneUponStore;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.AbstractTransformStep;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
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.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -176,16 +175,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private Set<List<Serializable>> getExistingKeys(RunBackendStepInput runBackendStepInput, UniqueKey uniqueKey) throws QException
|
private Set<List<Serializable>> getExistingKeys(RunBackendStepInput runBackendStepInput, UniqueKey uniqueKey) throws QException
|
||||||
{
|
{
|
||||||
return (getExistingKeys(runBackendStepInput, uniqueKey.getFieldNames()));
|
List<String> ukFieldNames = uniqueKey.getFieldNames();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private Set<List<Serializable>> getExistingKeys(RunBackendStepInput runBackendStepInput, List<String> ukFieldNames) throws QException
|
|
||||||
{
|
|
||||||
Set<List<Serializable>> existingRecords = new HashSet<>();
|
Set<List<Serializable>> existingRecords = new HashSet<>();
|
||||||
if(ukFieldNames != null)
|
if(ukFieldNames != null)
|
||||||
{
|
{
|
||||||
@ -276,7 +266,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
|||||||
{
|
{
|
||||||
UniqueKey uniqueKey = entry.getKey();
|
UniqueKey uniqueKey = entry.getKey();
|
||||||
ProcessSummaryLine ukErrorSummary = entry.getValue();
|
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
|
ukErrorSummary
|
||||||
.withSingularFutureMessage(tableLabel + " record will not be" + ukErrorSuffix)
|
.withSingularFutureMessage(tableLabel + " record will not be" + ukErrorSuffix)
|
||||||
@ -290,21 +280,4 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
|||||||
return (rs);
|
return (rs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private String getUkDescription(List<String> ukFieldNames)
|
|
||||||
{
|
|
||||||
List<String> fieldLabels = new ArrayList<>();
|
|
||||||
|
|
||||||
for(String fieldName : ukFieldNames)
|
|
||||||
{
|
|
||||||
fieldLabels.add(table.getField(fieldName).getLabel());
|
|
||||||
}
|
|
||||||
|
|
||||||
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -405,8 +405,10 @@ public class GeneralProcessUtils
|
|||||||
** get that record id.
|
** 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 //
|
// Get the selected recordId and verify we only 1 //
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
|
@ -69,8 +69,9 @@ class ChildRecordListRendererTest
|
|||||||
void testParentRecordNotFound() throws QException
|
void testParentRecordNotFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
RenderWidgetInput input = new RenderWidgetInput(qInstance);
|
RenderWidgetInput input = new RenderWidgetInput(qInstance);
|
||||||
@ -92,8 +93,9 @@ class ChildRecordListRendererTest
|
|||||||
void testNoChildRecordsFound() throws QException
|
void testNoChildRecordsFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
||||||
@ -122,8 +124,9 @@ class ChildRecordListRendererTest
|
|||||||
void testChildRecordsFound() throws QException
|
void testChildRecordsFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
||||||
|
@ -117,8 +117,9 @@ class ParentWidgetRendererTest
|
|||||||
void testNoChildRecordsFound() throws QException
|
void testNoChildRecordsFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
||||||
@ -147,8 +148,9 @@ class ParentWidgetRendererTest
|
|||||||
void testChildRecordsFound() throws QException
|
void testChildRecordsFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
||||||
|
@ -103,8 +103,9 @@ class ProcessWidgetRendererTest
|
|||||||
void testNoChildRecordsFound() throws QException
|
void testNoChildRecordsFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
||||||
@ -133,8 +134,9 @@ class ProcessWidgetRendererTest
|
|||||||
void testChildRecordsFound() throws QException
|
void testChildRecordsFound() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem"))
|
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem"))
|
||||||
.withLabel("Line Items");
|
.withLabel("Line Items")
|
||||||
|
.getWidgetMetaData();
|
||||||
qInstance.addWidget(widget);
|
qInstance.addWidget(widget);
|
||||||
|
|
||||||
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of(
|
||||||
|
@ -22,12 +22,28 @@
|
|||||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
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.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.GetInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
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 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 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.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -37,6 +53,19 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||||||
class GetActionTest
|
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
|
** 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.
|
** mock implementation - just confirming that all of the "wiring" works.
|
||||||
@ -55,4 +84,164 @@ class GetActionTest
|
|||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
assertNotNull(result.getRecord());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1180,8 +1180,7 @@ class QInstanceValidatorTest
|
|||||||
void testValidUniqueKeys()
|
void testValidUniqueKeys()
|
||||||
{
|
{
|
||||||
assertValidationSuccess((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
assertValidationSuccess((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||||
.withUniqueKey(new UniqueKey().withFieldName("id"))
|
.withUniqueKey(new UniqueKey().withFieldName("id")));
|
||||||
.withUniqueKey(new UniqueKey().withFieldName("firstName").withFieldName("lastName")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.QReportView;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
|
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.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.AutomationStatusTracking;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
|
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.QTableAutomationDetails;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
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.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.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
|
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.authentication.metadata.QAuthenticationMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
|
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.modules.backend.implementations.mock.MockBackendModule;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
|
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
|
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 PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport";
|
||||||
public static final String TABLE_NAME_PERSON_FILE = "personFile";
|
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 = "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_ID_AND_NAME_ONLY = "idAndNameOnly";
|
||||||
public static final String TABLE_NAME_BASEPULL = "basepullTest";
|
public static final String TABLE_NAME_BASEPULL = "basepullTest";
|
||||||
public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport";
|
public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport";
|
||||||
@ -164,6 +169,7 @@ public class TestUtils
|
|||||||
qInstance.addTable(defineTablePerson());
|
qInstance.addTable(defineTablePerson());
|
||||||
qInstance.addTable(definePersonFileTable());
|
qInstance.addTable(definePersonFileTable());
|
||||||
qInstance.addTable(definePersonMemoryTable());
|
qInstance.addTable(definePersonMemoryTable());
|
||||||
|
qInstance.addTable(definePersonMemoryCacheTable());
|
||||||
qInstance.addTable(defineTableIdAndNameOnly());
|
qInstance.addTable(defineTableIdAndNameOnly());
|
||||||
qInstance.addTable(defineTableShape());
|
qInstance.addTable(defineTableShape());
|
||||||
qInstance.addTable(defineTableBasepull());
|
qInstance.addTable(defineTableBasepull());
|
||||||
@ -608,6 +614,7 @@ public class TestUtils
|
|||||||
.withName(TABLE_NAME_PERSON_MEMORY)
|
.withName(TABLE_NAME_PERSON_MEMORY)
|
||||||
.withBackendName(MEMORY_BACKEND_NAME)
|
.withBackendName(MEMORY_BACKEND_NAME)
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
|
.withUniqueKey(new UniqueKey("firstName", "lastName"))
|
||||||
.withFields(TestUtils.defineTablePerson().getFields()))
|
.withFields(TestUtils.defineTablePerson().getFields()))
|
||||||
|
|
||||||
.withField(standardQqqAutomationStatusField())
|
.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)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -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.SleepUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
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.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.APIBackendMetaData;
|
||||||
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
|
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.HttpStatus;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
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.entity.StringEntity;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
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.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@ -122,7 +130,9 @@ public class BaseAPIActionUtil
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
String urlSuffix = buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey());
|
String urlSuffix = getInput.getPrimaryKey() != null
|
||||||
|
? buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey())
|
||||||
|
: buildUrlSuffixForSingleRecordGet(getInput.getUniqueKey());
|
||||||
String url = buildTableUrl(table);
|
String url = buildTableUrl(table);
|
||||||
HttpGet request = new HttpGet(url + urlSuffix);
|
HttpGet request = new HttpGet(url + urlSuffix);
|
||||||
|
|
||||||
@ -468,6 +478,8 @@ public class BaseAPIActionUtil
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
protected void handleResponseError(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws QException
|
protected void handleResponseError(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws QException
|
||||||
{
|
{
|
||||||
|
checkForOAuthExpiredToken(table, request, response);
|
||||||
|
|
||||||
int statusCode = response.getStatusCode();
|
int statusCode = response.getStatusCode();
|
||||||
String resultString = response.getContent();
|
String resultString = response.getContent();
|
||||||
String errorMessage = "HTTP " + request.getMethod() + " for table [" + table.getName() + "] failed with status " + statusCode + ": " + resultString;
|
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
|
** method to build up a query string based on a given QFilter object
|
||||||
**
|
**
|
||||||
@ -511,13 +539,29 @@ public class BaseAPIActionUtil
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String buildUrlSuffixForSingleRecordGet(Map<String, Serializable> uniqueKey) throws QException
|
||||||
|
{
|
||||||
|
QTableMetaData table = actionInput.getTable();
|
||||||
|
QQueryFilter filter = new QQueryFilter();
|
||||||
|
for(Map.Entry<String, Serializable> 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
|
** As part of making a request - set up its authorization header (not just
|
||||||
** strictly "Authorization", but whatever is needed for auth).
|
** strictly "Authorization", but whatever is needed for auth).
|
||||||
**
|
**
|
||||||
** Can be overridden if an API uses an authorization type we don't natively support.
|
** 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())
|
switch(backendMetaData.getAuthorizationType())
|
||||||
{
|
{
|
||||||
@ -533,6 +577,10 @@ public class BaseAPIActionUtil
|
|||||||
request.addHeader("API-Key", backendMetaData.getApiKey());
|
request.addHeader("API-Key", backendMetaData.getApiKey());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case OAUTH2:
|
||||||
|
request.setHeader("Authorization", "Bearer " + getOAuth2Token());
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType());
|
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.
|
** As part of making a request - set up its content-type header.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -729,6 +837,7 @@ public class BaseAPIActionUtil
|
|||||||
{
|
{
|
||||||
int sleepMillis = getInitialRateLimitBackoffMillis();
|
int sleepMillis = getInitialRateLimitBackoffMillis();
|
||||||
int rateLimitsCaught = 0;
|
int rateLimitsCaught = 0;
|
||||||
|
boolean caughtAnOAuthExpiredToken = false;
|
||||||
|
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
@ -768,6 +877,25 @@ public class BaseAPIActionUtil
|
|||||||
return (qResponse);
|
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)
|
catch(RateLimitException rle)
|
||||||
{
|
{
|
||||||
rateLimitsCaught++;
|
rateLimitsCaught++;
|
||||||
@ -781,6 +909,13 @@ public class BaseAPIActionUtil
|
|||||||
SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS);
|
SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS);
|
||||||
sleepMillis *= 2;
|
sleepMillis *= 2;
|
||||||
}
|
}
|
||||||
|
catch(QException qe)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// re-throw exceptions that QQQ or application code produced //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
throw (qe);
|
||||||
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
String message = "An unknown error occurred trying to make an HTTP request to [" + request.getURI() + "] on table [" + table.getName() + "].";
|
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()
|
protected int getInitialRateLimitBackoffMillis()
|
||||||
{
|
{
|
||||||
return (0);
|
return (500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -916,7 +1051,7 @@ public class BaseAPIActionUtil
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
protected int getMaxAllowedRateLimitErrors()
|
protected int getMaxAllowedRateLimitErrors()
|
||||||
{
|
{
|
||||||
return (0);
|
return (3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.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);
|
||||||
|
}
|
||||||
|
}
|
@ -22,10 +22,13 @@
|
|||||||
package com.kingsrook.qqq.backend.module.api.exceptions;
|
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
|
||||||
{
|
{
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
@ -30,5 +30,6 @@ public enum AuthorizationType
|
|||||||
API_KEY_HEADER,
|
API_KEY_HEADER,
|
||||||
BASIC_AUTH_API_KEY,
|
BASIC_AUTH_API_KEY,
|
||||||
BASIC_AUTH_USERNAME_PASSWORD,
|
BASIC_AUTH_USERNAME_PASSWORD,
|
||||||
|
OAUTH2,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@ public class APIBackendMetaData extends QBackendMetaData
|
|||||||
{
|
{
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
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
|
** Getter for username
|
||||||
**
|
**
|
||||||
@ -391,4 +461,16 @@ public class APIBackendMetaData extends QBackendMetaData
|
|||||||
{
|
{
|
||||||
qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName());
|
qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public boolean requiresPrimaryKeyOnTables()
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -414,7 +414,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
|||||||
case IS_BLANK:
|
case IS_BLANK:
|
||||||
{
|
{
|
||||||
clause += " IS NULL";
|
clause += " IS NULL";
|
||||||
if(isString(field.getType()))
|
if(field.getType().isStringLike())
|
||||||
{
|
{
|
||||||
clause += " OR " + column + " = ''";
|
clause += " OR " + column + " = ''";
|
||||||
}
|
}
|
||||||
@ -424,7 +424,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
|||||||
case IS_NOT_BLANK:
|
case IS_NOT_BLANK:
|
||||||
{
|
{
|
||||||
clause += " IS NOT NULL";
|
clause += " IS NOT NULL";
|
||||||
if(isString(field.getType()))
|
if(field.getType().isStringLike())
|
||||||
{
|
{
|
||||||
clause += " AND " + column + " != ''";
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -136,7 +136,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
|
|||||||
// execute the query - iterate over results //
|
// execute the query - iterate over results //
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
QueryOutput queryOutput = new QueryOutput(queryInput);
|
QueryOutput queryOutput = new QueryOutput(queryInput);
|
||||||
System.out.println(sql);
|
// System.out.println(sql);
|
||||||
PreparedStatement statement = createStatement(connection, sql.toString(), queryInput);
|
PreparedStatement statement = createStatement(connection, sql.toString(), queryInput);
|
||||||
QueryManager.executeStatement(statement, ((ResultSet resultSet) ->
|
QueryManager.executeStatement(statement, ((ResultSet resultSet) ->
|
||||||
{
|
{
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
#!/usr/bin/env groovy
|
#!/usr/bin/env groovy
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Script to convert a list of columnNames from a CREATE TABLE statement
|
** Script to convert a CREATE TABLE statement to fields for a QRecordEntity
|
||||||
** to fields for a QRecordEntity (on stdout)
|
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
|
if (args.length < 1)
|
||||||
|
{
|
||||||
|
System.out.println("Usage: ${this.class.getSimpleName()} EntityClassName [writeWholeClass] [writeTableMetaData]")
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
String className = args[0]
|
String className = args[0]
|
||||||
boolean writeWholeClass = args.length > 1 ? args[1] : false;
|
boolean writeWholeClass = args.length > 1 ? args[1] : false;
|
||||||
boolean writeTableMetaData = args.length > 2 ? args[2] : 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))
|
def reader = new BufferedReader(new InputStreamReader(System.in))
|
||||||
String line
|
String line
|
||||||
String allFieldNames = ""
|
String allFieldNames = ""
|
||||||
@ -27,7 +33,6 @@ if(writeWholeClass)
|
|||||||
public class %s extends QRecordEntity
|
public class %s extends QRecordEntity
|
||||||
{
|
{
|
||||||
public static final String TABLE_NAME = "%s";
|
public static final String TABLE_NAME = "%s";
|
||||||
|
|
||||||
""".formatted(className, className, classNameLcFirst));
|
""".formatted(className, className, classNameLcFirst));
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue
Block a user