From 060da69afbdd7db3fcec7dfee6ec2a7951a95a2c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 5 Dec 2022 10:24:15 -0600 Subject: [PATCH] Adding table-cacheOf concept; ability to add a child record from child-list widget --- .../dashboard/AbstractHTMLWidgetRenderer.java | 5 +- .../widgets/ChildRecordListRenderer.java | 82 +++++- .../core/actions/interfaces/GetInterface.java | 33 +++ .../core/actions/tables/GetAction.java | 234 ++++++++++++++++-- .../core/instances/QInstanceValidator.java | 121 ++++++++- .../model/actions/tables/get/GetInput.java | 41 ++- .../widgets/ChildRecordListData.java | 74 ++++++ .../qqq/backend/core/model/data/QRecord.java | 11 + .../AbstractWidgetMetaDataBuilder.java | 54 ++++ .../model/metadata/fields/QFieldType.java | 10 + .../model/metadata/tables/QTableMetaData.java | 37 +++ .../core/model/metadata/tables/UniqueKey.java | 51 ++++ .../model/metadata/tables/cache/CacheOf.java | 195 +++++++++++++++ .../metadata/tables/cache/CacheUseCase.java | 187 ++++++++++++++ .../memory/MemoryTableBackendDetails.java | 69 ++++++ .../bulk/insert/BulkInsertTransformStep.java | 31 +-- .../processes/utils/GeneralProcessUtils.java | 4 +- .../widgets/ChildRecordListRendererTest.java | 15 +- .../widgets/ParentWidgetRendererTest.java | 10 +- .../widgets/ProcessWidgetRendererTest.java | 10 +- .../core/actions/tables/GetActionTest.java | 189 ++++++++++++++ .../instances/QInstanceValidatorTest.java | 3 +- .../qqq/backend/core/utils/TestUtils.java | 37 +++ .../module/api/actions/BaseAPIActionUtil.java | 151 ++++++++++- .../exceptions/OAuthCredentialsException.java | 52 ++++ .../OAuthExpiredTokenException.java | 54 ++++ .../api/exceptions/RateLimitException.java | 5 +- .../module/api/model/AuthorizationType.java | 1 + .../model/metadata/APIBackendMetaData.java | 82 ++++++ .../rdbms/actions/AbstractRDBMSAction.java | 14 +- .../rdbms/actions/RDBMSQueryAction.java | 2 +- ...roovy => createTableToRecordEntity.groovy} | 11 +- 32 files changed, 1766 insertions(+), 109 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java rename qqq-dev-tools/bin/{createTableToEntityFields.groovy => createTableToRecordEntity.groovy} (95%) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index a80c095d..9dd03013 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard; import java.io.Serializable; import java.net.URLEncoder; import java.nio.charset.Charset; +import java.util.Objects; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; @@ -48,7 +49,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer protected String openTopLevelBulletList() { return (""" -
+
    """); } @@ -101,7 +102,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer *******************************************************************************/ protected String bulletNameValue(String name, String value) { - return ("
  • " + name + "   " + value + "
  • "); + return ("
  • " + name + "   " + Objects.requireNonNullElse(value, "--") + "
  • "); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java index a5507f42..c2918111 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java @@ -22,9 +22,12 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets; +import java.io.Serializable; import java.net.URLEncoder; import java.nio.charset.Charset; +import java.util.HashMap; import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -42,11 +45,14 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListDat import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.AbstractWidgetMetaDataBuilder; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -58,13 +64,64 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer /******************************************************************************* ** *******************************************************************************/ - public static QWidgetMetaData defineWidgetFromJoin(QJoinMetaData join) + public static Builder widgetMetaDataBuilder(QJoinMetaData join) { - return (new QWidgetMetaData() + return (new Builder(new QWidgetMetaData() .withName(join.getName()) .withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null)) .withType(WidgetType.CHILD_RECORD_LIST.getType()) - .withDefaultValue("joinName", join.getName())); + .withDefaultValue("joinName", join.getName()))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class Builder extends AbstractWidgetMetaDataBuilder + { + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Builder(QWidgetMetaData widgetMetaData) + { + super(widgetMetaData); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Builder withName(String name) + { + widgetMetaData.setName(name); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Builder withLabel(String label) + { + widgetMetaData.setLabel(label); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public AbstractWidgetMetaDataBuilder withCanAddChildRecord(boolean b) + { + widgetMetaData.withDefaultValue("canAddChildRecord", true); + return (this); + } } @@ -119,7 +176,24 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer String tablePath = input.getInstance().getTablePath(input, table.getName()); String viewAllLink = tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()); - return (new RenderWidgetOutput(new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink))); + ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink); + + if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord")))) + { + widgetData.setCanAddChildRecord(true); + + ////////////////////////////////////////////////////////// + // new child records must have values from the join-ons // + ////////////////////////////////////////////////////////// + Map defaultValuesForNewChildRecords = new HashMap<>(); + for(JoinOn joinOn : join.getJoinOns()) + { + defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField())); + } + widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords); + } + + return (new RenderWidgetOutput(widgetData)); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java index 258cd856..aeff3162 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java @@ -22,9 +22,12 @@ package com.kingsrook.qqq.backend.core.actions.interfaces; +import java.util.HashSet; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; /******************************************************************************* @@ -37,4 +40,34 @@ public interface GetInterface ** *******************************************************************************/ GetOutput execute(GetInput getInput) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + default void validateInput(GetInput getInput) throws QException + { + if(getInput.getPrimaryKey() != null & getInput.getUniqueKey() != null) + { + throw new QException("A GetInput may not contain both a primary key [" + getInput.getPrimaryKey() + "] and unique key [" + getInput.getUniqueKey() + "]"); + } + + if(getInput.getUniqueKey() != null) + { + QTableMetaData table = getInput.getTable(); + boolean foundMatch = false; + for(UniqueKey uniqueKey : table.getUniqueKeys()) + { + if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet())) + { + foundMatch = true; + break; + } + } + + if(!foundMatch) + { + throw new QException("Table [" + table.getName() + "] does not have a unique key defined on fields: " + getInput.getUniqueKey().keySet().stream().sorted().toList()); + } + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java index da923b7b..f6162271 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java @@ -22,7 +22,11 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.io.Serializable; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; import com.kingsrook.qqq.backend.core.actions.ActionHelper; @@ -32,16 +36,26 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.commons.lang.NotImplementedException; /******************************************************************************* @@ -65,7 +79,8 @@ public class GetAction { ActionHelper.validateSession(getInput); - postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole()); + QTableMetaData table = getInput.getTable(); + postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(table, TableCustomizers.POST_QUERY_RECORD.getRole()); this.getInput = getInput; QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); @@ -79,22 +94,55 @@ public class GetAction } catch(IllegalStateException ise) { - //////////////////////////////////////////////////////////////////////////////////////////////// - // if a module doesn't implement Get directly - try to do a Get by a Query by the primary key // - // see below. // - //////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if a module doesn't implement Get directly - try to do a Get by a Query in the DefaultGetInterface (inner class) // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// } GetOutput getOutput; - if(getInterface != null) + if(getInterface == null) { - getOutput = getInterface.execute(getInput); - } - else - { - getOutput = performGetViaQuery(getInput); + getInterface = new DefaultGetInterface(); } + getInterface.validateInput(getInput); + getOutput = getInterface.execute(getInput); + + //////////////////////////// + // handle cache use-cases // + //////////////////////////// + if(table.getCacheOf() != null) + { + if(getOutput.getRecord() == null) + { + /////////////////////////////////////////////////////////////////////// + // if the record wasn't found, see if we should look in cache-source // + /////////////////////////////////////////////////////////////////////// + QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput); + if(recordFromSource != null) + { + QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource); + + InsertInput insertInput = new InsertInput(getInput.getInstance()); + insertInput.setSession(getInput.getSession()); + insertInput.setTableName(getInput.getTableName()); + insertInput.setRecords(List.of(recordToCache)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + getOutput.setRecord(insertOutput.getRecords().get(0)); + } + } + else + { + ///////////////////////////////////////////////////////////////////////////////// + // if the record was found, but it's too old, maybe re-fetch from cache source // + ///////////////////////////////////////////////////////////////////////////////// + refreshCacheIfExpired(getInput, getOutput); + } + } + + //////////////////////////////////////////////////////// + // if the record is found, perform post-actions on it // + //////////////////////////////////////////////////////// if(getOutput.getRecord() != null) { getOutput.setRecord(postRecordActions(getOutput.getRecord())); @@ -108,20 +156,162 @@ public class GetAction /******************************************************************************* ** *******************************************************************************/ - private GetOutput performGetViaQuery(GetInput getInput) throws QException + private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource) { - QueryInput queryInput = new QueryInput(getInput.getInstance()); - queryInput.setSession(getInput.getSession()); - queryInput.setTableName(getInput.getTableName()); - queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey())))); - QueryOutput queryOutput = new QueryAction().execute(queryInput); - - GetOutput getOutput = new GetOutput(); - if(!queryOutput.getRecords().isEmpty()) + QRecord cacheRecord = new QRecord(recordFromSource); + if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName())) { - getOutput.setRecord(queryOutput.getRecords().get(0)); + cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now()); + } + return (cacheRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException + { + QTableMetaData table = getInput.getTable(); + Integer expirationSeconds = table.getCacheOf().getExpirationSeconds(); + if(expirationSeconds != null) + { + QRecord cachedRecord = getOutput.getRecord(); + Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName()); + if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS))) + { + QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput); + if(recordFromSource != null) + { + /////////////////////////////////////////////////////////////////// + // if the record was found in the source, update it in the cache // + /////////////////////////////////////////////////////////////////// + QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource); + recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField())); + + UpdateInput updateInput = new UpdateInput(getInput.getInstance()); + updateInput.setSession(getInput.getSession()); + updateInput.setTableName(getInput.getTableName()); + updateInput.setRecords(List.of(recordToCache)); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + + getOutput.setRecord(updateOutput.getRecords().get(0)); + } + else + { + ///////////////////////////////////////////////////////////////////////////// + // if the record is no longer in the source, then remove it from the cache // + ///////////////////////////////////////////////////////////////////////////// + DeleteInput deleteInput = new DeleteInput(getInput.getInstance()); + deleteInput.setSession(getInput.getSession()); + deleteInput.setTableName(getInput.getTableName()); + deleteInput.setPrimaryKeys(List.of(getOutput.getRecord().getValue(table.getPrimaryKeyField()))); + new DeleteAction().execute(deleteInput); + + getOutput.setRecord(null); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QRecord tryToGetFromCacheSource(GetInput getInput, GetOutput getOutput) throws QException + { + QRecord recordFromSource = null; + QTableMetaData table = getInput.getTable(); + + for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases())) + { + if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null) + { + recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable()); + break; + } + else + { + // todo!! + throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType()); + } + } + + return (recordFromSource); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException + { + ///////////////////////////////////////////////////// + // do a Get on the source table, by the unique key // + ///////////////////////////////////////////////////// + GetInput sourceGetInput = new GetInput(getInput.getInstance()); + sourceGetInput.setSession(getInput.getSession()); + sourceGetInput.setTableName(sourceTableName); + sourceGetInput.setUniqueKey(getInput.getUniqueKey()); + GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput); + return (sourceGetOutput.getRecord()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static class DefaultGetInterface implements GetInterface + { + @Override + public GetOutput execute(GetInput getInput) throws QException + { + QueryInput queryInput = new QueryInput(getInput.getInstance()); + queryInput.setSession(getInput.getSession()); + queryInput.setTableName(getInput.getTableName()); + + ////////////////////////////////////////////////// + // build filter using either pkey or unique key // + ////////////////////////////////////////////////// + QQueryFilter filter = new QQueryFilter(); + if(getInput.getPrimaryKey() != null) + { + filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey())); + } + else if(getInput.getUniqueKey() != null) + { + for(Map.Entry entry : getInput.getUniqueKey().entrySet()) + { + if(entry.getValue() == null) + { + filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK)); + } + else + { + filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue())); + } + } + } + else + { + throw (new QException("No primaryKey or uniqueKey was passed to Get")); + } + + queryInput.setFilter(filter); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + GetOutput getOutput = new GetOutput(); + if(!queryOutput.getRecords().isEmpty()) + { + getOutput.setRecord(queryOutput.getRecords().get(0)); + } + return (getOutput); } - return (getOutput); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index ee58535c..2a76e5b7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -48,6 +48,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; @@ -65,6 +67,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; @@ -131,6 +135,7 @@ public class QInstanceValidator validateApps(qInstance); validatePossibleValueSources(qInstance); validateQueuesAndProviders(qInstance); + validateJoins(qInstance); validateUniqueTopLevelNames(qInstance); } @@ -149,6 +154,43 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validateJoins(QInstance qInstance) + { + qInstance.getJoins().forEach((joinName, join) -> + { + assertCondition(Objects.equals(joinName, join.getName()), "Inconsistent naming for join: " + joinName + "/" + join.getName() + "."); + + assertCondition(StringUtils.hasContent(join.getLeftTable()), "Missing left-table name in join: " + joinName); + assertCondition(StringUtils.hasContent(join.getRightTable()), "Missing right-table name in join: " + joinName); + assertCondition(join.getType() != null, "Missing type for join: " + joinName); + assertCondition(CollectionUtils.nullSafeHasContents(join.getJoinOns()), "Missing joinOns for join: " + joinName); + + boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " join " + joinName + " is not a defined table in this instance."); + boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " join " + joinName + " is not a defined table in this instance."); + + for(JoinOn joinOn : CollectionUtils.nonNullList(join.getJoinOns())) + { + assertCondition(StringUtils.hasContent(joinOn.getLeftField()), "Missing left-field name in a joinOn for join: " + joinName); + assertCondition(StringUtils.hasContent(joinOn.getRightField()), "Missing right-field name in a joinOn for join: " + joinName); + + if(leftTableExists) + { + assertNoException(() -> qInstance.getTable(join.getLeftTable()).getField(joinOn.getLeftField()), "Left field name in joinOn " + joinName + " is not a defined field in table " + join.getLeftTable()); + } + + if(rightTableExists) + { + assertNoException(() -> qInstance.getTable(join.getRightTable()).getField(joinOn.getRightField()), "Right field name in joinOn " + joinName + " is not a defined field in table " + join.getRightTable()); + } + } + }); + } + + + /******************************************************************************* ** there can be some unexpected bad-times if you have a table and process, or ** table and app (etc) with the same name (e.g., in app tree building). So, @@ -305,14 +347,7 @@ public class QInstanceValidator { table.getFields().forEach((fieldName, field) -> { - assertCondition(Objects.equals(fieldName, field.getName()), - "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); - - if(field.getPossibleValueSourceName() != null) - { - assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, - "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); - } + validateTableField(qInstance, tableName, fieldName, field); }); } @@ -392,12 +427,82 @@ public class QInstanceValidator { validateAssociatedScripts(table); } + + ////////////////////// + // validate cacheOf // + ////////////////////// + if(table.getCacheOf() != null) + { + validateTableCacheOf(qInstance, table); + } }); } } + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableField(QInstance qInstance, String tableName, String fieldName, QFieldMetaData field) + { + assertCondition(Objects.equals(fieldName, field.getName()), + "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); + + if(field.getPossibleValueSourceName() != null) + { + assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, + "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); + } + + ValueTooLongBehavior behavior = field.getBehavior(qInstance, ValueTooLongBehavior.class); + if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH)) + { + assertCondition(field.getMaxLength() != null, "Field " + fieldName + " in table " + tableName + " specifies a ValueTooLongBehavior, but not a maxLength."); + } + + if(field.getMaxLength() != null) + { + assertCondition(field.getMaxLength() > 0, "Field " + fieldName + " in table " + tableName + " has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0."); + assertCondition(field.getType().isStringLike(), "Field " + fieldName + " in table " + tableName + " has maxLength, but is not of a supported type (" + field.getType() + ") - must be a string-like type."); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableCacheOf(QInstance qInstance, QTableMetaData table) + { + CacheOf cacheOf = table.getCacheOf(); + String prefix = "Table " + table.getName() + " cacheOf "; + String sourceTableName = cacheOf.getSourceTable(); + if(assertCondition(StringUtils.hasContent(sourceTableName), prefix + "is missing a sourceTable name")) + { + assertCondition(qInstance.getTable(sourceTableName) != null, prefix + "is referencing an unknown sourceTable: " + sourceTableName); + + boolean hasExpirationSeconds = cacheOf.getExpirationSeconds() != null; + boolean hasCacheDateFieldName = StringUtils.hasContent(cacheOf.getCachedDateFieldName()); + assertCondition(hasExpirationSeconds && hasCacheDateFieldName || (!hasExpirationSeconds && !hasCacheDateFieldName), prefix + "is missing either expirationSeconds or cachedDateFieldName (must either have both, or neither.)"); + + if(hasCacheDateFieldName) + { + assertNoException(() -> table.getField(cacheOf.getCachedDateFieldName()), prefix + "cachedDateFieldName " + cacheOf.getCachedDateFieldName() + " is an unrecognized field."); + } + + if(assertCondition(CollectionUtils.nullSafeHasContents(cacheOf.getUseCases()), prefix + "does not have any useCases defined.")) + { + for(CacheUseCase useCase : cacheOf.getUseCases()) + { + assertCondition(useCase.getType() != null, prefix + "has a useCase without a type."); + } + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java index e08e3f22..ccb3bb1f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get; import java.io.Serializable; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -36,7 +37,9 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; public class GetInput extends AbstractTableActionInput { private QBackendTransaction transaction; - private Serializable primaryKey; + + private Serializable primaryKey; + private Map uniqueKey; private boolean shouldTranslatePossibleValues = false; private boolean shouldGenerateDisplayValues = false; @@ -104,7 +107,41 @@ public class GetInput extends AbstractTableActionInput this.primaryKey = primaryKey; return (this); } - + + + + /******************************************************************************* + ** Getter for uniqueKey + ** + *******************************************************************************/ + public Map getUniqueKey() + { + return uniqueKey; + } + + + + /******************************************************************************* + ** Setter for uniqueKey + ** + *******************************************************************************/ + public void setUniqueKey(Map uniqueKey) + { + this.uniqueKey = uniqueKey; + } + + + + /******************************************************************************* + ** Fluent setter for uniqueKey + ** + *******************************************************************************/ + public GetInput withUniqueKey(Map uniqueKey) + { + this.uniqueKey = uniqueKey; + return (this); + } + /******************************************************************************* diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java index 75ddbf4d..86339e57 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets; +import java.io.Serializable; +import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -39,6 +41,9 @@ public class ChildRecordListData implements QWidget private String tablePath; private String viewAllLink; + private boolean canAddChildRecord = true; + private Map defaultValuesForNewChildRecords; + /******************************************************************************* @@ -209,4 +214,73 @@ public class ChildRecordListData implements QWidget { this.viewAllLink = viewAllLink; } + + + + /******************************************************************************* + ** Getter for canAddChildRecord + ** + *******************************************************************************/ + public boolean getCanAddChildRecord() + { + return canAddChildRecord; + } + + + + /******************************************************************************* + ** Setter for canAddChildRecord + ** + *******************************************************************************/ + public void setCanAddChildRecord(boolean canAddChildRecord) + { + this.canAddChildRecord = canAddChildRecord; + } + + + + /******************************************************************************* + ** Fluent setter for canAddChildRecord + ** + *******************************************************************************/ + public ChildRecordListData withCanAddChildRecord(boolean canAddChildRecord) + { + this.canAddChildRecord = canAddChildRecord; + return (this); + } + + + + /******************************************************************************* + ** Getter for defaultValuesForNewChildRecords + ** + *******************************************************************************/ + public Map getDefaultValuesForNewChildRecords() + { + return defaultValuesForNewChildRecords; + } + + + + /******************************************************************************* + ** Setter for defaultValuesForNewChildRecords + ** + *******************************************************************************/ + public void setDefaultValuesForNewChildRecords(Map defaultValuesForNewChildRecords) + { + this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords; + } + + + + /******************************************************************************* + ** Fluent setter for defaultValuesForNewChildRecords + ** + *******************************************************************************/ + public ChildRecordListData withDefaultValuesForNewChildRecords(Map defaultValuesForNewChildRecords) + { + this.defaultValuesForNewChildRecords = defaultValuesForNewChildRecords; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 308f0b7c..e5fc5505 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; import java.util.ArrayList; @@ -416,6 +417,16 @@ public class QRecord implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(values.get(fieldName))); + } + + + /******************************************************************************* ** Getter for backendDetails ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java new file mode 100644 index 00000000..24b5a06b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/AbstractWidgetMetaDataBuilder.java @@ -0,0 +1,54 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.dashboard; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AbstractWidgetMetaDataBuilder +{ + protected QWidgetMetaData widgetMetaData; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AbstractWidgetMetaDataBuilder(QWidgetMetaData widgetMetaData) + { + this.widgetMetaData = widgetMetaData; + } + + + + /******************************************************************************* + ** Getter for widgetMetaData + ** + *******************************************************************************/ + public QWidgetMetaData getWidgetMetaData() + { + return widgetMetaData; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 41ffdcc4..10f6d567 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -88,4 +88,14 @@ public enum QFieldType throw (new QException("Unrecognized class [" + c + "]")); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public boolean isStringLike() + { + return this == QFieldType.STRING || this == QFieldType.TEXT || this == QFieldType.HTML || this == QFieldType.PASSWORD; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 61a007cf..1530576c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; /******************************************************************************* @@ -85,6 +86,8 @@ public class QTableMetaData implements QAppChildMetaData, Serializable private Set enabledCapabilities = new HashSet<>(); private Set disabledCapabilities = new HashSet<>(); + private CacheOf cacheOf; + /******************************************************************************* @@ -1000,4 +1003,38 @@ public class QTableMetaData implements QAppChildMetaData, Serializable return (this); } + + + /******************************************************************************* + ** Getter for cacheOf + ** + *******************************************************************************/ + public CacheOf getCacheOf() + { + return cacheOf; + } + + + + /******************************************************************************* + ** Setter for cacheOf + ** + *******************************************************************************/ + public void setCacheOf(CacheOf cacheOf) + { + this.cacheOf = cacheOf; + } + + + + /******************************************************************************* + ** Fluent setter for cacheOf + ** + *******************************************************************************/ + public QTableMetaData withCacheOf(CacheOf cacheOf) + { + this.cacheOf = cacheOf; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java index 698e7417..a2c1204c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java @@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -37,6 +39,38 @@ public class UniqueKey + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public UniqueKey() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public UniqueKey(List fieldNames) + { + this.fieldNames = fieldNames; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public UniqueKey(String... fieldNames) + { + this.fieldNames = Arrays.stream(fieldNames).toList(); + } + + + /******************************************************************************* ** Getter for fieldNames ** @@ -117,4 +151,21 @@ public class UniqueKey this.fieldNames.add(fieldName); return (this); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getDescription(QTableMetaData table) + { + List fieldLabels = new ArrayList<>(); + + for(String fieldName : getFieldNames()) + { + fieldLabels.add(table.getField(fieldName).getLabel()); + } + + return (StringUtils.joinWithCommasAndAnd(fieldLabels)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java new file mode 100644 index 00000000..c7359c85 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheOf.java @@ -0,0 +1,195 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.tables.cache; + + +import java.util.ArrayList; +import java.util.List; + + +/******************************************************************************* + ** Meta-data, to assign to a table which is a "cache of" another table. + ** e.g., a database table that's a "cache of" an api table - we'd have + ** databaseTable.withCacheOf(sourceTable=apiTable) + *******************************************************************************/ +public class CacheOf +{ + private String sourceTable; + private Integer expirationSeconds; + private String cachedDateFieldName; + private List useCases; + + // private QCodeReference mapper; + + + + /******************************************************************************* + ** Getter for sourceTable + ** + *******************************************************************************/ + public String getSourceTable() + { + return sourceTable; + } + + + + /******************************************************************************* + ** Setter for sourceTable + ** + *******************************************************************************/ + public void setSourceTable(String sourceTable) + { + this.sourceTable = sourceTable; + } + + + + /******************************************************************************* + ** Fluent setter for sourceTable + ** + *******************************************************************************/ + public CacheOf withSourceTable(String sourceTable) + { + this.sourceTable = sourceTable; + return (this); + } + + + + /******************************************************************************* + ** Getter for expirationSeconds + ** + *******************************************************************************/ + public Integer getExpirationSeconds() + { + return expirationSeconds; + } + + + + /******************************************************************************* + ** Setter for expirationSeconds + ** + *******************************************************************************/ + public void setExpirationSeconds(Integer expirationSeconds) + { + this.expirationSeconds = expirationSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for expirationSeconds + ** + *******************************************************************************/ + public CacheOf withExpirationSeconds(Integer expirationSeconds) + { + this.expirationSeconds = expirationSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for cachedDateFieldName + ** + *******************************************************************************/ + public String getCachedDateFieldName() + { + return cachedDateFieldName; + } + + + + /******************************************************************************* + ** Setter for cachedDateFieldName + ** + *******************************************************************************/ + public void setCachedDateFieldName(String cachedDateFieldName) + { + this.cachedDateFieldName = cachedDateFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for cachedDateFieldName + ** + *******************************************************************************/ + public CacheOf withCachedDateFieldName(String cachedDateFieldName) + { + this.cachedDateFieldName = cachedDateFieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for useCases + ** + *******************************************************************************/ + public List getUseCases() + { + return useCases; + } + + + + /******************************************************************************* + ** Setter for useCases + ** + *******************************************************************************/ + public void setUseCases(List useCases) + { + this.useCases = useCases; + } + + + + /******************************************************************************* + ** Fluent setter for useCases + ** + *******************************************************************************/ + public CacheOf withUseCases(List useCases) + { + this.useCases = useCases; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for useCases + ** + *******************************************************************************/ + public CacheOf withUseCase(CacheUseCase useCase) + { + if(this.useCases == null) + { + this.useCases = new ArrayList<>(); + } + this.useCases.add(useCase); + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java new file mode 100644 index 00000000..62def005 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/cache/CacheUseCase.java @@ -0,0 +1,187 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.tables.cache; + + +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class CacheUseCase +{ + public enum Type + { + PRIMARY_KEY_TO_PRIMARY_KEY, // e.g., the primary key in the cache table equals the primary key in the source table. + UNIQUE_KEY_TO_PRIMARY_KEY, // e.g., a unique key in the cache table equals the primary key in the source table. + UNIQUE_KEY_TO_UNIQUE_KEY // e..g, a unique key in the cache table equals a unique key in the source table. + } + + + + private Type type; + private boolean cacheSourceMisses = false; // whether or not, if a "miss" happens in the SOURCE, if that fact gets cached. + + ////////////////////////// + // for UNIQUE_KEY types // + ////////////////////////// + private UniqueKey cacheUniqueKey; + private UniqueKey sourceUniqueKey; + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public Type getType() + { + return type; + } + + + + /******************************************************************************* + ** Setter for type + ** + *******************************************************************************/ + public void setType(Type type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + ** + *******************************************************************************/ + public CacheUseCase withType(Type type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** Getter for cacheSourceMisses + ** + *******************************************************************************/ + public boolean getCacheSourceMisses() + { + return cacheSourceMisses; + } + + + + /******************************************************************************* + ** Setter for cacheSourceMisses + ** + *******************************************************************************/ + public void setCacheSourceMisses(boolean cacheSourceMisses) + { + this.cacheSourceMisses = cacheSourceMisses; + } + + + + /******************************************************************************* + ** Fluent setter for cacheSourceMisses + ** + *******************************************************************************/ + public CacheUseCase withCacheSourceMisses(boolean cacheSourceMisses) + { + this.cacheSourceMisses = cacheSourceMisses; + return (this); + } + + + + /******************************************************************************* + ** Getter for cacheUniqueKey + ** + *******************************************************************************/ + public UniqueKey getCacheUniqueKey() + { + return cacheUniqueKey; + } + + + + /******************************************************************************* + ** Setter for cacheUniqueKey + ** + *******************************************************************************/ + public void setCacheUniqueKey(UniqueKey cacheUniqueKey) + { + this.cacheUniqueKey = cacheUniqueKey; + } + + + + /******************************************************************************* + ** Fluent setter for cacheUniqueKey + ** + *******************************************************************************/ + public CacheUseCase withCacheUniqueKey(UniqueKey cacheUniqueKey) + { + this.cacheUniqueKey = cacheUniqueKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for sourceUniqueKey + ** + *******************************************************************************/ + public UniqueKey getSourceUniqueKey() + { + return sourceUniqueKey; + } + + + + /******************************************************************************* + ** Setter for sourceUniqueKey + ** + *******************************************************************************/ + public void setSourceUniqueKey(UniqueKey sourceUniqueKey) + { + this.sourceUniqueKey = sourceUniqueKey; + } + + + + /******************************************************************************* + ** Fluent setter for sourceUniqueKey + ** + *******************************************************************************/ + public CacheUseCase withSourceUniqueKey(UniqueKey sourceUniqueKey) + { + this.sourceUniqueKey = sourceUniqueKey; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java new file mode 100644 index 00000000..c3726fb9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryTableBackendDetails.java @@ -0,0 +1,69 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MemoryTableBackendDetails extends QTableBackendDetails +{ + private boolean cloneUponStore = false; + + + + /******************************************************************************* + ** Getter for cloneUponStore + ** + *******************************************************************************/ + public boolean getCloneUponStore() + { + return cloneUponStore; + } + + + + /******************************************************************************* + ** Setter for cloneUponStore + ** + *******************************************************************************/ + public void setCloneUponStore(boolean cloneUponStore) + { + this.cloneUponStore = cloneUponStore; + } + + + + /******************************************************************************* + ** Fluent setter for cloneUponStore + ** + *******************************************************************************/ + public MemoryTableBackendDetails withCloneUponStore(boolean cloneUponStore) + { + this.cloneUponStore = cloneUponStore; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java index ba7912d5..0f9c12ad 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java @@ -48,7 +48,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -176,16 +175,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep *******************************************************************************/ private Set> getExistingKeys(RunBackendStepInput runBackendStepInput, UniqueKey uniqueKey) throws QException { - return (getExistingKeys(runBackendStepInput, uniqueKey.getFieldNames())); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private Set> getExistingKeys(RunBackendStepInput runBackendStepInput, List ukFieldNames) throws QException - { + List ukFieldNames = uniqueKey.getFieldNames(); Set> existingRecords = new HashSet<>(); if(ukFieldNames != null) { @@ -276,7 +266,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep { UniqueKey uniqueKey = entry.getKey(); ProcessSummaryLine ukErrorSummary = entry.getValue(); - String ukErrorSuffix = " inserted, because they contain a duplicate key (" + getUkDescription(uniqueKey.getFieldNames()) + ")"; + String ukErrorSuffix = " inserted, because they contain a duplicate key (" + uniqueKey.getDescription(table) + ")"; ukErrorSummary .withSingularFutureMessage(tableLabel + " record will not be" + ukErrorSuffix) @@ -290,21 +280,4 @@ public class BulkInsertTransformStep extends AbstractTransformStep return (rs); } - - - /******************************************************************************* - ** - *******************************************************************************/ - private String getUkDescription(List ukFieldNames) - { - List fieldLabels = new ArrayList<>(); - - for(String fieldName : ukFieldNames) - { - fieldLabels.add(table.getField(fieldName).getLabel()); - } - - return (StringUtils.joinWithCommasAndAnd(fieldLabels)); - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java index 1ec145b1..9aa75a73 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java @@ -405,8 +405,10 @@ public class GeneralProcessUtils ** get that record id. ** *******************************************************************************/ - public static Integer validateSingleSelectedId(RunBackendStepInput runBackendStepInput, String tableLabel) throws QException + public static Integer validateSingleSelectedId(RunBackendStepInput runBackendStepInput, String tableName) throws QException { + String tableLabel = runBackendStepInput.getInstance().getTable(tableName).getLabel(); + //////////////////////////////////////////////////// // Get the selected recordId and verify we only 1 // //////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java index 60ec3cb0..5775802c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java @@ -69,8 +69,9 @@ class ChildRecordListRendererTest void testParentRecordNotFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); RenderWidgetInput input = new RenderWidgetInput(qInstance); @@ -92,8 +93,9 @@ class ChildRecordListRendererTest void testNoChildRecordsFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( @@ -122,8 +124,9 @@ class ChildRecordListRendererTest void testChildRecordsFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java index 3c927c8d..13a9009e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java @@ -117,8 +117,9 @@ class ParentWidgetRendererTest void testNoChildRecordsFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( @@ -147,8 +148,9 @@ class ParentWidgetRendererTest void testChildRecordsFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java index 97fbbdd8..2f788cf9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java @@ -103,8 +103,9 @@ class ProcessWidgetRendererTest void testNoChildRecordsFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( @@ -133,8 +134,9 @@ class ProcessWidgetRendererTest void testChildRecordsFound() throws QException { QInstance qInstance = TestUtils.defineInstance(); - QWidgetMetaData widget = ChildRecordListRenderer.defineWidgetFromJoin(qInstance.getJoin("orderLineItem")) - .withLabel("Line Items"); + QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) + .withLabel("Line Items") + .getWidgetMetaData(); qInstance.addWidget(widget); TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java index dd576194..71575bd2 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java @@ -22,12 +22,28 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.time.Instant; +import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* @@ -37,6 +53,19 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; class GetActionTest { + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); + } + + + /******************************************************************************* ** At the core level, there isn't much that can be asserted, as it uses the ** mock implementation - just confirming that all of the "wiring" works. @@ -55,4 +84,164 @@ class GetActionTest assertNotNull(result); assertNotNull(result.getRecord()); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUniqueKeyCache() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + + ///////////////////////////////////// + // insert rows in the source table // + ///////////////////////////////////// + TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("firstName", "George").withValue("lastName", "Washington").withValue("noOfShoes", 5), + new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Adams"), + new QRecord().withValue("id", 3).withValue("firstName", "Thomas").withValue("lastName", "Jefferson") + )); + + ///////////////////////////////////////////////////////////////////////////// + // get from the table which caches it - confirm they are (magically) found // + ///////////////////////////////////////////////////////////////////////////// + { + GetInput getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington")); + GetOutput getOutput = new GetAction().execute(getInput); + assertNotNull(getOutput.getRecord()); + assertNotNull(getOutput.getRecord().getValue("cachedDate")); + assertEquals(5, getOutput.getRecord().getValue("noOfShoes")); + } + + /////////////////////////////////////////////////////////////////////////// + // request a row that doesn't exist in cache or source, should miss both // + /////////////////////////////////////////////////////////////////////////// + { + GetInput getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "John", "lastName", "McCain")); + GetOutput getOutput = new GetAction().execute(getInput); + assertNull(getOutput.getRecord()); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // update the record in the source table - then re-get from cache table - shouldn't see new value. // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + { + UpdateInput updateInput = new UpdateInput(qInstance); + updateInput.setSession(new QSession()); + updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 6))); + new UpdateAction().execute(updateInput); + + GetInput getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington")); + GetOutput getOutput = new GetAction().execute(getInput); + assertNotNull(getOutput.getRecord()); + assertNotNull(getOutput.getRecord().getValue("cachedDate")); + assertEquals(5, getOutput.getRecord().getValue("noOfShoes")); + } + + /////////////////////////////////////////////////////////////////////////// + // delete the cached record; re-get, and we should see the updated value // + /////////////////////////////////////////////////////////////////////////// + { + DeleteInput deleteInput = new DeleteInput(qInstance); + deleteInput.setSession(new QSession()); + deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "George"))); + new DeleteAction().execute(deleteInput); + + GetInput getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington")); + GetOutput getOutput = new GetAction().execute(getInput); + assertNotNull(getOutput.getRecord()); + assertNotNull(getOutput.getRecord().getValue("cachedDate")); + assertEquals(6, getOutput.getRecord().getValue("noOfShoes")); + } + + /////////////////////////////////////////////////////////////////// + // update the source record; see that it isn't updated in cache. // + /////////////////////////////////////////////////////////////////// + { + UpdateInput updateInput = new UpdateInput(qInstance); + updateInput.setSession(new QSession()); + updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 7))); + new UpdateAction().execute(updateInput); + + GetInput getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington")); + GetOutput getOutput = new GetAction().execute(getInput); + assertNotNull(getOutput.getRecord()); + assertNotNull(getOutput.getRecord().getValue("cachedDate")); + assertEquals(6, getOutput.getRecord().getValue("noOfShoes")); + + /////////////////////////////////////////////////////////////////////// + // then artificially move back the cachedDate in the cache table. // + // then re-get from cache table, and we should see the updated value // + /////////////////////////////////////////////////////////////////////// + updateInput = new UpdateInput(qInstance); + updateInput.setSession(new QSession()); + updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z")))); + new UpdateAction().execute(updateInput); + + getOutput = new GetAction().execute(getInput); + assertEquals(7, getOutput.getRecord().getValue("noOfShoes")); + } + + ///////////////////////////////////////////////// + // should only be 1 cache record at this point // + ///////////////////////////////////////////////// + assertEquals(1, TestUtils.queryTable(TestUtils.defineInstance(), TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE).size()); + + ////////////////////////////////////////////////////////////////////// + // delete the source record - it will still be in the cache though. // + ////////////////////////////////////////////////////////////////////// + { + DeleteInput deleteInput = new DeleteInput(qInstance); + deleteInput.setSession(new QSession()); + deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + deleteInput.setPrimaryKeys(List.of(1)); + new DeleteAction().execute(deleteInput); + + GetInput getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington")); + GetOutput getOutput = new GetAction().execute(getInput); + assertNotNull(getOutput.getRecord()); + + //////////////////////////////////////////////////////////////////// + // then artificially move back the cachedDate in the cache table. // + // then re-get from cache table, and now it should go away // + //////////////////////////////////////////////////////////////////// + UpdateInput updateInput = new UpdateInput(qInstance); + updateInput.setSession(new QSession()); + updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z")))); + new UpdateAction().execute(updateInput); + + getInput = new GetInput(qInstance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE); + getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington")); + getOutput = new GetAction().execute(getInput); + assertNull(getOutput.getRecord()); + } + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 84033870..11ae63ff 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -1180,8 +1180,7 @@ class QInstanceValidatorTest void testValidUniqueKeys() { assertValidationSuccess((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) - .withUniqueKey(new UniqueKey().withFieldName("id")) - .withUniqueKey(new UniqueKey().withFieldName("firstName").withFieldName("lastName"))); + .withUniqueKey(new UniqueKey().withFieldName("id"))); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 749896ac..55fe4ace 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -86,15 +86,19 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryTableBackendDetails; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; @@ -133,6 +137,7 @@ public class TestUtils public static final String PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport"; public static final String TABLE_NAME_PERSON_FILE = "personFile"; public static final String TABLE_NAME_PERSON_MEMORY = "personMemory"; + public static final String TABLE_NAME_PERSON_MEMORY_CACHE = "personMemoryCache"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; public static final String TABLE_NAME_BASEPULL = "basepullTest"; public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport"; @@ -164,6 +169,7 @@ public class TestUtils qInstance.addTable(defineTablePerson()); qInstance.addTable(definePersonFileTable()); qInstance.addTable(definePersonMemoryTable()); + qInstance.addTable(definePersonMemoryCacheTable()); qInstance.addTable(defineTableIdAndNameOnly()); qInstance.addTable(defineTableShape()); qInstance.addTable(defineTableBasepull()); @@ -608,6 +614,7 @@ public class TestUtils .withName(TABLE_NAME_PERSON_MEMORY) .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("firstName", "lastName")) .withFields(TestUtils.defineTablePerson().getFields())) .withField(standardQqqAutomationStatusField()) @@ -635,6 +642,36 @@ public class TestUtils + /******************************************************************************* + ** Define yet another version of the 'person' table, also in-memory, and as a + ** cache on the other in-memory one... + *******************************************************************************/ + public static QTableMetaData definePersonMemoryCacheTable() + { + UniqueKey uniqueKey = new UniqueKey("firstName", "lastName"); + return (new QTableMetaData() + .withName(TABLE_NAME_PERSON_MEMORY_CACHE) + .withBackendName(MEMORY_BACKEND_NAME) + .withBackendDetails(new MemoryTableBackendDetails() + .withCloneUponStore(true)) + .withPrimaryKeyField("id") + .withUniqueKey(uniqueKey) + .withFields(TestUtils.defineTablePerson().getFields())) + .withField(new QFieldMetaData("cachedDate", QFieldType.DATE_TIME)) + .withCacheOf(new CacheOf() + .withSourceTable(TABLE_NAME_PERSON_MEMORY) + .withCachedDateFieldName("cachedDate") + .withExpirationSeconds(60) + .withUseCase(new CacheUseCase() + .withType(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY) + .withSourceUniqueKey(uniqueKey) + .withCacheUniqueKey(uniqueKey) + .withCacheSourceMisses(false) + )); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index 606e0da6..4996b8d2 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -57,9 +57,14 @@ import com.kingsrook.qqq.backend.core.utils.QLogger; import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.module.api.exceptions.OAuthCredentialsException; +import com.kingsrook.qqq.backend.module.api.exceptions.OAuthExpiredTokenException; import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; +import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -69,6 +74,9 @@ import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONObject; @@ -122,9 +130,11 @@ public class BaseAPIActionUtil { try { - String urlSuffix = buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey()); - String url = buildTableUrl(table); - HttpGet request = new HttpGet(url + urlSuffix); + String urlSuffix = getInput.getPrimaryKey() != null + ? buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey()) + : buildUrlSuffixForSingleRecordGet(getInput.getUniqueKey()); + String url = buildTableUrl(table); + HttpGet request = new HttpGet(url + urlSuffix); GetOutput rs = new GetOutput(); QHttpResponse response = makeRequest(table, request); @@ -468,6 +478,8 @@ public class BaseAPIActionUtil *******************************************************************************/ protected void handleResponseError(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws QException { + checkForOAuthExpiredToken(table, request, response); + int statusCode = response.getStatusCode(); String resultString = response.getContent(); String errorMessage = "HTTP " + request.getMethod() + " for table [" + table.getName() + "] failed with status " + statusCode + ": " + resultString; @@ -486,6 +498,22 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** + *******************************************************************************/ + protected void checkForOAuthExpiredToken(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws OAuthExpiredTokenException + { + if(backendMetaData.getAuthorizationType().equals(AuthorizationType.OAUTH2)) + { + if(response.getStatusCode().equals(HttpStatus.SC_UNAUTHORIZED)) // 401 + { + throw (new OAuthExpiredTokenException("Expired token indicated by response: " + response)); + } + } + } + + + /******************************************************************************* ** method to build up a query string based on a given QFilter object ** @@ -511,13 +539,29 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** + *******************************************************************************/ + public String buildUrlSuffixForSingleRecordGet(Map uniqueKey) throws QException + { + QTableMetaData table = actionInput.getTable(); + QQueryFilter filter = new QQueryFilter(); + for(Map.Entry entry : uniqueKey.entrySet()) + { + filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue())); + } + return (buildQueryStringForGet(filter, 1, 0, table.getFields())); + } + + + /******************************************************************************* ** As part of making a request - set up its authorization header (not just ** strictly "Authorization", but whatever is needed for auth). ** ** Can be overridden if an API uses an authorization type we don't natively support. *******************************************************************************/ - protected void setupAuthorizationInRequest(HttpRequestBase request) + protected void setupAuthorizationInRequest(HttpRequestBase request) throws QException { switch(backendMetaData.getAuthorizationType()) { @@ -533,6 +577,10 @@ public class BaseAPIActionUtil request.addHeader("API-Key", backendMetaData.getApiKey()); break; + case OAUTH2: + request.setHeader("Authorization", "Bearer " + getOAuth2Token()); + break; + default: throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType()); } @@ -540,6 +588,66 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** + *******************************************************************************/ + public String getOAuth2Token() throws OAuthCredentialsException + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // check for the access token in the backend meta data. if it's not there, then issue a request for a token. // + // this is not generally meant to be put in the meta data by the app programmer - rather, we're just using // + // it as a "cheap & easy" way to "cache" the token within our process's memory... // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + String accessToken = ValueUtils.getValueAsString(backendMetaData.getCustomValue("accessToken")); + + if(!StringUtils.hasContent(accessToken)) + { + String fullURL = backendMetaData.getBaseUrl() + "oauth/token"; + String postBody = "grant_type=client_credentials&client_id=" + backendMetaData.getClientId() + "&client_secret=" + backendMetaData.getClientSecret(); + + LOG.info(session, "Fetching OAuth2 token from " + fullURL); + + try(CloseableHttpClient client = HttpClients.custom().setConnectionManager(new PoolingHttpClientConnectionManager()).build()) + { + HttpPost request = new HttpPost(fullURL); + request.setEntity(new StringEntity(postBody)); + request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + + HttpResponse response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + String resultString = EntityUtils.toString(entity); + if(statusCode != HttpStatus.SC_OK) + { + throw (new OAuthCredentialsException("Did not receive successful response when requesting oauth token [" + statusCode + "]: " + resultString)); + } + + JSONObject resultJSON = new JSONObject(resultString); + accessToken = (resultJSON.getString("access_token")); + LOG.debug(session, "Fetched access token: " + accessToken); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // stash the access token in the backendMetaData, from which it will be used for future requests // + /////////////////////////////////////////////////////////////////////////////////////////////////// + backendMetaData.withCustomValue("accessToken", accessToken); + } + catch(OAuthCredentialsException oce) + { + throw (oce); + } + catch(Exception e) + { + String errorMessage = "Error getting OAuth Token"; + LOG.warn(session, errorMessage, e); + throw (new OAuthCredentialsException(errorMessage, e)); + } + } + + return (accessToken); + } + + + /******************************************************************************* ** As part of making a request - set up its content-type header. *******************************************************************************/ @@ -727,8 +835,9 @@ public class BaseAPIActionUtil *******************************************************************************/ protected QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException { - int sleepMillis = getInitialRateLimitBackoffMillis(); - int rateLimitsCaught = 0; + int sleepMillis = getInitialRateLimitBackoffMillis(); + int rateLimitsCaught = 0; + boolean caughtAnOAuthExpiredToken = false; while(true) { @@ -768,6 +877,25 @@ public class BaseAPIActionUtil return (qResponse); } } + catch(OAuthCredentialsException oce) + { + LOG.error(session, "OAuth Credential failure for [" + table.getName() + "]"); + throw (oce); + } + catch(OAuthExpiredTokenException oete) + { + if(!caughtAnOAuthExpiredToken) + { + LOG.info(session, "OAuth Expired token for [" + table.getName() + "] - retrying"); + backendMetaData.withCustomValue("accessToken", null); + caughtAnOAuthExpiredToken = true; + } + else + { + LOG.info(session, "OAuth Expired token for [" + table.getName() + "] even after a retry. Giving up."); + throw (oete); + } + } catch(RateLimitException rle) { rateLimitsCaught++; @@ -781,6 +909,13 @@ public class BaseAPIActionUtil SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS); sleepMillis *= 2; } + catch(QException qe) + { + /////////////////////////////////////////////////////////////// + // re-throw exceptions that QQQ or application code produced // + /////////////////////////////////////////////////////////////// + throw (qe); + } catch(Exception e) { String message = "An unknown error occurred trying to make an HTTP request to [" + request.getURI() + "] on table [" + table.getName() + "]."; @@ -906,7 +1041,7 @@ public class BaseAPIActionUtil *******************************************************************************/ protected int getInitialRateLimitBackoffMillis() { - return (0); + return (500); } @@ -916,7 +1051,7 @@ public class BaseAPIActionUtil *******************************************************************************/ protected int getMaxAllowedRateLimitErrors() { - return (0); + return (3); } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java new file mode 100644 index 00000000..a418e1bf --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthCredentialsException.java @@ -0,0 +1,52 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.exceptions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** Exception to be thrown during OAuth Token generation. + ** + *******************************************************************************/ +public class OAuthCredentialsException extends QException +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public OAuthCredentialsException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public OAuthCredentialsException(String errorMessage, Exception e) + { + super(errorMessage, e); + } +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java new file mode 100644 index 00000000..6c68e98f --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/OAuthExpiredTokenException.java @@ -0,0 +1,54 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.exceptions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** Exception to be thrown by a request that uses OAuth, if the current token + ** is expired. Generally should signal that the token needs refreshed, and the + ** request should be tried again. + ** + *******************************************************************************/ +public class OAuthExpiredTokenException extends QException +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public OAuthExpiredTokenException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public OAuthExpiredTokenException(String errorMessage, Exception e) + { + super(errorMessage, e); + } +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java index f1852f53..92a4aaf7 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/exceptions/RateLimitException.java @@ -22,10 +22,13 @@ package com.kingsrook.qqq.backend.module.api.exceptions; +import com.kingsrook.qqq.backend.core.exceptions.QException; + + /******************************************************************************* ** *******************************************************************************/ -public class RateLimitException extends Exception +public class RateLimitException extends QException { /******************************************************************************* diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java index fbfde3b9..0c542373 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java @@ -30,5 +30,6 @@ public enum AuthorizationType API_KEY_HEADER, BASIC_AUTH_API_KEY, BASIC_AUTH_USERNAME_PASSWORD, + OAUTH2, } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java index 773b61f8..310dc704 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java @@ -39,6 +39,8 @@ public class APIBackendMetaData extends QBackendMetaData { private String baseUrl; private String apiKey; + private String clientId; + private String clientSecret; private String username; private String password; @@ -156,6 +158,74 @@ public class APIBackendMetaData extends QBackendMetaData + /******************************************************************************* + ** Getter for clientId + ** + *******************************************************************************/ + public String getClientId() + { + return clientId; + } + + + + /******************************************************************************* + ** Setter for clientId + ** + *******************************************************************************/ + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + + + /******************************************************************************* + ** Fluent setter for clientId + ** + *******************************************************************************/ + public APIBackendMetaData withClientId(String clientId) + { + this.clientId = clientId; + return (this); + } + + + + /******************************************************************************* + ** Getter for clientSecret + ** + *******************************************************************************/ + public String getClientSecret() + { + return clientSecret; + } + + + + /******************************************************************************* + ** Setter for clientSecret + ** + *******************************************************************************/ + public void setClientSecret(String clientSecret) + { + this.clientSecret = clientSecret; + } + + + + /******************************************************************************* + ** Fluent setter for clientSecret + ** + *******************************************************************************/ + public APIBackendMetaData withClientSecret(String clientSecret) + { + this.clientSecret = clientSecret; + return (this); + } + + + /******************************************************************************* ** Getter for username ** @@ -391,4 +461,16 @@ public class APIBackendMetaData extends QBackendMetaData { qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName()); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean requiresPrimaryKeyOnTables() + { + return (false); + } + } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index b0585069..bbe66c2d 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -414,7 +414,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface case IS_BLANK: { clause += " IS NULL"; - if(isString(field.getType())) + if(field.getType().isStringLike()) { clause += " OR " + column + " = ''"; } @@ -424,7 +424,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface case IS_NOT_BLANK: { clause += " IS NOT NULL"; - if(isString(field.getType())) + if(field.getType().isStringLike()) { clause += " AND " + column + " != ''"; } @@ -479,16 +479,6 @@ public abstract class AbstractRDBMSAction implements QActionInterface - /******************************************************************************* - ** - *******************************************************************************/ - private static boolean isString(QFieldType fieldType) - { - return fieldType == QFieldType.STRING || fieldType == QFieldType.TEXT || fieldType == QFieldType.HTML || fieldType == QFieldType.PASSWORD; - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 2d1916d2..409b8774 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -136,7 +136,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf // execute the query - iterate over results // ////////////////////////////////////////////// QueryOutput queryOutput = new QueryOutput(queryInput); - System.out.println(sql); + // System.out.println(sql); PreparedStatement statement = createStatement(connection, sql.toString(), queryInput); QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { diff --git a/qqq-dev-tools/bin/createTableToEntityFields.groovy b/qqq-dev-tools/bin/createTableToRecordEntity.groovy similarity index 95% rename from qqq-dev-tools/bin/createTableToEntityFields.groovy rename to qqq-dev-tools/bin/createTableToRecordEntity.groovy index 91af3078..a2afb2eb 100755 --- a/qqq-dev-tools/bin/createTableToEntityFields.groovy +++ b/qqq-dev-tools/bin/createTableToRecordEntity.groovy @@ -1,14 +1,20 @@ #!/usr/bin/env groovy /******************************************************************************* - ** Script to convert a list of columnNames from a CREATE TABLE statement - ** to fields for a QRecordEntity (on stdout) + ** Script to convert a CREATE TABLE statement to fields for a QRecordEntity *******************************************************************************/ +if (args.length < 1) +{ + System.out.println("Usage: ${this.class.getSimpleName()} EntityClassName [writeWholeClass] [writeTableMetaData]") + System.exit(1); +} + String className = args[0] boolean writeWholeClass = args.length > 1 ? args[1] : false; boolean writeTableMetaData = args.length > 2 ? args[2] : false; +println("Please paste in a CREATE TABLE statement (then a newline and an end-of-file (CTRL-D))") def reader = new BufferedReader(new InputStreamReader(System.in)) String line String allFieldNames = "" @@ -27,7 +33,6 @@ if(writeWholeClass) public class %s extends QRecordEntity { public static final String TABLE_NAME = "%s"; - """.formatted(className, className, classNameLcFirst)); }